Hey @junkie25! Sounds like you’re running into issues related to content script environments or “worlds.” The short version is that a content script can either run in a separate JavaScript environment from the page (the extension’s ISOLATED
world) that shares access to the same underlying DOM tree, or in the same environment as the page’s own scripts (the MAIN
world).
While it’s possible for extension authors to pierce the barrier of these JS environments in Firefox using xray vision, I’d generally recommend against doing so. The xray vision API is a bit tricky to work with and even harder to do so safely. This technique also doesn’t work in other browsers.
Instead, I’d suggest using a pair of content scripts (a MAIN
and ISOLATED
script) to accomplish what you’re describing. The MAIN
script will handle monkey-patching the page’s JS environment while the ISOLATED
script will handle exchanging messages with your extension’s background context. All that’s left then is to exchange messages between worlds.
As of today there’s no WebExtensions platform-provided feature that allows scripts to communicate with each other across worlds. Instead, we have to lean on the existing features of the web platform to implement a message passing solution between these scripts. Extension developers commonly do this by transmitting data across worlds using a CustomEvent with a well-known name. Both scripts should be injected at document_start
(see runAt) in order to perform their setup before page scripts have an opportunity to load. Here’s a minimal example:
manifest.json
{
"name": "Cross-world Message Communications Demo",
"version": "1.0",
"manifest_version": 3,
"content_scripts": [
{
"matches": ["*://example.com/*"],
"js": ["content-main.js"],
"run_at": "document_start",
"world": "MAIN"
},
{
"matches": ["*://example.com/*"],
"js": ["content-isolated.js"],
"run_at": "document_start",
"world": "ISOLATED"
}
]
}
content-isolated.json
// Generate a random event name to (a) avoid conflicts with other scripts on the
// page and (b) prevent other scripts from easily monitoring our communications.
const customEventName = Math.random().toString(36).slice(2);
// Now that we have a custom event name, we can set up a session-specific
// listener.
window.addEventListener(`${customEventName}-isolated`, (event) => {
console.log('content-isolated.js received message:', event.detail);
});
// Now that we've set up our session-specific listener, we can pass the custom
// event name to the MAIN script so it can set up its own listener.
document.dispatchEvent(new CustomEvent('cwc-setup', {detail: customEventName}));
function sendMessage(detail) {
// Send a message to the main world
const event = new CustomEvent(`${customEventName}-main`, {detail});
window.dispatchEvent(event);
}
// Once MAIN binds it's own listeners for the session-specific messages, we can
// start sending messages.
sendMessage('Hello from the ISOLATED world!');
content-main.js
// This script is injected into the same JS context as all other page scripts.
// We use a IIFE to prevent our logic it from polluting that environment.
(() => {
// First, we must wait for the ISOLATED script to pass us the custom event
// name so we can set up a session-specific communication channel.
const customEventSetup = new Promise((resolve) => {
document.addEventListener('cwc-setup', (event) => {
event.stopImmediatePropagation();
resolve(event.detail);
}, {once: true, capture: true});
});
// Once we have the custom event name, we can start listening for messages
// from the ISOLATED script.
customEventSetup.then(customEventName => {
window.addEventListener.call(window, `${customEventName}-main`, (event) => {
console.log('content-main.js received message:', event.detail);
});
function sendMessage(detail) {
// Send a message to the isolated world
const event = new window.CustomEvent(`${customEventName}-isolated`, {detail});
window.dispatchEvent.call(window, event);
}
// Now that the session-specific communication channel is set up, we can
// start sending messages.
sendMessage('Hello from the MAIN world!');
});
})();
Finally, a brief note on security: if you are concerned about a website seeing the data you transmit back to your extension, you will have to take extra measures to harden your code. This includes protective measures in your ISOLATED
content script like saving unmodified references built-ins at document_start
(example from @rob) and treating any messages passed from your content scripts to your background context as untrusted (which you should do anyway).