Working around Firefox's window.open() Same-origin policy with port.emit()

Firefox's Same-origin policy is an implementation of some standards from WHATWG that is stricter about cross-origin windows and iframes than Chrome and Safari.

When the two documents do not have the same origin, these references provide very limited access to Window and Location objects, as described in the next two sections.

I ran into this functionality via a "permission denied" error when calling window.open() in a Firefox extension. In mediathread-firefox, the content page's origin could be, for example, images.google.com or en.wikipedia.org. I wasn't clear about the pop-up window's origin. According to the Same-origin policy's documentation, my about:blank pop-up window should inherit the parent window's origin. However, according to someone on irc, this inheritance rule only applies to iframes.

Inherited origins

Content from about:blank, javascript: and data: URLs inherits the origin from the document that loaded the URL, since the URL itself does not give any information about the origin.

"Limited access to the Window object" described above isn't sufficient to do what the extension was trying to do: Add DOM elements to window.document.

I couldn't think of a way to get around this security policy. Windows, origins, and CORS rules get muddled up when running in a browser extension's content script. Because I knew it was possible and permitted to do this kind of thing from the main add-on code (in mediathread-firefox, this is everything in the src/ directory, as opposed to data/), I decided to see what was required to make it work that way instead.

It's not straightforward, since we don't have the DOM available on the add-on script side. But we can communicate between the two threads with port.emit() and port.on(). See the diagram here: Content_Scripts#Communicating_with_the_add-on.

The Add-on SDK has a few different modules to choose from to open a pop-up style dialog box: windows, panel. I chose to use panel, since calling windows.open() with this API opened a full browser window with toolbars and scrollbars, and I couldn't figure out how to hide them.

I'm transferring the HTML data that needs to be displayed in the pop-up with the collect event from the content script to the add-on script. The add-on script then opens a Panel that contains a content script. I use the form-payload event to transfer the form's HTML from the add-on script to the Panel's content script. It's confusing! On the add-on script side:

var worker = tabs.activeTab.attach({
    contentScriptFile: [
        self.data.url('./lib/jquery-2.1.4.min.js'),
        self.data.url('./lib/URI.js'),
        self.data.url('./src/collect-panel.js'),
        self.data.url('./src/common/settings.js'),
        self.data.url('./src/common/host-handler.js'),
        self.data.url('./src/common/asset-handler.js'),
        self.data.url('./src/common/collect.js'),
        self.data.url('./src/init.js')
    ],
    contentScriptWhen: 'ready'
});

worker.port.on('collect', function(payload) {
    var panel = Panel({
        width: 400,
        height: 400,
        contentURL: self.data.url('./collect-popup/index.html'),
        contentStyleFile: self.data.url('./collect-popup/style.css'),
        contentScriptFile: [
            self.data.url('./lib/jquery-2.1.4.min.js'),
            self.data.url('./collect-popup/popup.js')
        ]
    });
    panel.port.on('collect-cancel', function() {
        panel.hide();
    });
    panel.port.on('collect-submit', function() {
        // Tell the main content script about the submission so
        // it can display a notice.
        worker.port.emit('collect-submit');
        panel.hide();
    });

    panel.show();
    panel.port.emit('form-payload', payload.form);
});

The pop-up Panel's content script:

$(document).ready(function() {
    self.port.on('form-payload', function(form) {
        var $form = $(form);
        $form.find('input.cont').remove();
        $form.find('input.analyze').remove();
        $form.append(
            '<input type="hidden" value="cont" name="button">');
        $form.append('');
        $form.append('');
        $form.append(
            '<div class="help-text">' +
            'Clicking "Save" will add this item to your ' +
            'Mediathread collection and return you to ' +
            'collecting.' +
            '</div>');

        $('#bucket-wrap').append($form);

        $('#submit-cancel').click(function() {
            self.port.emit('collect-cancel');
        });

        $('form').on('submit', function() {
            self.port.emit('collect-submit');
        });
    });
});