Interaction between privileged and non-privileged pages

Sending data from unprivileged document to chrome

An easy way to send data from a web page to an extension is by using custom DOM events. In your extension's browser.xul overlay, write code which listens for a custom DOM event. Here we call the event MyExtensionEvent.

var myExtension = {
  myListener: function(evt) {
    alert("Received from web page: " +
          evt.target.getAttribute("attribute1") + "/" +
          evt.target.getAttribute("attribute2"));
  }
}
document.addEventListener("MyExtensionEvent", function(e) { myExtension.myListener(e); }, false, true);
// The last value is a Mozilla-specific value to indicate untrusted content is allowed to trigger the event.

The data from the web page (unprivileged code) will be the values of attribute1 and attribute2. To trigger the alert() in the listener and pass the data from the web page, write code such as this in the web page:

var element = document.createElement("MyExtensionDataElement");
element.setAttribute("attribute1", "foobar");
element.setAttribute("attribute2", "hello world");
document.documentElement.appendChild(element);

var evt = document.createEvent("Events");
evt.initEvent("MyExtensionEvent", true, false);
element.dispatchEvent(evt);

This code creates an arbitrary element -- <MyExtensionDataElement/> -- and inserts it into the web page's DOM. Values are set for two arbitrary attributes on the element. These can also be named anything you like, but we've chosen attribute1 and attribute2. Finally, the code creates and dispatches a custom event named MyExtensionEvent -- similar to the standard DOM click event you catch with onclick handlers. The event bubbles up from the web page and reaches the extension (privileged code) where your listener catches it and reads the attribute values from the DOM element where the event originated.

(To better ensure others do not also implement the same event with a different meaning, one might either attach a namespace to <MyExtensionDataElement/> and check on the event handler for the correct namespaceURI property, or as per the DOM specification, use initEvent() with an event name that is itself namespaced (XML Name characters only): "It is also strongly recommended that third parties adding their own events use their own prefix to avoid confusion and lessen the probability of conflicts with other new events.")

In the case where your extension's overlay does not interact directly with browser.xul, such as in a sidebar, it might be easier to add the event listener to the top-level document directly as shown below (also see: accessing the elements of the top-level document from a child window).

var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                  .getInterface(Components.interfaces.nsIWebNavigation)
                  .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
                  .rootTreeItem
                  .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                  .getInterface(Components.interfaces.nsIDOMWindow);
mainWindow.document.addEventListener("MyExtensionEvent", function(e) { myExtension.myListener(e); }, false, true);

If you need to to pass lots of data, consider using CDATA sections instead of the simple attributes on a custom element.

Note: If you're using HTML5's postMessage() to send a message from unprivileged code to privileged code, adding 'true' to the end of your event listener in your privileged chrome code will allow the message to be received.

document.addEventListener("message", function(e) { yourFunction(e); }, false, true);

Sending data from chrome to unprivileged document

To "answer" the web page (e.g., return code), your extension can set an attribute or attach child elements on the event target element (<MyExtensionDataElement/> in this example).

You can optionally clean up the created element, or create it once when the web page loads then re-use it each time.

Another option is to send a return event from the extension to the web page. This can be done using the same principle as the above example.

There is only one extension, but there can be many active web pages. So to trigger the right event on the right page we have to tell the extension which page to call. The information we need for that is contained in evt.target.ownerDocument.

We can extend the above example with some data transfer from the extension to the web page. In the following code sample 2 methods are combined: Setting an extra attribute in the original event target element, and creating a new event message with a new event target element. For this to work we need to define the original target element globally. We need a new event trigger in the web page and some code to show the event message actually arrived. In the extension we have to dispatch an event message to the right web page.

The code containing the callback could look like this:

In the extension:

var myExtension =
{
  myListener: function(evt)
  {
    alert("Received from web page: " + 
           evt.target.getAttribute("attribute1") + "/" +
           evt.target.getAttribute("attribute2"));

/* the extension answers the page*/
    evt.target.setAttribute("attribute3", "The extension");

    var doc = evt.target.ownerDocument;

    var AnswerEvt = doc.createElement("MyExtensionAnswer");
    AnswerEvt.setAttribute("Part1", "answers this.");

    doc.documentElement.appendChild(AnswerEvt);

    var event = doc.createEvent("HTMLEvents");
    event.initEvent("MyAnswerEvent", true, false);
    AnswerEvt.dispatchEvent(event);
  }
}

document.addEventListener("MyExtensionEvent", function(e) { myExtension.myListener(e); }, false, true);
// The last value is a Mozilla-specific value to indicate untrusted content is allowed to trigger the event.

In the web page:

document.addEventListener("MyAnswerEvent",function(e) { ExtensionAnswer(e); },false);

var element;

function CallExtension()
{
  var element = document.createElement("MyExtensionDataElement");
  element.setAttribute("attribute1", "foobar");
  element.setAttribute("attribute2", "hello world");
  document.documentElement.appendChild(element);
  var evt = document.createEvent("Events");
  evt.initEvent("MyExtensionEvent", true, false);
  element.dispatchEvent(evt);
}

