Changes to unsafeWindow for the Add-on SDK

wbamberg

25

In Firefox 30 the Add-on SDK is making a change to the execution environment for content scripts. While this change will not affect most add-ons, some patterns will no longer work. Specifically, content scripts will no longer be able to use unsafeWindow or window.wrappedJSObject to make JavaScript objects available to content. The more common pattern of accessing objects in the page directly via unsafeWindow will continue to work as expected.

What we’re introducing instead are some new APIs that you can use to share functions and objects with content explicitly. While you’re working on migrating to these new APIs, there’s a mechanism you can use to switch your add-on back to the old behavior as a short-term measure.

Who’s affected?

Add-on SDK-based add-ons that use unsafeWindow or window.wrappedJSObject in content scripts to share JavaScript objects with the scripts loaded by a web page will be broken by this change.

By default, content scripts and the scripts loaded into web pages are insulated from each other. They can both see the DOM, but content scripts can’t access objects defined by page scripts, and page scripts can’t access objects defined by content scripts:

// content-script.js
var button = document.getElementById("show-page-script-var");
button.addEventListener("click", function() {
  // access object defined in page script
  console.log(window.pageScriptObject.greeting);        // undefined
}, false);
 
window.contentScriptObject = {"greeting" : "hello from add-on"};
// page-script.js
var button = document.getElementById("show-content-script-var");
 
button.addEventListener("click", function() {
  // access object defined by content script
  console.log(window.contentScriptObject.greeting);     // undefined
}, false);
 
// define object to be accessed by content script
window.pageScriptObject = {"greeting" : "hello from web page"};

Sometimes a content script wants to break through this insulation, and to do that it can use unsafeWindow (or window.wrappedJSObject, which is identical):

// content-script.js
var button = document.getElementById("show-page-script-var");
button.addEventListener("click", function() {
  // access object defined in page script
  console.log(unsafeWindow.pageScriptObject.greeting);  // "hello from web page"
}, false);
 
unsafeWindow.contentScriptObject = {"greeting" : "hello from add-on"};
// page-script.js
var button = document.getElementById("show-content-script-var");
 
button.addEventListener("click", function() {
  // access object defined by content script
  console.log(window.contentScriptObject.greeting);     // "hello from add-on"
}, false);
 
window.pageScriptObject = {"greeting" : "hello from web page"};

From Firefox 30 onwards, this mechanism won’t work any more to share objects from the content script to the page script:

// content-script.js
var button = document.getElementById("show-page-script-var");
button.addEventListener("click", function() {
  // access object defined in page script
  console.log(unsafeWindow.pageScriptObject.greeting);  // "hello from web page"
}, false);
 
unsafeWindow.contentScriptObject = {"greeting" : "hello from add-on"};
unsafeWindow.contentScriptPrimitive = 42;
// page-script.js
var button = document.getElementById("show-content-script-var");
 
button.addEventListener("click", function() {
  // access primitive defined by content script
  console.log(window.contentScriptPrimitive);            // 42
  // access object defined by content script
  console.log(window.contentScriptObject.greeting);     // undefined
}, false);
 
window.pageScriptObject = {"greeting" : "hello from web page"};

Again, the use of unsafeWindow to access variables defined in the page script is unaffected, so this change is asymmetric. Content scripts can still access page objects using unsafeWindow, but not vice versa.

Also note that the content script will still be able to share primitive values with the page using unsafeWindow, just not objects.

If the page script tries to read a variable defined using unsafeWindow, it will get the value undefined. If it tries to write to such a variable, the browser will throw an exception with this message: “Permission denied to access property [name of the property]”.

The workaround

In the short term, there’s a new option in package.json called “unsafe-content-script” under the “permissions” key. By setting it to true you can revert to the old behavior:

"permissions": {
  "unsafe-content-script": true
}

However, this is only a temporary fix to enable your add-on to keep working while you update to the new APIs. We will deprecate and eventually remove this flag.

The real fix

The real fix is to use three new APIs to share objects and functions with web content: cloneInto(), exportFunction(), and createObjectIn(). All three of these functions are made available to content scripts as globals.

cloneInto()

You can use cloneInto() to clone an object from the content script’s context into the page script’s context. cloneInto() creates a structured clone of the object in the target context, and returns a reference to the clone. You can then assign that to a property of the target window, and the page script can access it:

// content-script.js
contentScriptObject = {"greeting" : "hello from add-on"};
unsafeWindow.contentScriptObject = cloneInto(contentScriptObject, unsafeWindow);
// page-script.js
var button = document.getElementById("show-content-script-var");
 
