--- 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>