Bug 1146724 - Use a SendingContext for WebChannels. r=MattN, r=markh, a=abillings
authorShane Tomlinson <stomlinson@mozilla.com>
Fri, 24 Apr 2015 16:07:33 +1000
changeset 260293 56d740d0769f
parent 260292 a0b48af4bb54
child 260294 e1fb2a5ab48d
push id741
push userryanvm@gmail.com
push date2015-04-27 20:01 +0000
treeherdermozilla-release@d10817faa571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, markh, abillings
bugs1146724
milestone38.0
Bug 1146724 - Use a SendingContext for WebChannels. r=MattN, r=markh, a=abillings
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
@@ -561,31 +561,49 @@ let AboutReaderListener = {
 AboutReaderListener.init();
 
 // 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 ContentSearchMediator = {
 
--- a/services/fxaccounts/FxAccountsOAuthClient.jsm
+++ b/services/fxaccounts/FxAccountsOAuthClient.jsm
@@ -144,24 +144,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 state parameter and call onComplete
             let result = null;
             if (this.parameters.state === data.state) {
               result = {
                 code: data.code,
--- 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);
   });
 });