Content Scripts

Many add-ons need to access and modify the content of web pages. But the main add-on code doesn't get direct access to web content. Instead, SDK add-ons need to factor the code that gets access to web content into separate scripts that are called content scripts. This page describes how to develop and implement content scripts.

Content scripts can be one of the more confusing aspects of working with the SDK, but you're very likely to have to use them. There are five basic principles:

  • the add-on's main code, including "main.js" and other modules in "lib", can use the SDK high-level and low-level APIs, but can't access web content directly
  • content scripts can't use the SDK's APIs (no access to globals exports, require) but can access web content
  • SDK APIs that use content scripts, like page-mod and tabs, provide functions that enable the add-on's main code to load content scripts into web pages
  • content scripts can be loaded in as strings, but are more often stored as separate files under the add-on's "data" directory. jpm doesn't make a "data" directory by default, so you must add it and put your content scripts in there.
  • a message-passing API allows the main code and content scripts to communicate with each other

This complete add-on illustrates all of these principles. Its "main.js" attaches a content script to the current tab using the tabs module. In this case the content script is passed in as a string. The content script simply replaces the content of the page:

// main.js
var tabs = require("sdk/tabs");
var contentScriptString = 'document.body.innerHTML = "<h1>this page has been eaten</h1>";'

tabs.activeTab.attach({
  contentScript: contentScriptString
});

The following high-level SDK modules can use content scripts to modify web pages:

  • page-mod: enables you to attach content scripts to web pages that match a specific URL pattern.
  • tabs: exports a Tab object for working with a browser tab. The Tab object includes an attach() function to attach a content script to the tab.
  • page-worker: lets you retrieve a web page without displaying it. You can attach content scripts to the page, to access and manipulate the page's DOM.
  • context-menu: use a content script to interact with the page in which the menu is invoked.

Additionally, some SDK user interface components - panel, sidebar, frame - are specified using HTML, and use separate scripts to interact with this content. In many ways these are like content scripts, but they're not the focus of this article. To learn about how to interact with the content for a given user interface module, please see the module-specific documentation: panel, sidebar, frame.

Almost all the examples presented in this guide are available as complete, but minimal, add-ons in the addon-sdk-content-scripts repository on GitHub.

Loading content scripts

You can load a single script by assigning a string to either the contentScript or the contentScriptFile option. The contentScript option treats the string itself as a script:

// main.js

var pageMod = require("sdk/page-mod");
var contentScriptValue = 'document.body.innerHTML = ' +
                         ' "<h1>Page matches ruleset</h1>";';

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScript: contentScriptValue
});

The contentScriptFile option treats the string as a resource:// URL pointing to a script file stored in your add-on's data directory. jpm doesn't make a "data" directory by default, so you must add it and put your content scripts in there.

This add-on supplies a URL pointing to the file "content-script.js", located in the data subdirectory under the add-on's root directory:

// main.js

var data = require("sdk/self").data;
var pageMod = require("sdk/page-mod");

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScriptFile: data.url("content-script.js")
});
// content-script.js

document.body.innerHTML = "<h1>Page matches ruleset</h1>";

From Firefox 34 onwards, you can use "./content-script.js" as an alias for self.data.url("content-script.js"). So you can rewrite the above main.js code like this:

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

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScriptFile: "./content-script.js"
});

Unless your content script is extremely simple and consists only of a static string, don't use contentScript: if you do, you may have problems getting your add-on approved on AMO.

Instead, keep the script in a separate file and load it using contentScriptFile. This makes your code easier to maintain, secure, debug and review.

You can load multiple scripts by passing an array of strings to either contentScript or contentScriptFile:

// main.js

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

tabs.on('ready', function(tab) {
  tab.attach({
      contentScript: ['document.body.style.border = "5px solid red";', 'window.alert("hi");']
  });
});
// main.js

var data = require("sdk/self").data;
var pageMod = require("sdk/page-mod");

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScriptFile: [data.url("jquery.min.js"), data.url("my-content-script.js")]
});

