Interacting with page scripts

We are planning to deprecate the use by Firefox add-ons of the techniques described in this document.

Don't use these techniques to develop new add-ons. Use WebExtensions instead.

If you maintain an add-on which uses the techniques described here, consider migrating it to use WebExtensions instead.

Add-ons developed using these techniques might not work with multiprocess Firefox (e10s), which is already the default in Firefox Nightly and Firefox Developer Edition, and will soon be the default in Beta and Release versions of Firefox. We have documentation on making your add-ons multiprocess-compatible, but it will be more future-proof for you to migrate to WebExtensions.

A wiki page containing resources, migration paths, office hours, and more, is available to help developers transition to the new technologies.

By default, content scripts loaded by add-ons and scripts loaded by web pages are insulated from each other:

  • content scripts can't interact directly with page scripts or access JavaScript objects they create
  • page scripts can't interact directly with content scripts or access objects they create.

For the sake of security, stability, and simplicity, this is usually what you want. You don't want arbitrary web pages to be able to access objects in content scripts, and you don't want objects created by content scripts to clash with objects created by page scripts.

But sometimes, you will want to share objects between the two scopes. This guide describes:

  • how to share objects between content scripts and page scripts
  • how to send messages between content scripts and page scripts

Sharing objects with page scripts

There are two possible cases here:

  • a content script might want to access an object defined by a page script
  • a content script might want to expose an object to a page script

Access objects defined by page scripts

To access page script objects from content scripts, you can use the global unsafeWindow object. With unsafeWindow you can see JavaScript objects that have been defined by page scripts, and if a page script has modified the behavior of native DOM functions, you'll get the modified version of them as well.

In this example a page script adds a string variable foo to the window:

<!DOCTYPE html">
<html>
  <head>
    <script>
    window.foo = "hello from page script"
    </script>
  </head>
</html>

The content script can see this object if it uses unsafeWindow.foo instead of window.foo:

// main.js

var tabs = require("sdk/tabs");
var mod = require("sdk/page-mod");
var self = require("sdk/self");

var pageUrl = self.data.url("page.html")

var pageMod = mod.PageMod({
  include: pageUrl,
  contentScript: "console.log(unsafeWindow.foo);"
})

tabs.open(pageUrl);

Be careful using unsafeWindow: you can't rely on any of its properties or functions being, or doing, what you expect. Any of them, even setters and getters, could have been redefined by a page script. Don't use it unless you trust the page, and even then be careful.

Also, unsafeWindow isn't a supported API, so it could be removed or changed in a future version of the SDK.

Expose objects to page scripts

Until Firefox 30, you could use unsafeWindow to perform the reverse procedure, and make objects defined in content scripts available to page scripts:

// content-script.js

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);

After Firefox 30, you can still do this for primitive values, but can no longer do it for objects. Instead, you need to use the global cloneInto() function to clone the object into the page script's scope. 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.

Here's an example. The "main.js"  opens the local file "page.html" and attaches a content script to it:

// main.js

var tabs = require("sdk/tabs");
var self = require("sdk/self");

tabs.open({
  url: self.data.url("page.html"),
  onReady: attachScript
});

function attachScript(tab) {
  tab.attach({
    contentScriptFile: self.data.url("content-script.js")
  });
}

The content script defines an object and assigns it to unsafeWindow twice: the first time using cloneInto(), the second time using simple assignment:

// content-script.js

var contentScriptObject = {"greeting" : "hello from add-on"};

unsafeWindow.clonedContentScriptObject = cloneInto(contentScriptObject, unsafeWindow);
unsafeWindow.assignedContentScriptObject = contentScriptObject;

The "page.html" file adds two buttons and assigns an event listener to each: one listener displays a property of the cloned object, and the other listener displays a property of the assigned object:

<html>
  <head>
  </head>
  <body>
    <input id="works" type="button" value="I will work"/>
    <input id="fails" type="button" value="I will not work"/>

    <script>
      var works = document.getElementById("works");
      works.addEventListener("click", function() {
        alert(clonedContentScriptObject.greeting);
      }, false);

      var fails = document.getElementById("fails");
      fails.addEventListener("click", function() {
        alert(assignedContentScriptObject.greeting);
      }, false);
    </script>

  </body>
</html>

If you run the example, clicking "I will work" displays the value of "greeting" in an alert. Clicking "I will not work" fails, and the following message is logged:

