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.
btw, sometimes people use IFRAME url in different protocol, i.e.
https:// (while current page is http:// ).
So, IFRAME url should be relative, like src="//example.com"
Related
Currently we are trying to access the HTML of (with javascript) dynamically generated html-elements (e.g. to get the url of an image). If the html-elements are generated with javascript only - their is no problem. Just extracting the image url with javascript by accessing the DOM elements. But now we have trouble with an iframe.
This is the situation at the moment:
We include external script-file (www.test.com/script.js) to create a gallery on our website
The script.js generates a new iframe in our website (document.write('iframe code here')); referencing to www.test.com/iframe.html)
The iframe.html contains javascript-code again to generate the gallery by creating serveral dom-elements (divs, imgs, a, p,....)
Now we need to read the src-attribute of these images. With the debugging tool of the browser, it is no problem. Without the iframe, it's also no problem. But with the iframe, because of the cross domain policy of the browsers we can not access the html of the iframe.html with javascript.
One possible solution was to try to get the src of the iframe tag with javascript, call a server-side script to get the html content of the src-url and run the content via eval()-function on the client again.
I hope you have other better ways to solve that.
It is not clear from your question, but if the iframe is being served by your app, you can use postMessage in order to communicate between the iframe and its parent window.
The crossrider sidepanel is simply an iframe (you can use js-injected html, but I'm interested in using an iframe to reduce interference with the rest of the page). I'm having trouble getting any interaction between my browser extension and the iframe at all.
I see no point at all in adding a sidepanel with an extension unless you can do some basic JS communication. In this case I want a few options, checkboxes etc, in the iframe which control the extension. Since this plugin exists, I'm assuming there must be a way.
Ideally I'd like to have some basic input handling js in the child iframe and have it send back the odd save/load command. Is the answer really some form of message passing? If so which API should I be using here?
I believe this is related: Accessing iframe from chrome extension
[EDIT]
OK, so I've tried a few things...
It seems the expected usage is to host the iframe's html content somewhere. A bit strange considering it should be local and part of the extension. What happens if you want to view some pages offline?? This is just silly and I'm dismissing it as an option. Why waste resources hosting something that should just be available locally.
The alternative is to provide the HTML that goes in the sidebar. Note that this HTML doesn't get put in an iframe. I like the idea of an iframe because it keeps the CSS and JS very separate, so there's minimal interference between a page and your extension.
So I tried creating an iframe via the html sidebar attribute with and ID and injected the content after a 100ms delay using myiframe.contentWindow.document.open/writeln/close(). This works fine on chrome but fails in firefox with a security error (The operation is insecure on open()).
Another way is to provide the iframe content via the src url (for the sidebar I use a data address for the url attribute): Html code as IFRAME source rather than a URL. This works in firefox but results in a CORS error in chrome: The frame requesting access has a protocol of "http", the frame being accessed has a protocol of "data". Protocols must match. and Warning: Blocked a frame with origin "http://localhost" from accessing a cross-origin frame. Function-name: appAPI.message.addListener
These CORS issues strike me as really daft. It's all my code coming from the same extension, injected into the same page. There's no cross origin happening, I created the damn thing. If I have the power to change the origin, then it's not secure in the first place so why bother.
Assuming you are using the url sidebar property to load your sidebar's HTML (i.e. a hosted web page), you can use the extension's Run in Iframe feature to communicate between the iframe extension and the parent window's extension.
To achieve this, first enable the extension to run in iframes (Settings > Run in Iframes) and then you can use the extension.js to load your sidebar and handle messaging. For example, the following code loads a page that has a button with identification btnSave:
Hosted web page file:
<html>
<head>
</head>
<body>
<div id="mySidebar">
My sidebar form
<br />
<button id="btnSave">Save</button>
</div>
</body>
</html>
extension.js file:
appAPI.ready(function($) {
// Check if running in iframe and the sidebar page loaded
if (appAPI.dom.isIframe() && $('#mySidebar').length) {
// Set click handler for button to send message to parent window
$('#btnSave').click(function() {
appAPI.message.toCurrentTabWindow({
type:'save',
data:'My save data'
});
});
// End of Iframe code ... exit
return;
}
// Parent window message listener
appAPI.message.addListener(function(msg) {
if (msg.type === 'save') {
console.log('Extn:: Parent received data: ' +
appAPI.JSON.stringify(msg.data));
}
});
// Create the sidebar
var sidebar = new appAPI.sidebar({
position:'right',
url: 'http://yourdomain.com/sidebar_page.html',
title:{
content:'Sidebar Title',
close:true
},
opacity:1.0,
width:'300px',
height:'650px',
preloader:true,
sticky:true,
slide:150,
openAction:['click', 'mouseover'],
closeAction:'click',
theme:'default',
scrollbars:false,
openOnInstall:true,
events:{
onShow:function () {
console.log("Extn:: Show sidebar event triggered");
},
onHide:function () {
console.log("Extn:: Hide sidebar event triggered");
}
}
});
});
However, if you are using the html sidebar property to load your sidebar's HTML, then this solution will not work since the extension does not run in this context. However, you may be able to utilize the methods described in the StackOverflow thread you quoted to communicate with the parent window (this would be browser specific) that in turn can communicate with the extension using our CrossriderAPI event.
[Disclaimer: I am a Crossrider employee]
I want to achieve the fallowing goal.I want by the click of a button to get the domain name entered in one input and load it into an iframe. What i have trouble with is after i load the specific site in the iframe how can i get the DOM of the loaded site in the iframe ? I need to save the exact path of an element of my choosing from the newly loaded site in the iframe by clicking on it. Any help would be much appreciated. Thank you.
You can access the <iframe>'s contents via the contentDocument property.
var iFrameContent = $('myIFrame')[0].contentDocument;
$(iFrameContent).find('.mySelector').text();
I should also point out accessing the <iframe>'s contents can only be done if the src of the <iframe> is from the same domain as your script. If the src of the <iframe> is from any other domain, the browser will deny access to the <iframe>contents.
This is a CORS consideration.
UPDATE:
To get around the CORS limitation, you'll have to write a server-side proxy for the URL that is the src of the <iframe> (and it could be pretty simple).
If I were to do something like this in ASP.Net (as an example), I would write a web service that:
takes the actual URL as a String parameter.
Uses the HTTPWebRequest object (or similar) to get the URL contents.
Return it back to the browser.
Then you could set your <iframe> src to "http://mysite.com/getURLService?url=www.lalala.com" and get the contents because the contents are now delivered by "http://mysite.com".
You use .contents()
like $("iframe").contents().find(); etc
I have a simple html page. It has a iframe to some other site. I want to change the color of anchor tag that is nesteed in that Iframe. is is possible to access Elements of Iframe via javascript
If the other page is in another domain, due to cross-domain security, it will not be possible to edit HTML content of an iframe from the main page.
There are workaround for this, such as writing the change you want to make in the url. But this is really dirty.
If it is in the same domain, i suggest using, as example:
$('div', $('iframe')[0].contentWindow.document)
for getting all div elements inside your iframe
I did it by using following code
function loadFrame(){
document.getElementById('pcl_frame').contentWindow.document.getElementsByTagName('a')[0].style.color='blue';
}
You need JavaScript. It is the same as doing it in the parent page, except you must prefix your JavaScript command with the name of the iframe.
Remember, the same origin policy applies, so you can only do this to iframe is coming from your own server.
frame1.$('mydiv').style.border='1px solid #000000'
or
frame1.$('mydiv').addClassName('withborder')
You can get the values of the elemets inside the iframe using
$('#iframeId').contents().find('#id-of-element-inside-iframe');
But the values cannot be altered.
There is just one simple solution, which will only work when you own the content in the iframe.
In your parent source add:
<script type="text/javascript">
var innerDocument = null;
</script>
In your iframe add:
<script type="text/javascript">
parent.innerDocument = document;
</script>
When the iframe is loaded you can now target the iframe's document by using innerDocument.
This circumvents the cross-domain security.
I can't overcome this issue. Can someone give some advice?
I have this application that uses ExtJS library that I will need to run in Chrome extension. I have successfully created my messaging bridge (postMessage), and sandboxed the whole application in it, and everything works as usual. ExtJS loaded, application is running.
Then I have this piece of logic where I need to preview a piece of HTML snippets in my ExtJS viewport. I created an iframe in the Panel itself and on afterrender I tried to write the snippet in it. This is the code I use:
html: '<iframe src="about:blank" style="width:100%;height:100%;border:none;"></iframe>';
......
//p is the panel found in afterrender
p.body.down('iframe').dom.contentDocument.write(content);
Then the error:
Unsafe JavaScript attempt to access frame with URL about:blank from frame with URL chrome-extension://fcnpmlgapilgclcelfanblpbglmkghbc/core/themes/default/app.html. Domains, protocols and ports must match.
I have tried with postMessage within sandbox to this dynamic iframe but nothing happens. Setting the sandbox attribute in manifest doesn't work either. This is the only way and it works. See my answer below.
Question:
How should one set the manifest to support this kind of use case?
Or is there any better way to preview HTML snippet without using an iframe? Afaik previewing with iframe is the best as it sandboxed the snippet without being messed with parent css.
Note
This piece of code was working fine in manifest v1 but I planned to migrate it to manifest v2. I didn't realize Content Security Policy (CSP) has became that strict.
A screen to describe the problem ;)
Sorry, this is awkward. Apparently postMessage is the only way, and I can't manage to get it work previously because my iframe is not loaded yet. Also, accessing the dom document in the iframe is a big no under CSP, but it's possible to access contentWindow to do a postMessage.
This is what I did to solve this issue. Hope someone would benefit from this:
Create a preview.html in your extension root
Under manifest.json, add it as part of the sandbox attribute
Inside the preview.html, add the following code. Note that my snippet is a full html, so I used document.write instead.
<!DOCTYPE html>
<html>
<head>
<script>
window.addEventListener("message", function(e) {
if (e.data.content) {
document.write(e.data.content);
}
});
</script>
</head>
<body></body>
</html>
Create the iframe in your code as usual, point it to your preview.html, and attach it to the parent div or Ext.Panel.
Use the following code to postMessage after the element has been drawn/created/appended.
var content = '<!DOCTYPE html><html><body>Hello World!</body></html>';
var iframe = p.body.down('iframe').dom; //or the iframe node
iframe.onload = function() {
iframe.contentWindow.postMessage({content: content}, '*');
};
Enjoy ;)
Edit
As pointed by Mike in Chromium forum, this issue can be solved by srcdoc as well. Simply set the iframe's srcdoc and problem is solved.
Just not sure of the status of srcdoc