function ExtensionAnswer(EvtAnswer)
{
  alert(element.getAttribute("attribute3") + " " +
        EvtAnswer.target.getAttribute("Part1"));
}

Basic example of similar idea, extension passes information via attributes and fires event on div in page, here.

Chromium-like messaging: json request with json callback

Web page:

<html>
  <head>
    <script>
      var something = {
        send_request: function(data, callback) { // analogue of chrome.extension.sendRequest
          var request = document.createTextNode(JSON.stringify(data));

          request.addEventListener("something-response", function(event) {
            request.parentNode.removeChild(request);

            if (callback) {
              var response = JSON.parse(request.nodeValue);
              callback(response);
            }
          }, false);

          document.head.appendChild(request);

          var event = document.createEvent("HTMLEvents");
          event.initEvent("something-query", true, false);
          request.dispatchEvent(event);
        },

        callback: function(response) {
          return alert("response: " + (response ? response.toSource() : response));
        }
      }
    </script>
  </head>
  <body>
    <button onclick="return something.send_request({foo: 1}, something.callback)">send {foo: 1} with callback</button>
    <button onclick="return something.send_request({baz: 3}, something.callback)">send {baz: 3} with callback</button>
    <button onclick="return something.send_request({mozilla: 3})">send {mozilla: 3} without callback</button>
    <button onclick="return something.send_request({firefox: 4}, something.callback)">send {firefox: 4} with callback</button>
  </body>
</html>

Overlay on browser.xul in your extension:

var something = {
  listen_request: function(callback) { // analogue of chrome.extension.onRequest.addListener
    document.addEventListener("something-query", function(event) {
      var node = event.target;
      if (!node || node.nodeType != Node.TEXT_NODE)
        return;

      var doc = node.ownerDocument;
      callback(JSON.parse(node.nodeValue), doc, function(response) {
        node.nodeValue = JSON.stringify(response);

        var event = doc.createEvent("HTMLEvents");
        event.initEvent("something-response", true, false);
        return node.dispatchEvent(event);
      });
    }, false, true);
  },

  callback: function(request, sender, callback) {
    if (request.foo) {
      return setTimeout(function() {
      callback({bar: 2});
      }, 1000);
    }

    if (request.baz) {
      return setTimeout(function() {
      callback({quux: 4});
      }, 3000);
    }

    if (request.mozilla) {
      return alert("alert in chrome");
    }

    return callback(null);
  }
}

something.listen_request(something.callback);

Message Passing in Chromium

Sending structured data

The above mechanisms use element attributes and are thus only strings. You may want to transfer objects. Gecko prevents chrome to access custom object properties added by the content, because that can create security holes. A workaround is to treat the communication between webpage and chrome as a normal network protocol and use XML.

With element attributes and E4X, this is fairly easy. You do need to convert your data to/from E4X objects, though. And your chrome needs to carefully check every value passed (you need to do that either way).

var targetDoc = null;

function onLoad() {
  var iframe = document.getElementById("contentiframe");
  targetDoc = iframe.contentDocument;
  iframe.contentWindow.addEventListener("newStuff", receiveStuffFromPage, false);
}

function receiveStuffFromPage(event) {
  var uc = getEventData(event); // uc = unchecked data in form of E4X XML
  var stuff = {};
  stuff.id = sanitize.integer(uc.@id);
  stuff.name = sanitize.label(uc.@name);
}

function sendSomethingToPage (something) {
  var somethingXML = <something/>; // |something| object as E4X XML
  somethingXML.@id = something.id;
  somethingXML.@weight = something.weight;
  sendMsg("sendSomething", somethingXML);
}

/**
 * Send msgs from chrome to the page
 * @param type {String} the event type. The receiver needs to use that
 * when doing addEventListener(type, ...)
 * @param dataXML {E4X} the data or detail
 */
function sendMsg(type, dataXML) {
  var el = targetDoc.body;
  el.setAttribute("eventDataToPage", dataXML ? dataXML.toString() : "");
  var event = targetDoc.createEvent("Event")
  event.initEvent(type, true, true);
  el.dispatchEvent(event);
}

/**
 * Verifies that the event is indeed coming from our page
 * as expected, and returns the data for that event.
 * @returns {E4X} the (unchecked) detail data from the page.
 * You must check the data.
 * @see <https://developer.mozilla.org/docs/Code_snippets/
 * Interaction_between_privileged_and_non-privileged_pages#Security_notes>
 */
function getEventData(event) {
  if (event.target.ownerDocument != targetDoc)
    throw "event from unexpected source";
  return new XML(event.target.getAttribute("eventDataFromPage"));
}

Security notes

  • Never invoke the web page's JavaScript functions from your extension - doing this increases the chance of creating a security hole, where a malicious web page can trick the browser to run its code with extended privileges (just like your extension) with, for example, the ability to delete local files.
  • It is highly recommended to check the source of the event (via event.target.ownerDocument.location) and make your extension ignore any events from pages not from your server.

Resources

Mozillazine Forum Discussion

Communication between HTML and your extension

See also