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.
Sylvain Giroux wrote on
wbamberg wrote on
Nivesh wrote on
wbamberg wrote on
nivesh wrote on
Sylvain Giroux wrote on
wbamberg wrote on
Sylvain Giroux wrote on
wbamberg wrote on
Sylvain Giroux wrote on
oliezekat wrote on
wbamberg wrote on
oliezekat wrote on
dbullman wrote on
Jeff Griffiths wrote on
Hasan Basri Jumin wrote on
chinhtran wrote on
brand wrote on
al wrote on
Angry anon wrote on
rajesh wrote on
wbamberg wrote on
lukesUbuntu wrote on
Ivan wrote on
Disco wrote on