Permission denied to access property 'greeting'

Expose functions to page scripts

The structured clone algorithm is a bit more powerful than simple JSON serialization, but not much: in particular, functions aren't cloned. This means that you can't expose a function to a page script using cloneInto(), and if you clone an object into the page script scope, its methods will not be available to the page script.

To expose a function defined in a content script to a page script so the page script can call it, use exportFunction().

Here's an example. The "main.js"  opens the local file "page.html" and attaches a content script to it:

// main.js

var tabs = require("sdk/tabs");
var self = require("sdk/self");

tabs.open({
  url: self.data.url("page.html"),
  onReady: attachScript
});

function attachScript(tab) {
  tab.attach({
    contentScriptFile: self.data.url("content-script.js")
  });
}

The content script defines a function greetme() and exports it to the page script context. Note that the function's closure will be exported, too:

// content-script.js

var salutation = "hello, ";
function greetme(user) {
  return salutation + user;
}
 
exportFunction(greetme, unsafeWindow, {defineAs: "greetme"});

Finally, the "page.html" file adds a button and a page script that calls the exported function when the user pushes the button:

<html>
  <head>
  </head>
  <body>
    <input id="test" type="button" value="Click me"/>
    <script>
      var test = document.getElementById("test");
      test.addEventListener("click", function() {
        alert(window.greetme("page script"));
      }, false);
    </script>
  </body>
</html>

exportFunction() works by structured cloning the arguments and return value of the function from one scope to the other. Since structured cloning doesn't work for functions, this means that you can't export functions that take functions as arguments (such as callbacks) or functions that return functions.

Create objects in page script scope

Finally, a content script can use the createObjectIn() function to create an object in the page script's scope. It can then clone objects or export functions as members of that new object. For example, in the example above the content script could be written like so:

// content-script.js

var salutation = "hello, ";
function greetme(user) {
  return salutation + user;
}
 
var foo = createObjectIn(unsafeWindow, {defineAs: "foo"});
exportFunction(greetme, foo, {defineAs: "greetme"});

This creates a new object foo in the page script scope, and exports greetme() as a member of foo. Now the page script can call the function as foo.greetme():

<html>
  <head>
  </head>
  <body>
    <input id="test" type="button" value="Click me"/>
    <script>
      var test = document.getElementById("test");
      test.addEventListener("click", function() {
        alert(window.foo.greetme("page script"));
      }, false);
    </script>
  </body>
</html>

Reverting to the old behavior

You can switch Firefox back to the old behavior in which content scripts could expose objects and functions to page scripts using unsafeWindow. To do this, add an option called “unsafe-content-script” under the “permissions” key in package.json, and set it to true.

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

This is only a temporary migration aid, and will be removed eventually.

Communicating with page scripts

There are two different ways a content script can communicate with a page script:

Using the DOM postMessage API

Note that before Firefox 31 code in content scripts can't use window to access postMessage() and addEventListener() and instead must use document.defaultView. See the section below on using postMessage() before Firefox 31.

You can communicate between the content script and page scripts using window.postMessage().

Content script to page script

Suppose we have a page called "listen.html" hosted at "my-domain.org", and we want to send messages from the add-on to a script embedded in that page.

In the main add-on code, we have a page-mod that attaches the content script "talk.js" to the right page:

var data = require("sdk/self").data;

var pageMod = require("sdk/page-mod");
pageMod.PageMod({
  include: "http://my-domain.org/listen.html",
  contentScriptFile: data.url("talk.js")
});

The "talk.js" content script uses window.postMessage() to send the message to the page:

// talk.js
window.postMessage("Message from content script", "http://my-domain.org/");

The second argument may be '*' which will allow communication with any domain.

Finally, "listen.html" uses window.addEventListener() to listen for messages from the content script:

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <script>
      window.addEventListener('message', function(event) {
        window.alert(event.data);  // Message from content script
      }, false);
    </script>
  </body>
 
</html>

Page script to content script

Sending messages from the page script to the content script is just the same, but in reverse.

Here "main.js" creates a page-mod that attaches "listen.js" to the web page:

var data = require("sdk/self").data;

var pageMod = require("sdk/page-mod");
pageMod.PageMod({
  include: "http://my-domain.org/talk.html",
  contentScriptFile: data.url("listen.js")
});

