Accessing iframe from chrome extension

2019-01-08 23:30发布

问题:

I'm developing a chrome extension and bumped into a big problem.

I'm using content scripts to inject my javascript code on a web site. The web site has an iframe. I can change the source code of the iframe but don't seem to get any access to the iframe's contentWindow property. I need it to insert text at the current carret position.

So basically this code works perfectly in the context of the page:

$("#iframe1").contentWindow.document.execCommand("InsertHTML", false, 'test text');

But when I try it to run in the context of my chrome extension I get this error:

TypeError: Cannot read property 'document' of undefined

What's strange is that I can access the html of the iframe. So this code works perfectly from the chrome extension:

$("#iframe1").contents().find('div').html('test')

I tried putting "all_frames": true in the manifest file but no luck :(

回答1:

To understand why your code does not work, I include a fragment of my previous answer:

Content scripts do not have any access to a page's global window object. For content scripts, the following applies:

  • The window variable does not refer to the page's global object. Instead, it refers to a new context, a "layer" over the page. The page's DOM is fully accessible. #execution-environment

Given a document consisting of   <iframe id="frameName" src="http://domain/"></iframe>:

  • Access to the contents of a frame is restricted by the Same origin policy of the page; the permissions of your extension does not relax the policy.
  • frames[0] and frames['frameName'], (normally referring to the the frame's containing global window object) is undefined.
  • var iframe = document.getElementById('frameName');
    • iframe.contentDocument returns a document object of the containing frame, because content scripts have access to the DOM of a page. This property is null when the Same origin policy applies.
    • iframe.contentDocument.defaultView (refers to the window object associated with the document) is undefined.
    • iframe.contentWindow is undefined.

Solution for same-origin frames

In your case, either of the following will work:

// jQuery:
$("#iframe1").contents()[0].execCommand( ... );

// VanillaJS
document.getElementById("iframe1").contentDocument.execCommand( ... );

// "Unlock" contentWindow property by injecting code in context of page
var s = document.createElement('script');
s.textContent = 'document.getElementById("iframe1").contentWindow.document.execCommand( ... );';
document.head.appendChild(s);

Generic solution

The generic solution is using "all_frames": true in the manifest file, and use something like this:

if (window != top) {
    parent.postMessage({fromExtension:true}, '*');
    addEventListener('message', function(event) {
        if (event.data && event.data.inserHTML) {
            document.execCommand('insertHTML', false, event.data.insertHTML);
        }
    });
} else {
    var test_html = 'test string';
    // Explanation of injection at https://stackoverflow.com/a/9517879/938089 :
    // Run code in the context of the page, so that the `contentWindow`
    //  property becomes accessible
    var script = document.createElement('script');
    script.textContent = '(' + function(s_html) {
        addEventListener('message', function(event) {
            if (event.data.fromExtension === true) {
                var iframe = document.getElementById('iframe1');
                if (iframe && (iframe.contentWindow === event.source)) {
                    // Window recognised, post message back
                    iframe.contentWindow.postMessage({insertHTML: s_html}, '*');
                }
            }
        });
    } + ')(' + JSON.stringify(test_html) + ');';
    (document.head||document.documentElement).appendChild(script);
    script.parentNode.removeChild(script);
}

This demo is for educational purposes only, do not use this demo in a real extension. Why? Because it uses postMessage to pass messages around. These events can also be generated by the client, which causes a security leak (XSS: arbitrary HTML injection).

The alternative to postMessage is Chrome's message API. For a demo, see this answer. You won't be able to compare the window objects though. What you can do is to rely the window.name property. The window.name property is automatically set to the value of the iframe's name attribute (just once, when the iframe is loaded).