If you do this, the scripts can interact directly with each other, just like scripts loaded by the same web page.

You can also use contentScript and contentScriptFile together. If you do this, scripts specified using contentScriptFile are loaded before those specified using contentScript. This enables you to load a JavaScript library like jQuery by URL, then pass in a simple script inline that can use jQuery:

// main.js

var data = require("sdk/self").data;
var pageMod = require("sdk/page-mod");

var contentScriptString = '$("body").html("<h1>Page matches ruleset</h1>");';

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScript: contentScriptString,
  contentScriptFile: data.url("jquery.js")
});

Unless your content script is extremely simple and consists only of a static string, don't use contentScript: if you do, you may have problems getting your add-on approved on AMO.

Instead, keep the script in a separate file and load it using contentScriptFile. This makes your code easier to maintain, secure, debug and review.

Controlling when to attach the script

The contentScriptWhen option specifies when the content script(s) should be loaded. It takes one of:

  • "start": load the scripts immediately after the document element for the page is inserted into the DOM. At this point the DOM content hasn't been loaded yet, so the script won't be able to interact with it.
  • "ready": load the scripts after the DOM for the page has been loaded: that is, at the point the DOMContentLoaded event fires. At this point, content scripts are able to interact with the DOM content, but externally-referenced stylesheets and images may not have finished loading.
  • "end": load the scripts after all content (DOM, JS, CSS, images) for the page has been loaded, at the time the window.onload event fires.

The default value is "end".

Note that tab.attach() doesn't accept contentScriptWhen, because it's generally called after the page has loaded.

Passing configuration options

The contentScriptOptions is a JSON object that is exposed to content scripts as a read-only value under the self.options property:

// main.js

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

tabs.on('ready', function(tab) {
  tab.attach({
      contentScript: 'window.alert(self.options.message);',
      contentScriptOptions: {"message" : "hello world"}
  });
});

Any kind of jsonable value (object, array, string, etc.) can be used here.

Accessing the DOM

Content scripts can access the DOM of a page, of course, just like any scripts that the page has loaded (page scripts). But content scripts are insulated from page scripts:

  • content scripts don't see any JavaScript objects added to the page by page scripts
  • if a page script has redefined the behavior of some DOM object, the content script sees the original behavior.

The same applies in reverse: page scripts can't see JavaScript objects added by content scripts.

For example, consider a page that adds a variable foo to the window object using a page script:

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

Another script loaded into the page after this script will be able to access foo. But a content script will not:

// 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(window.foo);"
})

tabs.open(pageUrl);
console.log: my-addon: null

There are good reasons for this insulation. First, it means that content scripts don't leak objects to web pages, potentially opening up security holes. Second, it means that content scripts can create objects without worrying about whether they might clash with objects added by page scripts.

This insulation means that, for example, if a web page loads the jQuery library, then the content script won't be able to see the jQuery object added by the library - but the content script can add its own jQuery object, and it won't clash with the page script's version.

Interacting with page scripts

Usually the insulation between content scripts and page scripts is what you want. But sometimes you might want to interact with page scripts: you might want to share objects between content scripts and page scripts or to send messages between them. If you need to do this, read about interacting with page scripts.

Event listeners

You can listen for DOM events in a content script just as you can in a normal page script, but there are two important differences:

First, if you define an event listener by passing it as a string into setAttribute(), then the listener is evaluated in the page's context, so it will not have access to any variables defined in the content script.

For example, this content script will fail with the error "theMessage is not defined":

var theMessage = "Hello from content script!";
anElement.setAttribute("onclick", "alert(theMessage);");

Second, if you define an event listener by direct assignment to a global event handler like onclick, then the assignment might be overridden by the page. For example, here's an add-on that tries to add a click handler by assignment to window.onclick:

var myScript = "window.onclick = function() {" +
               "  console.log('unsafewindow.onclick: ' + window.document.title);" +
               "}";