The web page "talk.html" embeds a script that uses window.postMessage() to send the content script a message when the user clicks a button:

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <script>
      function sendMessage() {
        window.postMessage("Message from page script", "http://my-domain.org/");
      }
    </script>
    <button onclick="sendMessage()">Send Message</button>
  </body>
 
</html>

Finally, the content script "listen.js" uses window.addEventListener() to listen for messages from the page script:

// listen.js
window.addEventListener('message', function(event) {
  console.log(event.data);    // Message from page script
  console.log(event.origin);
}, false);

postMessage() before Firefox 31

If your add-on is running in a version of Firefox before Firefox 31, then your content script can't access the postMessage() or addEventListener() APIs using window, but must access them using document.defaultView instead. So the content scripts in the above examples need to be rewritten like this:

// talk.js
document.defaultView.postMessage("Message from content script", "http://my-domain.org/");
// listen.js
document.defaultView.addEventListener('message', function(event) {
  console.log(event.data);    // Message from page script
  console.log(event.origin);
}, false);

Using Custom DOM Events

As an alternative to using postMessage() you can use custom DOM events to communicate between page scripts and content scripts.

Content script to page script

From Firefox 30 onwards, the execution environment for content scripts has changed, so content scripts can't directly share objects with page scripts. This affects the use of custom events to send messages from content scripts to page scripts.

Before Firefox 30

Here's an example showing how to use custom DOM events to send a message from a content script to a page script, before Firefox 30.

First, "main.js" will create a page-mod that will attach "content-script.js" to the target web page, and will then load the target web page:

var tabs = require("sdk/tabs");
var mod = require("sdk/page-mod");
var self = require("sdk/self");

var pageUrl = self.data.url("page.html")

var pageMod = mod.PageMod({
  include: pageUrl,
  contentScriptFile: self.data.url("content-script.js"),
  contentScriptWhen: "ready"
})

tabs.open(pageUrl);

The target web page "page.html" includes a button and a page script:

<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <input id="message" type="button" value="Send a message"/>
    <script type="text/javascript" src="page-script.js"></script>
  </body>
</html>

The content script "content-script.js" adds an event listener to the button, that sends a custom event containing a message:

var messenger = document.getElementById("message");
messenger.addEventListener("click", sendCustomEvent, false);

function sendCustomEvent() {
  var greeting = {"greeting" : "hello world"};
  var event = new CustomEvent("addon-message", { bubbles: true, detail: greeting });
  document.documentElement.dispatchEvent(event);
}

Finally, the page script "page-script.js" listens for the message and logs the greeting to the Web Console:

window.addEventListener("addon-message", function(event) {
  console.log(event.detail.greeting);
}, false);

After Firefox 30: clone the message object

This technique depends on being able to share the message payload between the content script scope and the page script scope. From Firefox 30 this sharing requires an extra step: the content script needs to explicitly clone the message payload into the page script's scope using the global cloneInto() function:

var messenger = document.getElementById("message");
messenger.addEventListener("click", sendCustomEvent, false);

function sendCustomEvent() {
  var greeting = {"greeting" : "hello world"};
  var cloned = cloneInto(greeting, document.defaultView);
 var event = new CustomEvent("addon-message", { bubbles: true, detail: cloned }); 
  document.documentElement.dispatchEvent(event);
}

Page script to content script

Sending messages using custom DOM events from the page script to the content script is just the same, but in reverse. Also, there's no need to clone the message when using custom DOM events in this direction.

In this example, "main.js" creates a page-mod to target the page we are interested in:

var data = require("sdk/self").data;

var pageMod = require("sdk/page-mod");
pageMod.PageMod({
  include: "http://my-domain.org/talk.html",
  contentScriptFile: data.url("listen.js")
});

The web page "talk.html" creates and dispatches a custom DOM event, using initCustomEvent()'s detail parameter to supply the payload:

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <script>
      function sendMessage() {
        var event = document.createEvent('CustomEvent');
        event.initCustomEvent("addon-message", true, true, { hello: 'world' });
        document.documentElement.dispatchEvent(event);
      }
    </script>
    <button onclick="sendMessage()">Send Message</button>
  </body>
</html>

Finally, the content script "listen.js" listens for the new event and retrieves the payload from its detail attribute:

window.addEventListener("addon-message", function(event) {
  console.log(JSON.stringify(event.detail));
}, false);

Document Tags and Contributors

 Last updated by: wbamberg,