Bug 1146724 - Use a SendingContext for WebChannels. r=MattN/markh
authorShane Tomlinson <stomlinson@mozilla.com>
Fri, 24 Apr 2015 16:07:33 +1000
changeset 240835 780cf81ab0d3ed3c6833912471c8aeb198a6e099
parent 240834 a37cc5ecc92d3836e2b52c5e28e10200c4680b1f
child 240836 dee8993cef060b138d9c78a45df8c7c1c921aa03
child 241119 11c4c1801cbeba671a92e20022dcc2c4122ae360
push id28646
push usercbook@mozilla.com
push dateFri, 24 Apr 2015 12:24:55 +0000
treeherdermozilla-central@dee8993cef06 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, markh
bugs1146724
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1146724 - Use a SendingContext for WebChannels. r=MattN/markh
browser/base/content/content.js
services/fxaccounts/FxAccountsOAuthClient.jsm
toolkit/modules/WebChannel.jsm
toolkit/modules/tests/xpcshell/test_web_channel_broker.js
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -272,31 +272,49 @@ let AboutNetErrorListener = {
 AboutNetErrorListener.init(this);
 
 // An event listener for custom "WebChannelMessageToChrome" events on pages
 addEventListener("WebChannelMessageToChrome", function (e) {
   // if target is window then we want the document principal, otherwise fallback to target itself.
   let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal;
 
   if (e.detail) {
-    sendAsyncMessage("WebChannelMessageToChrome", e.detail, null, principal);
+    sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal);
   } else  {
     Cu.reportError("WebChannel message failed. No message detail.");
   }
 }, true, true);
 
 // Add message listener for "WebChannelMessageToContent" messages from chrome scripts
 addMessageListener("WebChannelMessageToContent", function (e) {
   if (e.data) {
-    content.dispatchEvent(new content.CustomEvent("WebChannelMessageToContent", {
-      detail: Cu.cloneInto({
-        id: e.data.id,
-        message: e.data.message,
-      }, content),
-    }));
+    // e.objects.eventTarget will be defined if sending a response to
+    // a WebChannelMessageToChrome event. An unsolicited send
+    // may not have an eventTarget defined, in this case send to the
+    // main content window.
+    let eventTarget = e.objects.eventTarget || content;
+
+    // if eventTarget is window then we want the document principal,
+    // otherwise use target itself.
+    let targetPrincipal = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget.document.nodePrincipal : eventTarget.nodePrincipal;
+
+    if (e.principal.subsumes(targetPrincipal)) {
+      // if eventTarget is a window, use it as the targetWindow, otherwise
+      // find the window that owns the eventTarget.
+      let targetWindow = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget : eventTarget.ownerDocument.defaultView;
+
+      eventTarget.dispatchEvent(new targetWindow.CustomEvent("WebChannelMessageToContent", {
+        detail: Cu.cloneInto({
+          id: e.data.id,
+          message: e.data.message,
+        }, targetWindow),
+      }));
+    } else {
+      Cu.reportError("WebChannel message failed. Principal mismatch.");
+    }
   } else {
     Cu.reportError("WebChannel message failed. No message data.");
   }
 });
 
 
 let ClickEventHandler = {
   init: function init() {
--- a/services/fxaccounts/FxAccountsOAuthClient.jsm
+++ b/services/fxaccounts/FxAccountsOAuthClient.jsm
@@ -158,24 +158,25 @@ this.FxAccountsOAuthClient.prototype = {
   _registerChannel: function() {
     /**
      * Processes messages that are called back from the FxAccountsChannel
      *
      * @param webChannelId {String}
      *        Command webChannelId
      * @param message {Object}
      *        Command message
-     * @param target {EventTarget}
-     *        Channel message event target
+     * @param sendingContext {Object}
+     *        Channel message event sendingContext
      * @private
      */
-    let listener = function (webChannelId, message, target) {
+    let listener = function (webChannelId, message, sendingContext) {
       if (message) {
         let command = message.command;
         let data = message.data;
+        let target = sendingContext && sendingContext.browser;
 
         switch (command) {
           case "oauth_complete":
             // validate the returned state and call onComplete or onError
             let result = null;
             let err = null;
 
             if (this.parameters.state !== data.state) {
--- a/toolkit/modules/WebChannel.jsm
+++ b/toolkit/modules/WebChannel.jsm
@@ -61,36 +61,40 @@ let WebChannelBroker = Object.create({
 
   /**
    * @param event {Event}
    *        Message Manager event
    * @private
    */
   _listener: function (event) {
     let data = event.data;
-    let sender = event.target;
+    let sendingContext = {
+      browser: event.target,
+      eventTarget: event.objects.eventTarget,
+      principal: event.principal,
+    };
 
     if (data && data.id) {
       if (!event.principal) {
-        this._sendErrorEventToContent(data.id, sender, "Message principal missing");
+        this._sendErrorEventToContent(data.id, sendingContext, "Message principal missing");
       } else {
         let validChannelFound = false;
         data.message = data.message || {};
 
         for (var channel of this._channelMap.keys()) {
           if (channel.id === data.id &&
             channel._originCheckCallback(event.principal)) {
             validChannelFound = true;
-            channel.deliver(data, sender);
+            channel.deliver(data, sendingContext);
           }
         }
 
         // if no valid origins send an event that there is no such valid channel
         if (!validChannelFound) {
-          this._sendErrorEventToContent(data.id, sender, "No Such Channel");
+          this._sendErrorEventToContent(data.id, sendingContext, "No Such Channel");
         }
       }
     } else {
       Cu.reportError("WebChannel channel id missing");
     }
   },
   /**
    * The global message manager operates on every <browser>
@@ -103,30 +107,34 @@ let WebChannelBroker = Object.create({
   /**
    * Object to store pairs of message origins and callback functions
    */
   _channelMap: new Map(),
   /**
    *
    * @param id {String}
    *        The WebChannel id to include in the message
-   * @param sender {EventTarget}
-   *        EventTarget with a "messageManager" that will send be used to send the message
+   * @param sendingContext {Object}
+   *        Message sending context
    * @param [errorMsg] {String}
    *        Error message
    * @private
    */
-  _sendErrorEventToContent: function (id, sender, errorMsg) {
+  _sendErrorEventToContent: function (id, sendingContext, errorMsg) {
+    let { browser: targetBrowser, eventTarget, principal: targetPrincipal } = sendingContext;
+
     errorMsg = errorMsg || "Web Channel Broker error";
 
-    if (sender.messageManager) {
-      sender.messageManager.sendAsyncMessage("WebChannelMessageToContent", {
+    if (targetBrowser && targetBrowser.messageManager) {
+      targetBrowser.messageManager.sendAsyncMessage("WebChannelMessageToContent", {
         id: id,
         error: errorMsg,
-      }, sender);
+      }, { eventTarget: eventTarget }, targetPrincipal);
+    } else {
+      Cu.reportError("Failed to send a WebChannel error. Target invalid.");
     }
     Cu.reportError(id.toString() + " error message. " + errorMsg);
   },
 });
 
 
 /**
  * Creates a new WebChannel that listens and sends messages over some channel id
@@ -206,18 +214,27 @@ this.WebChannel.prototype = {
    * Registers the channel itself with the WebChannelBroker
    *
    * @param callback {Function}
    *        Callback that will be called when there is a message
    *        @param {String} id
    *        The WebChannel id that was used for this message
    *        @param {Object} message
    *        The message itself
-   *        @param {EventTarget} sender
-   *        The source of the message
+   *        @param sendingContext {Object}
+   *        The sending context of the source of the message. Can be passed to
+   *        `send` to respond to a message.
+   *               @param sendingContext.browser {browser}
+   *                      The <browser> object that captured the
+   *                      WebChannelMessageToChrome.
+   *               @param sendingContext.eventTarget {EventTarget}
+   *                      The <EventTarget> where the message was sent.
+   *               @param sendingContext.principal {Principal}
+   *                      The <Principal> of the EventTarget where the
+   *                      message was sent.
    */
   listen: function (callback) {
     if (this._deliverCallback) {
       throw new Error("Failed to listen. Listener already attached.");
     } else if (!callback) {
       throw new Error("Failed to listen. Callback argument missing.");
     } else {
       this._deliverCallback = callback;
@@ -234,49 +251,68 @@ this.WebChannel.prototype = {
     this._deliverCallback = null;
   },
 
   /**
    * Sends messages over the WebChannel id using the "WebChannelMessageToContent" event
    *
    * @param message {Object}
    *        The message object that will be sent
-   * @param target {browser}
-   *        The <browser> object that has a "messageManager" that sends messages
-   *
+   * @param target {Object}
+   *        A <target> with the information of where to send the message.
+   *        @param target.browser {browser}
+   *               The <browser> object with a "messageManager" that will
+   *               be used to send the message.
+   *        @param target.principal {Principal}
+   *               Principal of the target. Prevents messages from
+   *               being dispatched to unexpected origins. The system principal
+   *               can be specified to send to any target.
+   *        @param [target.eventTarget] {EventTarget}
+   *               Optional eventTarget within the browser, use to send to a
+   *               specific element, e.g., an iframe.
    */
   send: function (message, target) {
-    if (message && target && target.messageManager) {
-      target.messageManager.sendAsyncMessage("WebChannelMessageToContent", {
+    let { browser, principal, eventTarget } = target;
+
+    if (message && browser && browser.messageManager && principal) {
+      browser.messageManager.sendAsyncMessage("WebChannelMessageToContent", {
         id: this.id,
         message: message
-      });
+      }, { eventTarget }, principal);
     } else if (!message) {
       Cu.reportError("Failed to send a WebChannel message. Message not set.");
     } else {
       Cu.reportError("Failed to send a WebChannel message. Target invalid.");
     }
   },
 
   /**
    * Deliver WebChannel messages to the set "_channelCallback"
    *
    * @param data {Object}
    *        Message data
-   * @param sender {browser}
-   *        Message sender
+   * @param sendingContext {Object}
+   *        Message sending context.
+   *        @param sendingContext.browser {browser}
+   *               The <browser> object that captured the
+   *               WebChannelMessageToChrome.
+   *        @param sendingContext.eventTarget {EventTarget}
+   *               The <EventTarget> where the message was sent.
+   *        @param sendingContext.principal {Principal}
+   *               The <Principal> of the EventTarget where the message was sent.
+   *
    */
-  deliver: function(data, sender) {
+  deliver: function(data, sendingContext) {
     if (this._deliverCallback) {
       try {
-        this._deliverCallback(data.id, data.message, sender);
+        this._deliverCallback(data.id, data.message, sendingContext);
       } catch (ex) {
         this.send({
           errno: ERRNO_UNKNOWN_ERROR,
           error: ex.message ? ex.message : ERROR_UNKNOWN
-        }, sender);
+        }, sendingContext);
         Cu.reportError("Failed to execute callback:" + ex);
       }
     } else {
       Cu.reportError("No callback set for this channel.");
     }
   }
 };
--- a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js
+++ b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js
@@ -72,14 +72,16 @@ add_task(function test_web_channel_broke
       data: {
         id: VALID_WEB_CHANNEL_ID,
         message: {
           command: "hello"
         }
       },
       principal: {
         origin: URL_STRING
-      }
+      },
+      objects: {
+      },
     };
 
     WebChannelBroker._listener(mockEvent);
   });
 });