require("sdk/page-mod").PageMod({
  include: "*",
  contentScript: myScript,
  contentScriptWhen: "start"
});

This will work fine on most pages, but will fail on pages which also assign to onclick:

<html>
  <head>
  </head>
  <body>
    <script>
    window.onclick = function() {
      window.alert("it's my click now!");
    }
    </script>
  </body>
</html>

For these reasons, it's better to add event listeners using addEventListener(), defining the listener as a function:

var theMessage = "Hello from content script!";

anElement.onclick = function() {
  alert(theMessage);
};

anotherElement.addEventListener("click", function() {
  alert(theMessage);
});

Communicating with the add-on

To enable add-on scripts and content scripts to communicate with each other, each end of the conversation has access to a port object.

  • to send messages from one side to the other, use port.emit()
  • to receive messages sent from the other side, use port.on()

Messages are asynchronous: that is, the sender does not wait for a reply from the recipient but just emits the message and continues processing.

Here's a simple add-on that sends a message to a content script using port:

// main.js

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

tabs.on("ready", function(tab) {
  var worker = tab.attach({
    contentScriptFile: self.data.url("content-script.js")
  });
  worker.port.emit("alert", "Message from the add-on");
});

tabs.open("http://www.mozilla.org");
// content-script.js

self.port.on("alert", function(message) {
  window.alert(message);
});

The context-menu module doesn't use the communication model described here. To learn about communicating with content scripts loaded using context-menu, see the context-menu documentation.

Accessing port in the content script

In the content script the port object is available as a property of the global self object. So to emit a message from a content script:

self.port.emit("myContentScriptMessage", myContentScriptMessagePayload);

To receive a message from the add-on code:

self.port.on("myAddonMessage", function(myAddonMessagePayload) {
  // Handle the message
});

Note that the global self object is completely different from the self module, which provides an API for an add-on to access its data files and ID.

Accessing port in the add-on script

In the add-on code, the channel of communication between the add-on and a particular content script context is encapsulated by the worker object. So the port object for communicating with a content script is a property of the corresponding worker object.

However, the worker is not exposed to add-on code in quite the same way in all modules.

From page-worker

The page-worker object integrates the worker API directly. So to receive messages from a content script associated with a page-worker you use pageWorker.port.on():

// main.js

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

var pageWorker = require("sdk/page-worker").Page({
  contentScriptFile: self.data.url("content-script.js"),
  contentURL: "http://en.wikipedia.org/wiki/Internet"
});

pageWorker.port.on("first-para", function(firstPara) {
  console.log(firstPara);
});

To emit user-defined messages from your add-on you can just call pageWorker.port.emit():

// main.js

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

var pageWorker = require("sdk/page-worker").Page({
  contentScriptFile: self.data.url("content-script.js"),
  contentURL: "http://en.wikipedia.org/wiki/Internet"
});

pageWorker.port.on("first-para", function(firstPara) {
  console.log(firstPara);
});

pageWorker.port.emit("get-first-para");
// content-script.js

self.port.on("get-first-para", getFirstPara);

function getFirstPara() {
  var paras = document.getElementsByTagName("p");
  if (paras.length > 0) {
    var firstPara = paras[0].textContent;
    self.port.emit("first-para", firstPara);
  }
}

From page-mod

A single page-mod object might attach its scripts to multiple pages, each with its own context in which the content scripts are executing, so it needs a separate channel (worker) for each page.

So page-mod does not integrate the worker API directly. Instead, each time a content script is attached to a page, the page-mod emits an attach event, whose listener is passed the worker for that context. By supplying a listener to attach you can access the port object for content scripts attached to that page by this page-mod:

// main.js

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

var pageMod = pageMods.PageMod({
  include: ['*'],
  contentScriptFile: self.data.url("content-script.js"),
  onAttach: startListening
});

function startListening(worker) {
  worker.port.on('click', function(html) {
    worker.port.emit('warning', 'Do not click this again');
  });
}
// content-script.js

