Even though it is not recommended, JavaScript allows you to access elements by their id without the use of getElementById.
<iframe id="myframe" />
<script>
var i = window.myframe;
</script>
This presents a problem for extensions that need to access said elements when they themselves are being accessed. The way to do this is to call Object.defineProperty() on the prototype for getElementById so that when it is called, you can then access the element. But when referencing an element directly getElementById is not called so consequently, they are a blind spot for the extension. I've tried the following but without any luck.
Does anyone know the internal mechanism that JavaScript uses when an element is referenced like this? I'm curious if there is another function that it calls internally which could possibly be hooked like getElementById. Or is there a way to ensure the content script code is run after the DOM loads but before the page scripts runs?
Or is there a way to ensure the content script code is run after the DOM loads but before the page scripts runs?
A problem is that page scripts can run before the DOM is fully loaded. For example, below:
<div id="foo"></div>
<script>
foo.textContent = 'foo';
</script>
<div id="bar"></div>
the script will run before the bar
element is created (before the DOM is loaded).
It's possible to find all elements with IDs currently in the DOM with [id]
, and it's possible to reassign those properties on the window by simply reassigning the property - or, if you want to run code when the element is retrieved, you can use Object.defineProperty
to turn it into a getter. You need to be able to find the IDs and reassign them before scripts load, which can be accomplished with a subtree MutationObserver - right before a <script>
is inserted, iterate over the [id]
s in the DOM, and reassign them:
console.log('(empty JS block inserted by Stack Snippet editor)');
<script>
new MutationObserver((mutations) => {
// Check to see if a script was inserted
if (mutations.every(
mutation => [...mutation.addedNodes].every(
addedNode => addedNode.tagName !== 'SCRIPT'
)
)) {
// No scripts inserted
return;
}
console.log('id reassignment running');
for (const elm of document.querySelectorAll('[id]')) {
Object.defineProperty(window, elm.id, {
get() {
console.log('Custom code running');
return elm;
},
configurable: true
});
}
})
.observe(document.body, { childList: true, subtree: true });
</script>
<div id="foo"></div>
<script>
console.log('Lower script about to reference foo');
foo.textContent = 'foo';
console.log('Lower script has finished referencing foo');
</script>
<div id="bar"></div>
It can benefit from some polishing, but that's the general idea.