Alexey
Alexey

Reputation: 7442

Cannot load script into iframe

Test page: https://jsfiddle.net/y25rk55w/

On this test page you can see 3 <iframe>'s embeded into each other. Each <iframe> contains a <script> tag in it's <head> tag.

The problem is: only the <script> in the first <iframe> will be loaded by the browser. The other two <script> tags will be present in the dom but the browser will never even try to load them. The problem is not browser specific, it can be reroduced in chrome, firefox, ie. The problem cannot be fixed by adding timeouts or waiting before appending the scripts. It seems to be important that all the iframes have programatically generated content; if you replace this iframes with iframes with actual src links, the problem will disappear.

The question is: how can I actually load a script into iframes 2 and 3?

Full test code:

// It doesn't matter if the scripts exist or not
// Browser won't try to load them either way
var scripts = [
    '//testdomain.test/script1.js',
    '//testdomain.test/script2.js',
    '//testdomain.test/script3.js'
];

function createIFrame(win, onCreated) {
    var iframe = win.document.createElement('iframe');
    iframe.onload = function () {
        onCreated(iframe);
    };
    win.document.body.appendChild(iframe);
}

function loadScript(win, url) {
    var script = win.document.createElement('script');
    script.src = url;
    script.onload = function() {
        console.log("Script " + url + " is loaded.");
    };
    win.document.getElementsByTagName('head')[0].appendChild(script);
}

createIFrame(window, function(iframe1) {
    loadScript(iframe1.contentWindow, scripts[0]);
    createIFrame(iframe1.contentWindow, function (iframe2) {
        loadScript(iframe2.contentWindow, scripts[1]);
        createIFrame(iframe2.contentWindow, function (iframe3) {
            loadScript(iframe3.contentWindow, scripts[2]);
        });
    });
});

Upvotes: 2

Views: 4043

Answers (3)

Louis
Louis

Reputation: 151511

I've been successful using a simpler method than what the OP proposes in the self-answer. I produce the URLs using:

new URL(scriptURL, window.location.href).toString();

where scriptURL is the URL that needs to be fixed to get a proper protocol and window is the parent of the iframe element that holds the scripts. This can take care of scenarios that differ from the OPs example URLs: like relative URLs (../foo.js) or absolute URLs that don't start with a host (/foo.js). The above code is sufficient in my case.

If I were to replicate the search through the window hierarchy that the OP used, I'd probably do something like the following. This is TypeScript code. Strip out the type annotations to get plain JavaScript.

function url(win: Window, path: string): string {
  // We search up the window hierarchy for the first window which uses
  // a protocol that starts with "http".
  while (true) {
    if (win.location.protocol.startsWith("http")) {
      // Interpret the path relative to that window's href. So the path
      // will acquire the protocol used by the window. And the less we
      // specify in `path`, the more it gets from the window. For
      // instance, if path is "/foo.js", then the host name will also be
      // acquired from the window's location.
      return new URL(path, win.location.href).toString();
    }

    // We searched all the way to the top and found nothing useful.
    if (win === win.parent) {
      break;
    }

    win = win.parent;
  }

  // I've got a big problem on my hands if there's nothing that works.
  throw new Error("cannot normalize the URL");
}

I don't have a default return value if the window chain yield nothings useful because that would indicate a much larger issue than the issue of producing URLs. There'd be something wrong elsewhere in my setup.

Upvotes: 0

Alexey
Alexey

Reputation: 7442

In the question you can see that I was ommiting the protocol:

/* This is valid to omit the http:/https: protocol.
   In that case, browser should automatically append 
   protocol used by the parent page */
var scripts = [
    '//testdomain.test/script1.js',
    '//testdomain.test/script2.js',
    '//testdomain.test/script3.js'
];

The thing is, programatically created iframes have protocol about: (or javascript:, depending on how you create them). I still can't explain why the first script was loading or why the other two scripts were not showing up in the network tab at all, but I guess it's not very important.

The solution: either explicitly use https:// or programatically append protocol using something like the following code:

function appendSchema(win, url) {
    if (url.startsWith('//')) {
        var protocol = 'https:';
        try {
            var wPrev = undefined;
            var wCur = win;
            while (wPrev != wCur) {
                console.log(wCur.location.protocol);
                if (wCur.location.protocol.startsWith("http")) {
                    protocol = wCur.location.protocol;
                    break;
                }
                wPrev = wCur;
                wCur = wCur.parent;
            }
        } catch (e) {
            /* We cannot get protocol of a cross-site iframe.
             * So in case we are inside cross-site iframe, and
             * there are no http/https iframes before it,
             * we will just use https: */
        }
        return protocol + url;
    }
    return url;
}

Upvotes: 1

Vladu Ionut
Vladu Ionut

Reputation: 8193

Your code is working fine --> http://plnkr.co/edit/vQGsyD7JxZiDlg6EZvK4?p=preview

Make sure you execute createIFrame on window.onload or DOMContentLoaded.

var scripts = [
    'https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.js',
    'https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.2/jquery.js',
    'https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js'
];

function createIFrame(win, onCreated) {
    var iframe = win.document.createElement('iframe');
    iframe.onload = function () {
        onCreated(iframe);
    };
    win.document.body.appendChild(iframe);
}

function loadScript(win, url) {
    var script = win.document.createElement('script');
    script.src = url;
    script.onload = function() {
        console.log("Script " + url + " is loaded.");
    };
    win.document.getElementsByTagName('head')[0].appendChild(script);
}
window.onload = function(){
createIFrame(window, function(iframe1) {
    loadScript(iframe1.contentWindow, scripts[0]);
    createIFrame(iframe1.contentWindow, function (iframe2) {
        loadScript(iframe2.contentWindow, scripts[1]);
        createIFrame(iframe2.contentWindow, function (iframe3) {
            loadScript(iframe3.contentWindow, scripts[2]);
        });
    });
});
};

Upvotes: 2

Related Questions