window.addEventListener('click', function(event) {
  self.port.emit('click', event.target.toString());
  event.stopPropagation();
  event.preventDefault();
}, false);

self.port.on('warning', function(message) {
  window.alert(message);
});

In the add-on above there are two messages:

  • click is sent from the page-mod to the add-on, when the user clicks an element in the page
  • warning sends a silly string back to the page-mod

From Tab.attach()

The Tab.attach() method returns the worker you can use to communicate with the content script(s) you attached.

This add-on adds a button to Firefox: when the user clicks the button, the add-on attaches a content script to the active tab, sends the content script a message called "my-addon-message", and listens for a response called "my-script-response":

//main.js

var tabs = require("sdk/tabs");
var buttons = require("sdk/ui/button/action");
var self = require("sdk/self");

buttons.ActionButton({
  id: "attach-script",
  label: "Attach the script",
  icon: "./icon-16.png",
  onClick: attachScript
});

function attachScript() {
  var worker = tabs.activeTab.attach({
    contentScriptFile: self.data.url("content-script.js")
  });
  worker.port.on("my-script-response", function(response) {
    console.log(response);
  });
  worker.port.emit("my-addon-message", "Message from the add-on");
}
// content-script.js

self.port.on("my-addon-message", handleMessage);

function handleMessage(message) {
  alert(message);
  self.port.emit("my-script-response", "Response from content script");
}

The port API

See the reference page for the port object.

The postMessage API

Before the port object was added, add-on code and content scripts communicated using a different API:

  • the content script called self.postMessage() to send and self.on() to receive
  • the add-on script called worker.postMessage() to send and worker.on()to receive

The API is still available and documented, but there's no reason to use it instead of the port API described here. The exception is the context-menu module, which still uses postMessage.

Content script to content script

Content scripts can only communicate with each other directly if they have been loaded into the same context. For example, if a single call to Tab.attach() attaches two content scripts, then they can see each other directly, just as page scripts loaded by the same page can. But if you call Tab.attach() twice, attaching a content script each time, then these content scripts are not loaded into the same context and must communicate with each other using the normal methods of communicating from one context to another. One option is that you can relay messages through the main add-on code using the port API with the sending context script sending a message to the main add-on code and the the main add-on code sends the message to the other content script. This will work regardless of the context in which the content script was loaded.

In the special case where the two content scripts are loaded into the same exact page, it is possible for the content scripts to communicate directly to each other using the DOM postMessage() API or a CustomEvent. The following add-on shows a content script added by page-mod receiving a CustomEvent sent from a context-menu item when the context menu item is clicked. The page-mod script will then issue an alert with the URL of the link on which the context menu was displayed. The URL is passed to the page-mod script in the CustomEvent.

var pageMod = require("sdk/page-mod");
pageMod.PageMod({
  include: "*.mozilla.org",
  contentScript: 'function contextMenuAlert(href) {'
               + '    window.alert("The context menu was clicked on URL:\\n" + href);'
               + '};'
               + 'window.addEventListener("myAddonId-contextMenu-clicked",'
               + '    function(event){contextMenuAlert(event.detail);});'
});

let cm = require("sdk/context-menu");
cm.Item({
    label: "Alert URL",
    context: [
        cm.URLContext(["*.mozilla.org"]),
        cm.SelectorContext("a[href]")
    ],
    contentScript: 'self.on("click", function (node, data) {'
                 + '    var event = new CustomEvent("myAddonId-contextMenu-clicked",'
                 + '                                {detail:node.href});'
                 + '    window.dispatchEvent(event);'
                 + '});'
});

Cross-domain content scripts

By default, content scripts don't have any cross-domain privileges. In particular, they can't access content hosted in an iframe, if that content is served from a different domain, or make cross-domain XMLHttpRequests.

However, you can enable these features for specific domains by adding them to your add-on's package.json under the "cross-domain-content" key, which itself lives under the "permissions" key. See the article on cross-domain content scripts.