Bug 1258360: Implement onMessageExternal and onConnectExternal. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 11 Feb 2017 13:28:18 -0800
changeset 491657 1db4b96f85963f144b4dd0c1985a7634c904e5a9
parent 491654 e77029e048140ddfc860b422ec34a03a8d411b16
child 547505 acd681d146643bbf4b5d247a6bcaa47641156fcb
push id47367
push usermaglione.k@gmail.com
push dateThu, 02 Mar 2017 01:16:16 +0000
reviewersmixedpuppy
bugs1258360
milestone54.0a1
Bug 1258360: Implement onMessageExternal and onConnectExternal. r?mixedpuppy MozReview-Commit-ID: 7NTrgyWpXbv
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ext-c-runtime.js
toolkit/components/extensions/schemas/runtime.json
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html
toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -324,25 +324,26 @@ class Messenger {
     return this.context.wrapPromise(promise, responseCallback);
   }
 
   sendNativeMessage(messageManager, msg, recipient, responseCallback) {
     msg = NativeApp.encodeMessage(this.context, msg);
     return this.sendMessage(messageManager, msg, recipient, responseCallback);
   }
 
-  onMessage(name) {
+  _onMessage(name, filter) {
     return new SingletonEventManager(this.context, name, fire => {
       let listener = {
         messageFilterPermissive: this.optionalFilter,
         messageFilterStrict: this.filter,
 
         filterMessage: (sender, recipient) => {
           // Ignore the message if it was sent by this Messenger.
-          return sender.contextId !== this.context.contextId;
+          return (sender.contextId !== this.context.contextId &&
+                  filter(sender, recipient));
         },
 
         receiveMessage: ({target, data: message, sender, recipient}) => {
           if (!this.context.active) {
             return;
           }
 
           let sendResponse;
@@ -372,16 +373,24 @@ class Messenger {
 
       MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
       return () => {
         MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
       };
     }).api();
   }
 
+  onMessage(name) {
+    return this._onMessage(name, sender => sender.id === this.sender.id);
+  }
+
+  onMessageExternal(name) {
+    return this._onMessage(name, sender => sender.id !== this.sender.id);
+  }
+
   _connect(messageManager, port, recipient) {
     let msg = {
       name: port.name,
       portId: port.id,
     };
 
     this._sendMessage(messageManager, "Extension:Connect", msg, recipient).catch(error => {
       if (error.result === MessageChannel.RESULT_NO_HANDLER) {
@@ -406,25 +415,26 @@ class Messenger {
   connectNative(messageManager, name, recipient) {
     let portId = getUniqueId();
 
     let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
 
     return this._connect(messageManager, port, recipient);
   }
 
-  onConnect(name) {
+  _onConnect(name, filter) {
     return new SingletonEventManager(this.context, name, fire => {
       let listener = {
         messageFilterPermissive: this.optionalFilter,
         messageFilterStrict: this.filter,
 
         filterMessage: (sender, recipient) => {
           // Ignore the port if it was created by this Messenger.
-          return sender.contextId !== this.context.contextId;
+          return (sender.contextId !== this.context.contextId &&
+                  filter(sender, recipient));
         },
 
         receiveMessage: ({target, data: message, sender}) => {
           let {name, portId} = message;
           let mm = getMessageManager(target);
           let recipient = Object.assign({}, sender);
           if (recipient.tab) {
             recipient.tabId = recipient.tab.id;
@@ -437,16 +447,24 @@ class Messenger {
       };
 
       MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
       return () => {
         MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
       };
     }).api();
   }
+
+  onConnect(name) {
+    return this._onConnect(name, sender => sender.id === this.sender.id);
+  }
+
+  onConnectExternal(name) {
+    return this._onConnect(name, sender => sender.id !== this.sender.id);
+  }
 }
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("addon");
     this.initialized = false;
   }
 
@@ -777,17 +795,17 @@ class ExtensionBaseContextChild extends 
     let {viewType, uri, contentWindow, tabId} = params;
     this.viewType = viewType;
     this.uri = uri || extension.baseURI;
 
     this.setContentWindow(contentWindow);
 
     // This is the MessageSender property passed to extension.
     // It can be augmented by the "page-open" hook.
-    let sender = {id: extension.uuid};
+    let sender = {id: extension.id};
     if (viewType == "tab") {
       sender.tabId = tabId;
       this.tabId = tabId;
     }
     if (uri) {
       sender.url = uri.spec;
     }
     this.sender = sender;
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -192,20 +192,19 @@ class BaseContext {
    * @param {object} data
    * @param {object} [options]
    * @param {object} [options.sender]
    * @param {object} [options.recipient]
    *
    * @returns {Promise}
    */
   sendMessage(target, messageName, data, options = {}) {
-    options.recipient = options.recipient || {};
+    options.recipient = Object.assign({extensionId: this.extension.id}, options.recipient);
     options.sender = options.sender || {};
 
-    options.recipient.extensionId = this.extension.id;
     options.sender.extensionId = this.extension.id;
     options.sender.contextId = this.contextId;
 
     return MessageChannel.sendMessage(target, messageName, data, options);
   }
 
   get lastError() {
     this.checkedLastError = true;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -462,17 +462,17 @@ class ContentScriptContextChild extends 
     }
     Cu.nukeSandbox(this.sandbox);
     this.sandbox = null;
   }
 }
 
 defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
   // The |sender| parameter is passed directly to the extension.
-  let sender = {id: this.extension.uuid, frameId: this.frameId, url: this.url};
+  let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
   let filter = {extensionId: this.extension.id};
   let optionalFilter = {frameId: this.frameId};
 
   return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
 });
 
 defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() {
   let localApis = {};
--- a/toolkit/components/extensions/ext-c-runtime.js
+++ b/toolkit/components/extensions/ext-c-runtime.js
@@ -4,16 +4,20 @@ function runtimeApiFactory(context) {
   let {extension} = context;
 
   return {
     runtime: {
       onConnect: context.messenger.onConnect("runtime.onConnect"),
 
       onMessage: context.messenger.onMessage("runtime.onMessage"),
 
+      onConnectExternal: context.messenger.onConnectExternal("runtime.onConnectExternal"),
+
+      onMessageExternal: context.messenger.onMessageExternal("runtime.onMessageExternal"),
+
       connect: function(extensionId, connectInfo) {
         let name = connectInfo !== null && connectInfo.name || "";
         extensionId = extensionId || extension.id;
         let recipient = {extensionId};
 
         return context.messenger.connect(context.messageManager, name, recipient);
       },
 
@@ -42,17 +46,16 @@ function runtimeApiFactory(context) {
         }
 
         if (extensionId != null && typeof extensionId != "string") {
           return Promise.reject({message: "runtime.sendMessage's extensionId argument is invalid"});
         }
         if (options != null && typeof options != "object") {
           return Promise.reject({message: "runtime.sendMessage's options argument is invalid"});
         }
-        // TODO(robwu): Validate option keys and values when we support it.
 
         extensionId = extensionId || extension.id;
         let recipient = {extensionId};
 
         return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
       },
 
       connectNative(application) {
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -530,17 +530,16 @@
         "allowedContexts": ["content", "devtools"],
         "description": "Fired when a connection is made from either an extension process or a content script.",
         "parameters": [
           {"$ref": "Port", "name": "port"}
         ]
       },
       {
         "name": "onConnectExternal",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when a connection is made from another extension.",
         "parameters": [
           {"$ref": "Port", "name": "port"}
         ]
       },
       {
         "name": "onMessage",
@@ -555,17 +554,16 @@
         "returns": {
           "type": "boolean",
           "optional": true,
           "description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns."
         }
       },
       {
         "name": "onMessageExternal",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when a message is sent from another extension/app. Cannot be used in a content script.",
         "parameters": [
           {"name": "message", "type": "any", "optional": true, "description": "The message sent by the calling script."},
           {"name": "sender", "$ref": "MessageSender" },
           {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)." }
         ],
         "returns": {
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -62,16 +62,17 @@ skip-if = os == 'android' # Android does
 [test_ext_contentscript_exporthelpers.html]
 [test_ext_contentscript_incognito.html]
 skip-if = os == 'android' # Android does not support multiple windows.
 [test_ext_contentscript_css.html]
 [test_ext_contentscript_about_blank.html]
 [test_ext_contentscript_permission.html]
 [test_ext_contentscript_teardown.html]
 [test_ext_exclude_include_globs.html]
+[test_ext_external_messaging.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
 [test_ext_geolocation.html]
 skip-if = os == 'android' # Android support Bug 1336194
 [test_ext_notifications.html]
 [test_ext_permission_xhr.html]
 [test_ext_runtime_connect.html]
 [test_ext_runtime_connect_twoway.html]
copy from toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
copy to toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html
@@ -1,93 +1,111 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <title>WebExtension test</title>
+  <title>WebExtension external messaging</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript(token) {
-  browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
-
-    if (msg == "done") {
-      browser.test.notifyPass("sendmessage_reply");
-      return;
-    }
-
-    let tabId = sender.tab.id;
-    browser.tabs.sendMessage(tabId, `${token}-tabMessage`);
+function backgroundScript(id, otherId) {
+  browser.runtime.onMessage.addListener((msg, sender) => {
+    browser.test.fail(`Got unexpected message: ${uneval(msg)} ${uneval(sender)}`);
+  });
 
-    browser.test.assertEq(msg, token, "token matches");
-    sendReply(`${token}-done`);
+  browser.runtime.onConnect.addListener(port => {
+    browser.test.fail(`Got unexpected connection: ${uneval(port.sender)}`);
   });
-}
 
-function contentScript(token) {
-  let gotTabMessage = false;
-  let badTabMessage = false;
-  browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    if (msg == `${token}-tabMessage`) {
-      gotTabMessage = true;
-    } else {
-      badTabMessage = true;
-    }
+  browser.runtime.onMessageExternal.addListener((msg, sender) => {
+    browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`);
+    browser.test.assertEq(`helo-${id}`, msg, "Got expected message");
+
+    browser.test.sendMessage("onMessage-done");
+
+    return Promise.resolve(`ehlo-${otherId}`);
   });
 
-  browser.runtime.sendMessage(token, function(resp) {
-    if (resp != `${token}-done` || !gotTabMessage || badTabMessage) {
-      return; // test failed
+  browser.runtime.onConnectExternal.addListener(port => {
+    browser.test.assertEq(otherId, port.sender.id, `${id}: Got expected external connecter ID`);
+
+    port.onMessage.addListener(msg => {
+      browser.test.assertEq(`helo-${id}`, msg, "Got expected port message");
+
+      port.postMessage(`ehlo-${otherId}`);
+
+      browser.test.sendMessage("onConnect-done");
+    });
+  });
+
+  browser.test.onMessage.addListener(msg => {
+    if (msg === "go") {
+      browser.runtime.sendMessage(otherId, `helo-${otherId}`).then(result => {
+        browser.test.assertEq(`ehlo-${id}`, result, "Got expected reply");
+        browser.test.sendMessage("sendMessage-done");
+      });
+
+      let port = browser.runtime.connect(otherId);
+      port.postMessage(`helo-${otherId}`);
+
+      port.onMessage.addListener(msg => {
+        port.disconnect();
+
+        browser.test.assertEq(msg, `ehlo-${id}`, "Got expected port reply");
+        browser.test.sendMessage("connect-done");
+      });
     }
-    browser.runtime.sendMessage("done");
   });
 }
 
-function makeExtension() {
-  let token = Math.random();
+function makeExtension(id, otherId) {
+  let args = `${JSON.stringify(id)}, ${JSON.stringify(otherId)}`;
+
   let extensionData = {
-    background: `(${backgroundScript})(${token})`,
+    background: `(${backgroundScript})(${args})`,
     manifest: {
-      "permissions": ["tabs"],
-      "content_scripts": [{
-        "matches": ["http://mochi.test/*/file_sample.html"],
-        "js": ["content_script.js"],
-        "run_at": "document_start",
-      }],
-    },
-
-    files: {
-      "content_script.js": `(${contentScript})(${token})`,
+      "applications": {"gecko": {id}},
     },
   };
-  return extensionData;
+
+  return ExtensionTestUtils.loadExtension(extensionData);
 }
 
 add_task(function* test_contentscript() {
-  let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
-  let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+  const ID1 = "foo-message@mochitest.mozilla.org";
+  const ID2 = "bar-message@mochitest.mozilla.org";
+
+  let extension1 = makeExtension(ID1, ID2);
+  let extension2 = makeExtension(ID2, ID1);
 
   yield Promise.all([extension1.startup(), extension2.startup()]);
 
-  let win = window.open("file_sample.html");
+  extension1.sendMessage("go");
+  extension2.sendMessage("go");
+
+  yield Promise.all([
+    extension1.awaitMessage("sendMessage-done"),
+    extension2.awaitMessage("sendMessage-done"),
 
-  yield Promise.all([waitForLoad(win),
-                     extension1.awaitFinish("sendmessage_reply"),
-                     extension2.awaitFinish("sendmessage_reply")]);
+    extension1.awaitMessage("onMessage-done"),
+    extension2.awaitMessage("onMessage-done"),
 
-  win.close();
+    extension1.awaitMessage("connect-done"),
+    extension2.awaitMessage("connect-done"),
+
+    extension1.awaitMessage("onConnect-done"),
+    extension2.awaitMessage("onConnect-done"),
+  ]);
 
   yield extension1.unload();
   yield extension2.unload();
-  info("extensions unloaded");
 });
 </script>
 
 </body>
 </html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
@@ -8,86 +8,174 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript(token) {
+function backgroundScript(token, id, otherId) {
+  browser.tabs.create({url: "tab.html"});
+
   browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+    browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+    if (msg === `content-${token}`) {
+      browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"),
+                              `${id}: sender url correct`);
+
+      let tabId = sender.tab.id;
+      browser.tabs.sendMessage(tabId, `${token}-contentMessage`);
+
+      sendReply(`${token}-done`);
+    } else if (msg === `tab-${token}`) {
+      browser.runtime.sendMessage(otherId, `${otherId}-tabMessage`);
+      browser.runtime.sendMessage(`${token}-tabMessage`);
 
-    if (msg == "done") {
-      browser.test.notifyPass("sendmessage_reply");
-      return;
+      sendReply(`${token}-done`);
+    } else {
+      browser.test.fail(`${id}: Unexpected runtime message received: ${msg} ${uneval(sender)}`);
     }
+  });
+
+  browser.runtime.onMessageExternal.addListener((msg, sender, sendReply) => {
+    browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`);
 
-    let tabId = sender.tab.id;
-    browser.tabs.sendMessage(tabId, `${token}-tabMessage`);
+    if (msg === `content-${id}`) {
+      browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"),
+                              `${id}: external sender url correct`);
 
-    browser.test.assertEq(msg, token, "token matches");
-    sendReply(`${token}-done`);
+      sendReply(`${otherId}-done`);
+    } else if (msg === `tab-${id}`) {
+      sendReply(`${otherId}-done`);
+    } else if (msg !== `${id}-tabMessage`) {
+      browser.test.fail(`${id}: Unexpected runtime external message received: ${msg} ${uneval(sender)}`);
+    }
   });
 }
 
-function contentScript(token) {
-  let gotTabMessage = false;
-  let badTabMessage = false;
+function contentScript(token, id, otherId) {
+  let gotContentMessage = false;
   browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    if (msg == `${token}-tabMessage`) {
-      gotTabMessage = true;
-    } else {
-      badTabMessage = true;
+    browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+    browser.test.assertEq(`${token}-contentMessage`, msg,
+                          `${id}: Correct content script message`);
+    if (msg === `${token}-contentMessage`) {
+      gotContentMessage = true;
     }
   });
 
-  browser.runtime.sendMessage(token, function(resp) {
-    if (resp != `${token}-done` || !gotTabMessage || badTabMessage) {
-      return; // test failed
-    }
-    browser.runtime.sendMessage("done");
+  Promise.all([
+    browser.runtime.sendMessage(otherId, `content-${otherId}`).then(resp => {
+      browser.test.assertEq(`${id}-done`, resp, `${id}: Correct content script external response token`);
+    }),
+
+    browser.runtime.sendMessage(`content-${token}`).then(resp => {
+      browser.test.assertEq(`${token}-done`, resp, `${id}: Correct content script response token`);
+    }),
+  ]).then(() => {
+    browser.test.assertTrue(gotContentMessage, `${id}: Got content script message`);
+
+    browser.test.sendMessage("content-script-done");
   });
 }
 
-function makeExtension() {
+function tabScript(token, id, otherId) {
+  let gotTabMessage = false;
+  browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+    browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+    if (String(msg).startsWith("content-")) {
+      return;
+    }
+
+    browser.test.assertEq(`${token}-tabMessage`, msg,
+                          `${id}: Correct tab script message`);
+    if (msg === `${token}-tabMessage`) {
+      gotTabMessage = true;
+    }
+  });
+
+  Promise.all([
+    browser.runtime.sendMessage(otherId, `tab-${otherId}`).then(resp => {
+      browser.test.assertEq(`${id}-done`, resp, `${id}: Correct tab script external response token`);
+    }),
+
+    browser.runtime.sendMessage(`tab-${token}`).then(resp => {
+      browser.test.assertEq(`${token}-done`, resp, `${id}: Correct tab script response token`);
+    }),
+  ]).then(() => {
+    browser.test.assertTrue(gotTabMessage, `${id}: Got tab script message`);
+
+    window.close();
+
+    browser.test.sendMessage("tab-script-done");
+  });
+}
+
+function makeExtension(id, otherId) {
   let token = Math.random();
+
+  let args = `${token}, ${JSON.stringify(id)}, ${JSON.stringify(otherId)}`;
+
   let extensionData = {
-    background: `(${backgroundScript})(${token})`,
+    background: `(${backgroundScript})(${args})`,
     manifest: {
+      "applications": {"gecko": {id}},
+
       "permissions": ["tabs"],
+
+
       "content_scripts": [{
         "matches": ["http://mochi.test/*/file_sample.html"],
         "js": ["content_script.js"],
         "run_at": "document_start",
       }],
     },
 
     files: {
-      "content_script.js": `(${contentScript})(${token})`,
+      "tab.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <script src="tab.js"><\/script>
+          </head>
+        </html>`,
+
+      "tab.js": `(${tabScript})(${args})`,
+
+      "content_script.js": `(${contentScript})(${args})`,
     },
   };
   return extensionData;
 }
 
 add_task(function* test_contentscript() {
-  let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
-  let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+  const ID1 = "sendmessage1@mochitest.mozilla.org";
+  const ID2 = "sendmessage2@mochitest.mozilla.org";
+
+  let extension1 = ExtensionTestUtils.loadExtension(makeExtension(ID1, ID2));
+  let extension2 = ExtensionTestUtils.loadExtension(makeExtension(ID2, ID1));
 
   yield Promise.all([extension1.startup(), extension2.startup()]);
 
   let win = window.open("file_sample.html");
 
-  yield Promise.all([waitForLoad(win),
-                     extension1.awaitFinish("sendmessage_reply"),
-                     extension2.awaitFinish("sendmessage_reply")]);
+  yield waitForLoad(win);
+
+  yield Promise.all([
+    extension1.awaitMessage("content-script-done"),
+    extension2.awaitMessage("content-script-done"),
+    extension1.awaitMessage("tab-script-done"),
+    extension2.awaitMessage("tab-script-done"),
+  ]);
 
   win.close();
 
   yield extension1.unload();
   yield extension2.unload();
-  info("extensions unloaded");
 });
 </script>
 
 </body>
 </html>