I'm developing a Chrome extension to add convenience to a website.
I have access to the page's DOM, but I also need to interact with the "first-party" JS on that page, which I cannot do from my extension.
I can inject arbitrary tags into the page (most notably also <script> tags), but since escaping strings like
{
html: '<div onclick="doSomething(this, \'someName\')"></div>'
}
is a real pain, I'd like to keep the injected code at an absolute minimum.
I tried injecting event listeners into the page in order to fetch JS variables from the page, but ran into a problem.
It seems that if a CustomEvent is passed from an extension to a website or back, and if CustomEvent.detail contains certain types of objects (at least functions and errors) somewhere, the entire CustomEvent.detail will be purged, i.e. set to null.
Example
Script (extension.js):
(function()
{
var script = document.createElement('script');
script.innerHTML = [
"window.addEventListener('xyz', function(ev)",
" { ",
" console.log('after dispatch:'); ",
" console.log(ev.detail); ",
" }); ",
].join('\n');
document.head.appendChild(script);
// JSON-serializable data
var e = new CustomEvent('xyz', { detail: { x: 42, name: 'Schroedinger' } });
console.log('before dispatch:')
console.log(e.detail);
window.dispatchEvent(e);
// non-JSON-serializable data
var detail = { x: 42, name: 'Schroedinger' };
detail.detail = detail; // Create circular reference
e = new CustomEvent('xyz', { detail: detail });
console.log('before dispatch:')
console.log(e.detail);
window.dispatchEvent(e);
// data with function
e = new CustomEvent('xyz', { detail: { x: 42, name: 'Schroedinger', func: function(){} } });
console.log('before dispatch:');
console.log(e.detail);
window.dispatchEvent(e);
// data with error object
e = new CustomEvent('xyz', { detail: { x: 42, name: 'Schroedinger', err: new Error() } });
console.log('before dispatch:');
console.log(e.detail);
window.dispatchEvent(e);
})();
Output (paragraphed for readability):
before dispatch:
Object {x: 42, name: "Schroedinger"}
after dispatch:
Object {x: 42, name: "Schroedinger"}
before dispatch:
Object {x: 42, name: "Schroedinger", detail: Object}
after dispatch:
Object {x: 42, name: "Schroedinger", detail: Object}
before dispatch:
Object {x: 42, name: "Schroedinger", func: function (){}}
after dispatch:
null
before dispatch:
Object {x: 42, name: "Schroedinger", err: Error at chrome-extension://...}
after dispatch:
null
I initially thought JSON-serializability was the issue, but circular references pass just fine in events, when they would break if JSON-serialized.
It feels like certain objects "taint" the event detail the same way non-crossorigin images taint canvases, except there's nothing in the console.
I was unable to find any documentation regarding this behaviour, and (as Paul S. suggested), there does not seem to be a "privilege" for that on the Chrome permissions list.
Tested in Chrome 40.0.2214.115m, 43.0.2357.124m and 48.0.2547.0-dev.
What I found out
I initially thought this was a security feature, mostly because Firefox behaves that way.
The reason I put the above in a quote is because this is somewhat different in Chrome!
After some deeper investigation it looks like although the extension and the page share the DOM tree, they exist in two different contexts.
I don't know whether this is actually a security feature or just a technical consequence, but this, of course, has the consequence that only clonable objects can be passed back and forth.
What puzzles me though is the fact that the operation silently fails, when, according to the HTML standard, §2.7.5 (structured clone), the entire operation should fail with an error:
Workaround
I ended up using a fairly easy (although not so pretty) workaround:
In Chrome, there's no equivalent to
mozIJSSubScriptLoader, but you're allowed to append<script>tags to a page from within your extension (you're not allowed to do that in FF).Together with
chrome.extension.getURL, that can be used to run a JS file packaged with the extension in the context of the page:Of course that requires that
is set in
manifest.json, which isn't pretty, but shouldn't be an actual problem.The drawback of this is, of course, that from within
extension.jsyou no longer have access to any chrome API your extension has access to, but in my case I didn't need that. It wouldn't be too difficult to set up a proxy viaCustomEventfor that though, as the biggest part of the Chrome API only requires and returns data that is clonable.