I'm looking for a way to display an isolated overlay on some websites using WebExtensions.
An iframe would seem like the way to go for this as it provides a whole separate scope for css, js and the DOM. And another neat thing is that the target website won't be able to read or change the content.
In Chrome extensions that seems to be possible without any problems, but with WebExtensions in Firefox, even though they share the same syntax, I get security warnings/errors and it doesn't work.
I've tried two different things:
Creating an iframe without an src attribute and inject that into the body of website.
This method failed because I get CSP errors/warnings when I do iframe.contentWindow.document.open()
.
Relevant content-script code:
let html = `
<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>TEST</h1>
</body>
</html>
`
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
iframe.contentWindow.document.open()
iframe.contentWindow.document.write(html)
iframe.contentWindow.document.close()
The other thing I tried (which would make way more sense as it would disallow the website from accessing the content), was to put my iframe code into a file (overlay.html
) in my WebExtension and make the iframe load it by setting it's src to browser.extension.getURL('overlay.html')
.
Relevant content-script code:
let iframe = document.createElement('iframe')
iframe.src = browser.extension.getURL('overlay.html')
document.body.appendChild(iframe)
In the manifest I defined the overlay.html as web_accessible_resources
for this:
"web_accessible_resources": [
"overlay.html"
],
The thing about that is that the iframe simply doesn't load the overlay.html file. But it is definitely available, otherwise location.href = browser.extension.getURL('overlay.html')
wouldn't work. It would have been extremely convenient if that would have worked, as I could have stored the whole overlay (html, css, js) as separate files in my extension. As if it would be a standalone website. And using the content-script I could have accessed it to add new data or whatever.
Edit:
At the moment I'm using the srcdoc
attribute of my iframe to set the source code it should contain (thanks to wOxxOm for that). So at least I have something that works now. But what I really dislike about this approach is that I can't interact with the iframe content from my content script. Interestingly though, I can interact with the parent page from within the iframe's js code, but again not with the content script. It's also really messy, inconvenient and hard to maintain to put all your html, css, js code into one string instead of multiple files like a normal website.
The Problem
Firstly, we know the problem is a Security Issue, what exactly is the issue? Well when trying to load an extension resource into an iframe by setting the iframe src the browser complains about Security and prevents the iframe from connecting over a different protocol, in this instance 'moz-extension://'.
The Solution
Load the html etc from the extension context and inject as a string.
The Nitty Gritty
To get around this, we can set the src attribute of the iframe to data:text/html;charset=utf8,${markup}
.
This directly tells the iframe that the content is html, it uses utf8 encoding and it is followed by the raw markup. We're completely bypassing the need for the iframe to load any resources over the network.
The execution context of a Firefox content script is seperate from the page it has been loaded for. This means that you can make an xhr request without violating CSP.
If you make an xhr request to your markup, you can then get the content of the response as a string, and directly inject it into the iframe src attribute.
Thus the content script:
function loaded (evt) {
if (this.readyState === 4 && this.status === 200) {
var html = this.responseText;
console.log(html);
var iframe = document.createElement('iframe');
iframe.src = 'data:text/html;charset=utf-8,' + html;
document.body.appendChild(iframe);
console.log('iframe.contentWindow =', iframe.contentWindow);
} else {
console.log('problem loading');
}
}
var xhr = new XMLHttpRequest();
xhr.overrideMimeType("text/html");
xhr.open("GET", browser.extension.getURL('test.html'), false);
xhr.addEventListener("readystatechange", loaded);
xhr.send(null);
With a simple HTML file
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Test</title>
<meta charset="utf8" />
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
So now you have successfully injected a html template into the target iframe.
If you need any images, scripts, css files etc, you'll need to write a bootloader based on the method outlined above, injecting new script tags and so forth directly into the iframe document.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With