button.addEventListener("click", function() {
  // access object defined by content script
  console.log(window.contentScriptObject.greeting);     // "hello from add-on"
}, false);

You can also assign the reference to an object you’ve created using createObjectIn():

var foo = createObjectIn(unsafeWindow, {defineAs: "foo"});
var contentScriptObject = {"greeting" : "hello from add-on"};
unsafeWindow.foo.contentScriptObject = cloneInto(contentScriptObject, unsafeWindow);
// page-script.js
var button = document.getElementById("show-content-script-var");
 
button.addEventListener("click", function() {
  // access object defined by content script
  console.log(window.foo.contentScriptObject.greeting); // "hello from add-on"
}, false);

exportFunction()

You can use exportFunction() to expose a function from a content script to a page script. In this way a function defined in a content script can be called by the page script. Any non-native arguments, and the return value, are cloned into the page script’s context.

You can export the function to the target’s window object:

// content-script.js
var salutation = "hello, ";
function greetme(user) {
  return salutation + user;
}
 
exportFunction(greetme, unsafeWindow, {defineAs: "greetme"});
// page-script.js
var button = document.getElementById("call-content-script-function");
 
button.addEventListener("click", function() {
  console.log(window.greetme("page script"));           // "hello, page script"
}, false);

You can also export the function to an object you’ve created in the page context using createObjectIn(). This code creates a new object in the page’s context called foo, and attaches the greetme() function to that object:

// content-script.js
var salutation = "hello, ";
function greetme(user) {
  return salutation + user;
}
 
var foo = createObjectIn(unsafeWindow, {defineAs: "foo"});
exportFunction(greetme, foo, {defineAs: "greetme"});
// page-script.js
var button = document.getElementById("call-content-script-function");
 
button.addEventListener("click", function() {
  console.log(window.foo.greetme("page script"));
}, false);

createObjectIn()

You can use createObjectIn() to create a new object in the page script’s context. You can then export objects and functions to that object, instead of the target scope’s global window. See the previous sections for some examples of this.

Structured cloning

These functions use the structured clone algorithm to clone objects into the page context. This is more capable than JSON serialization, but still has some serious limitations. Most notably, you can’t clone functions, so you can’t export functions that take functions as arguments (such as callbacks) and functions that return functions. However, we’re working on adding better support at least for common patterns like callbacks.

More details

To learn more about the new APIs see the reference documentation:

If you have any questions, please ask in the Jetpack mailing list or the #jetpack channel on irc.mozilla.org.

25 responses

  1. Sylvain Giroux wrote on :

    Thanks for the heads-up!

    While we understand the reason behind this change, it also means that we need to change multiple dozens of our Apps currently all around the web right now.

    This will take weeks of development for our company to create the adjustments needed as we are using wrappedJSObject all around our framework and our Mozilla Apps.

    And Firefox 30 is just here, peaking around the corner to get live in June 2014!

    Big weeks of work ahead of us for sure.

    I would like to know if others are as much affected by this change as we are.

    1. wbamberg wrote on :

      Thanks for your understanding! Please drop a mail to the Jetpack list if you need any help, we’ll give you as much support as we can.

    2. Nivesh wrote on :

      In Firefox Fox 29.0.1,after debugging my add-on i found the error message as ‘ERROR: Attempt to use .WrappedJSObject in untrusted code.It worked fine till FF28.What am I supposed to do now?Do i have to make use of cloneInto(), exportFunction(), or createObjectIn().

      1. wbamberg wrote on :

        The change this blog post discusses happened in Firefox 30, not Firefox 29. So I don’t think it’s the cause of your problem. This change: http://stackoverflow.com/questions/23835434/firefox-29-xpcom-and-wrappedjsobject seems more likely.

        Without knowing what your code is doing it’s hard to recommend a fix. But I do recommend that you ask in the Jetpack group (https://groups.google.com/forum/#!forum/mozilla-labs-jetpack) rather than here.

    3. nivesh wrote on :

      I have an issue with this from FF29.My company’s addon is broken and I’m trying to fix it.But couldn’t find a good solution till now.

  2. Sylvain Giroux wrote on :

    Question here :

    Can we assume the current Firefox nightly build 31.0a1 (2014-04-11) doesn’t support unsafeWindow or window.wrappedJSObject anymore?

    I would like to start doing some test with Nightly and start moving forward with the needed changes.

    Regards,

    Sylvain.

    1. wbamberg wrote on :

      This change is in the current Nightly. But please be clear: unsafeWindow is still supported for most uses, it’s only this specific use case that’s no longer supported.

  3. Sylvain Giroux wrote on :

    Another question.

    Will this affect the XUL-based Add-ons as well?

    1. wbamberg wrote on :

      No: since XUL add-ons don’t use content scripts at all, this change won’t affect them.

      1. Sylvain Giroux wrote on :

        Thank you! As most of our Addons are XUL-based, I would seem we do not have to change anything on our side.

        Our Apps seem to works properly under the latest nightly build.

  4. oliezekat wrote on ::

    Questions about exportFunction():

    1/ Your example see exported function call a variable of “content” but what’s going on if “content” change this variable value after to export this function…
    If “Page” call this function, is it call of content’s variable reference or call of cloned variable ?

    2/ While exporting function, could we name it to replace existed function into the target scope ?

    3/ While exporting function, could we name it “postMessage” into the target scope ?
    Perhaps this tip could fix problem of window.postMessage in extensions…

    My addon “YMobActus” use addEventListener(,,,true) with Mozilla specific argument to receive data from an iframe.
    I feel new functions could replace this weird argument requirement.

    1. wbamberg wrote on :

      I’m not the real expert on this, but I think:

      1. If a content script changes the variable after it has exported the function, then the page script will get the original value when it calls the function, not the redefined version:

      // content-script.js
      function foo() { return “the original!”;}
      exportFunction(foo, unsafeWindow, {defineAs: “foo”});
      foo = function() { return “redefined!”;}

      console.log(foo()); // redefined!

      // page-script.js
      console.log(window.foo()); // the original!

      2. Yes.

      3. Yes.

      But really I’d recommend asking questions like this in the Jetpack list.

      1. oliezekat wrote on ::

        I’m not surprised exportFunction clone any used global variables. That’s mean we may export function again each time these global variables has changed.

        Isn’t any way Content give to Page a reference of its variables/objects ? That’s we need to fix problem of window.postMessage in extensions.

  5. dbullman wrote on ::

    After your last update four of my most important add-ons for university won’t work anymore, what can I do ??????????????????

    1. Jeff Griffiths wrote on ::

      I would ask for help either in #addons on irc.mozilla.org or on the forums:

      https://forums.mozilla.org/addons/

  6. Hasan Basri Jumin wrote on ::

    MS Word is always problem

  7. chinhtran wrote on :

    Firefox 30 is really bad. They changed the core without warning. I can’t dispatchEvent from contentscript to web page. widgets are removed with error “null object”. My system down because clients updated from 29 to 30 (100 computers).

  8. brand wrote on :

    Ah thank you so much! I was wondering when you were going to break my extension once again, forcing me to re-write a huge portion of it. =) This is why I hate Chrome, you write your extension once and it works for years. Boring.

    No wonder why you are gaining marketshare like crazy.

  9. al wrote on :

    Need `new Proxy(unsafeWindow)` implements for backward.

  10. Angry anon wrote on :

    Congratulations, you’ve messed up a significant part of Greasemonkey scripts.
    You really ought to leave a “make it back” workaround.

  11. rajesh wrote on :

    I inject content script in all frames + top frame.. and inside the contentscript use…
    unsafwindow.top == unsafewindow.self to detect toplevel frame ….as window.top == window.self does not work…..and there’s no way to detect top level frame inside the content script known to me…..

    so can I still use this code or there is better way to detect top level frame inside a content script….

    1. wbamberg wrote on :

      I think you will still be able to use that code. Please let us know on the mailing list (https://groups.google.com/forum/#!forum/mozilla-labs-jetpack) if it doesn’t work.

  12. lukesUbuntu wrote on :

    I totally agree i don’t have time to rewrite extensions, this last update has broken heaps of my extensions its stupid

  13. Ivan wrote on :

    HI.

    How can i detect content window is frame or not?
    I tried unsafeWindow.self, unsafeWindow.top and script just stop working.

  14. Disco wrote on :

    I’ve lost track of the number of times I’ve had to downgrade Firefox to regain disabled functionality. I am so sick and tired of this crap. I use a greasemonkey script I wrote to make enhancements to an intranet application, so security is not an issue. However, at some point, I do want to make sure my code works with current versions of the browser and the add-on.

    What I would like to see is a (!!!SIMPLE!!!) example of how to convert this code:

    unsafeWindow.MyFunction = function() { …do stuff… }

    To whatever is the current approved syntax (that will work for a little while and then get completely upended and will have to again be rewritten).