зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1252215 - [webext] LegacyExtensionsUtils JSM module and LegacyExtensionContext helper. r=aswan,kmag
- this new module contains helpers to be able to receive connections originated from a webextension context from a legacy extension context (implemented by the `LegacyExtensionContext` class exported from this new jsm module) - two new test files (an xpcshell-test and a mochitest-browser) ensures that the LegacyExtensionContext can receive a Port object and exchange messages with a background page and a content script (the content script test is in a different test file because it doesn't currently work on android, because it needs the browser.tabs API and the TabManager internal helper) MozReview-Commit-ID: DS1NTXk0fB6 --HG-- extra : rebase_source : 462d6a461167e317297d204e72c2f6773bc5c770
This commit is contained in:
Родитель
1cac8d1e6d
Коммит
9226a7b2f4
|
@ -40,6 +40,7 @@ tags = webextensions
|
||||||
[browser_ext_getViews.js]
|
[browser_ext_getViews.js]
|
||||||
[browser_ext_incognito_popup.js]
|
[browser_ext_incognito_popup.js]
|
||||||
[browser_ext_lastError.js]
|
[browser_ext_lastError.js]
|
||||||
|
[browser_ext_legacy_extension_context_contentscript.js]
|
||||||
[browser_ext_optionsPage_privileges.js]
|
[browser_ext_optionsPage_privileges.js]
|
||||||
[browser_ext_pageAction_context.js]
|
[browser_ext_pageAction_context.js]
|
||||||
[browser_ext_pageAction_popup.js]
|
[browser_ext_pageAction_popup.js]
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {
|
||||||
|
LegacyExtensionContext,
|
||||||
|
} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {});
|
||||||
|
|
||||||
|
function promiseAddonStartup(extension) {
|
||||||
|
const {Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let listener = (evt, extensionInstance) => {
|
||||||
|
Management.off("startup", listener);
|
||||||
|
resolve(extensionInstance);
|
||||||
|
};
|
||||||
|
Management.on("startup", listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test case ensures that the LegacyExtensionContext can receive a connection
|
||||||
|
* from a content script and that the received port contains the expected sender
|
||||||
|
* tab info.
|
||||||
|
*/
|
||||||
|
add_task(function* test_legacy_extension_context_contentscript_connection() {
|
||||||
|
function backgroundScript() {
|
||||||
|
// Extract the assigned uuid from the background page url and send it
|
||||||
|
// in a test message.
|
||||||
|
let uuid = window.location.hostname;
|
||||||
|
|
||||||
|
browser.test.onMessage.addListener(msg => {
|
||||||
|
if (msg == "open-test-tab") {
|
||||||
|
browser.tabs.create({url: "http://example.com/"})
|
||||||
|
.then(tab => browser.test.sendMessage("get-expected-sender-info", {
|
||||||
|
uuid, tab,
|
||||||
|
}));
|
||||||
|
} else if (msg == "close-current-tab") {
|
||||||
|
browser.tabs.query({active: true})
|
||||||
|
.then(tabs => browser.tabs.remove(tabs[0].id))
|
||||||
|
.then(() => browser.test.sendMessage("current-tab-closed", true))
|
||||||
|
.catch(() => browser.test.sendMessage("current-tab-closed", false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.test.sendMessage("ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentScript() {
|
||||||
|
browser.runtime.sendMessage("webextension -> legacy_extension message", (reply) => {
|
||||||
|
browser.test.assertEq("legacy_extension -> webextension reply", reply,
|
||||||
|
"Got the expected reply from the LegacyExtensionContext");
|
||||||
|
browser.test.sendMessage("got-reply-message");
|
||||||
|
});
|
||||||
|
|
||||||
|
let port = browser.runtime.connect();
|
||||||
|
|
||||||
|
port.onMessage.addListener(msg => {
|
||||||
|
browser.test.assertEq("legacy_extension -> webextension port message", msg,
|
||||||
|
"Got the expected message from the LegacyExtensionContext");
|
||||||
|
port.postMessage("webextension -> legacy_extension port message");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let extensionData = {
|
||||||
|
background: `new ${backgroundScript}`,
|
||||||
|
manifest: {
|
||||||
|
content_scripts: [
|
||||||
|
{
|
||||||
|
matches: ["http://example.com/*"],
|
||||||
|
js: ["content-script.js"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
"content-script.js": `new ${contentScript}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let extension = ExtensionTestUtils.loadExtension(extensionData);
|
||||||
|
|
||||||
|
let waitForExtensionReady = extension.awaitMessage("ready");
|
||||||
|
|
||||||
|
let waitForExtensionInstance = promiseAddonStartup(extension);
|
||||||
|
|
||||||
|
extension.startup();
|
||||||
|
|
||||||
|
let extensionInstance = yield waitForExtensionInstance;
|
||||||
|
|
||||||
|
// Connect to the target extension.id as an external context
|
||||||
|
// using the given custom sender info.
|
||||||
|
let legacyContext = new LegacyExtensionContext(extensionInstance);
|
||||||
|
|
||||||
|
let waitConnectPort = new Promise(resolve => {
|
||||||
|
let {browser} = legacyContext.api;
|
||||||
|
browser.runtime.onConnect.addListener(port => {
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let waitMessage = new Promise(resolve => {
|
||||||
|
let {browser} = legacyContext.api;
|
||||||
|
browser.runtime.onMessage.addListener((singleMsg, msgSender, sendReply) => {
|
||||||
|
sendReply("legacy_extension -> webextension reply");
|
||||||
|
resolve({singleMsg, msgSender});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
is(legacyContext.type, "legacy_extension",
|
||||||
|
"LegacyExtensionContext instance has the expected type");
|
||||||
|
|
||||||
|
ok(legacyContext.api, "Got the API object");
|
||||||
|
|
||||||
|
yield waitForExtensionReady;
|
||||||
|
|
||||||
|
extension.sendMessage("open-test-tab");
|
||||||
|
|
||||||
|
let {uuid, tab} = yield extension.awaitMessage("get-expected-sender-info");
|
||||||
|
|
||||||
|
let {singleMsg, msgSender} = yield waitMessage;
|
||||||
|
is(singleMsg, "webextension -> legacy_extension message",
|
||||||
|
"Got the expected message");
|
||||||
|
ok(msgSender, "Got a message sender object");
|
||||||
|
|
||||||
|
is(msgSender.id, uuid, "The sender has the expected id property");
|
||||||
|
is(msgSender.url, "http://example.com/", "The sender has the expected url property");
|
||||||
|
ok(msgSender.tab, "The sender has a tab property");
|
||||||
|
is(msgSender.tab.id, tab.id, "The port sender has the expected tab.id");
|
||||||
|
|
||||||
|
// Wait confirmation that the reply has been received.
|
||||||
|
yield extension.awaitMessage("got-reply-message");
|
||||||
|
|
||||||
|
let port = yield waitConnectPort;
|
||||||
|
|
||||||
|
ok(port, "Got the Port API object");
|
||||||
|
ok(port.sender, "The port has a sender property");
|
||||||
|
|
||||||
|
is(port.sender.id, uuid, "The port sender has an id property");
|
||||||
|
is(port.sender.url, "http://example.com/", "The port sender has the expected url property");
|
||||||
|
ok(port.sender.tab, "The port sender has a tab property");
|
||||||
|
is(port.sender.tab.id, tab.id, "The port sender has the expected tab.id");
|
||||||
|
|
||||||
|
let waitPortMessage = new Promise(resolve => {
|
||||||
|
port.onMessage.addListener((msg) => {
|
||||||
|
resolve(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
port.postMessage("legacy_extension -> webextension port message");
|
||||||
|
|
||||||
|
let msg = yield waitPortMessage;
|
||||||
|
|
||||||
|
is(msg, "webextension -> legacy_extension port message",
|
||||||
|
"LegacyExtensionContext received the expected message from the webextension");
|
||||||
|
|
||||||
|
let waitForDisconnect = new Promise(resolve => {
|
||||||
|
port.onDisconnect.addListener(resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
let waitForTestDone = extension.awaitMessage("current-tab-closed");
|
||||||
|
|
||||||
|
extension.sendMessage("close-current-tab");
|
||||||
|
|
||||||
|
yield waitForDisconnect;
|
||||||
|
|
||||||
|
info("Got the disconnect event on tab closed");
|
||||||
|
|
||||||
|
let success = yield waitForTestDone;
|
||||||
|
|
||||||
|
ok(success, "Test completed successfully");
|
||||||
|
|
||||||
|
yield extension.unload();
|
||||||
|
});
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
|
this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData", "ExtensionContext"];
|
||||||
|
|
||||||
/* globals Extension ExtensionData */
|
/* globals Extension ExtensionData */
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["LegacyExtensionsUtils"];
|
||||||
|
|
||||||
|
/* exported LegacyExtensionsUtils, LegacyExtensionContext */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file exports helpers for Legacy Extensions that want to embed a webextensions
|
||||||
|
* and exchange messages with the embedded WebExtension.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
// Lazy imports.
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
|
||||||
|
"resource://gre/modules/Extension.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContext",
|
||||||
|
"resource://gre/modules/Extension.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||||
|
"resource://gre/modules/NetUtil.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||||
|
"resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instances created from this class provide to a legacy extension
|
||||||
|
* a simple API to exchange messages with a webextension.
|
||||||
|
*/
|
||||||
|
var LegacyExtensionContext = class extends ExtensionContext {
|
||||||
|
/**
|
||||||
|
* Create a new LegacyExtensionContext given a target Extension instance and an optional
|
||||||
|
* url (which can be used to recognize the messages of container context).
|
||||||
|
*
|
||||||
|
* @param {Extension} targetExtension
|
||||||
|
* The webextension instance associated with this context. This will be the
|
||||||
|
* instance of the newly created embedded webextension when this class is
|
||||||
|
* used through the EmbeddedWebExtensionsUtils.
|
||||||
|
* @param {Object} [optionalParams]
|
||||||
|
* An object with the following properties:
|
||||||
|
* @param {string} [optionalParams.url]
|
||||||
|
* An URL to mark the messages sent from this context
|
||||||
|
* (e.g. EmbeddedWebExtension sets it to the base url of the container addon).
|
||||||
|
*/
|
||||||
|
constructor(targetExtension, optionalParams = {}) {
|
||||||
|
let {url} = optionalParams;
|
||||||
|
|
||||||
|
super(targetExtension, {
|
||||||
|
contentWindow: null,
|
||||||
|
uri: NetUtil.newURI(url || "about:blank"),
|
||||||
|
type: "legacy_extension",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legacy Extensions (xul overlays, bootstrap restartless and Addon SDK)
|
||||||
|
// runs with a systemPrincipal.
|
||||||
|
let addonPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||||
|
Object.defineProperty(
|
||||||
|
this, "principal",
|
||||||
|
{value: addonPrincipal, enumerable: true, configurable: true}
|
||||||
|
);
|
||||||
|
|
||||||
|
let cloneScope = Cu.Sandbox(this.principal, {});
|
||||||
|
Cu.setSandboxMetadata(cloneScope, {addonId: targetExtension.id});
|
||||||
|
Object.defineProperty(
|
||||||
|
this, "cloneScope",
|
||||||
|
{value: cloneScope, enumerable: true, configurable: true, writable: true}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.api = {
|
||||||
|
browser: {
|
||||||
|
runtime: {
|
||||||
|
onConnect: this.messenger.onConnect("runtime.onConnect"),
|
||||||
|
onMessage: this.messenger.onMessage("runtime.onMessage"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the extension shuts down or is unloaded,
|
||||||
|
* and it nukes the cloneScope sandbox, if any.
|
||||||
|
*/
|
||||||
|
unload() {
|
||||||
|
if (this.unloaded) {
|
||||||
|
throw new Error("Error trying to unload LegacyExtensionContext twice.");
|
||||||
|
}
|
||||||
|
super.unload();
|
||||||
|
Cu.nukeSandbox(this.cloneScope);
|
||||||
|
this.cloneScope = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The LegacyExtensionContext is not a visible context.
|
||||||
|
*/
|
||||||
|
get externallyVisible() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.LegacyExtensionsUtils = {};
|
|
@ -11,6 +11,7 @@ EXTRA_JS_MODULES += [
|
||||||
'ExtensionManagement.jsm',
|
'ExtensionManagement.jsm',
|
||||||
'ExtensionStorage.jsm',
|
'ExtensionStorage.jsm',
|
||||||
'ExtensionUtils.jsm',
|
'ExtensionUtils.jsm',
|
||||||
|
'LegacyExtensionsUtils.jsm',
|
||||||
'MessageChannel.jsm',
|
'MessageChannel.jsm',
|
||||||
'NativeMessaging.jsm',
|
'NativeMessaging.jsm',
|
||||||
'Schemas.jsm',
|
'Schemas.jsm',
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/* globals browser */
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Extension.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
const {LegacyExtensionContext} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test case ensures that LegacyExtensionContext instances:
|
||||||
|
* - expose the expected API object and can join the messaging
|
||||||
|
* of a webextension given its addon id
|
||||||
|
* - the exposed API object can receive a port related to a `runtime.connect`
|
||||||
|
* Port created in the webextension's background page
|
||||||
|
* - the received Port instance can exchange messages with the background page
|
||||||
|
* - the received Port receive a disconnect event when the webextension is
|
||||||
|
* shutting down
|
||||||
|
*/
|
||||||
|
add_task(function* test_legacy_extension_context() {
|
||||||
|
function backgroundScript() {
|
||||||
|
let bgURL = window.location.href;
|
||||||
|
|
||||||
|
let extensionInfo = {
|
||||||
|
bgURL,
|
||||||
|
// Extract the assigned uuid from the background page url.
|
||||||
|
uuid: window.location.hostname,
|
||||||
|
};
|
||||||
|
|
||||||
|
browser.test.sendMessage("webextension-ready", extensionInfo);
|
||||||
|
|
||||||
|
browser.test.onMessage.addListener(msg => {
|
||||||
|
if (msg == "do-send-message") {
|
||||||
|
browser.runtime.sendMessage("webextension -> legacy_extension message").then(reply => {
|
||||||
|
browser.test.assertEq("legacy_extension -> webextension reply", reply,
|
||||||
|
"Got the expected message from the LegacyExtensionContext");
|
||||||
|
browser.test.sendMessage("got-reply-message");
|
||||||
|
});
|
||||||
|
} else if (msg == "do-connect") {
|
||||||
|
let port = browser.runtime.connect();
|
||||||
|
|
||||||
|
port.onMessage.addListener(msg => {
|
||||||
|
browser.test.assertEq("legacy_extension -> webextension port message", msg,
|
||||||
|
"Got the expected message from the LegacyExtensionContext");
|
||||||
|
port.postMessage("webextension -> legacy_extension port message");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let extensionData = {
|
||||||
|
background: "new " + backgroundScript,
|
||||||
|
};
|
||||||
|
|
||||||
|
let extension = Extension.generate(extensionData);
|
||||||
|
|
||||||
|
let waitForExtensionInfo = new Promise((resolve, reject) => {
|
||||||
|
extension.on("test-message", function testMessageListener(kind, msg, ...args) {
|
||||||
|
if (msg != "webextension-ready") {
|
||||||
|
reject(new Error(`Got an unexpected test-message: ${msg}`));
|
||||||
|
} else {
|
||||||
|
extension.off("test-message", testMessageListener);
|
||||||
|
resolve(args[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
yield extension.startup();
|
||||||
|
|
||||||
|
let extensionInfo = yield waitForExtensionInfo;
|
||||||
|
|
||||||
|
// Connect to the target extension.id as an external context
|
||||||
|
// using the given custom sender info.
|
||||||
|
let legacyContext = new LegacyExtensionContext(extension);
|
||||||
|
|
||||||
|
equal(legacyContext.type, "legacy_extension",
|
||||||
|
"LegacyExtensionContext instance has the expected type");
|
||||||
|
|
||||||
|
ok(legacyContext.api, "Got the expected API object");
|
||||||
|
ok(legacyContext.api.browser, "Got the expected browser property");
|
||||||
|
|
||||||
|
let waitMessage = new Promise(resolve => {
|
||||||
|
const {browser} = legacyContext.api;
|
||||||
|
browser.runtime.onMessage.addListener((singleMsg, msgSender) => {
|
||||||
|
resolve({singleMsg, msgSender});
|
||||||
|
|
||||||
|
// Send a reply to the sender.
|
||||||
|
return Promise.resolve("legacy_extension -> webextension reply");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
extension.testMessage("do-send-message");
|
||||||
|
|
||||||
|
let {singleMsg, msgSender} = yield waitMessage;
|
||||||
|
equal(singleMsg, "webextension -> legacy_extension message",
|
||||||
|
"Got the expected message");
|
||||||
|
ok(msgSender, "Got a message sender object");
|
||||||
|
|
||||||
|
equal(msgSender.id, extensionInfo.uuid, "The sender has the expected id property");
|
||||||
|
equal(msgSender.url, extensionInfo.bgURL, "The sender has the expected url property");
|
||||||
|
|
||||||
|
// Wait confirmation that the reply has been received.
|
||||||
|
yield new Promise((resolve, reject) => {
|
||||||
|
extension.on("test-message", function testMessageListener(kind, msg, ...args) {
|
||||||
|
if (msg != "got-reply-message") {
|
||||||
|
reject(new Error(`Got an unexpected test-message: ${msg}`));
|
||||||
|
} else {
|
||||||
|
extension.off("test-message", testMessageListener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let waitConnectPort = new Promise(resolve => {
|
||||||
|
let {browser} = legacyContext.api;
|
||||||
|
browser.runtime.onConnect.addListener(port => {
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
extension.testMessage("do-connect");
|
||||||
|
|
||||||
|
let port = yield waitConnectPort;
|
||||||
|
|
||||||
|
ok(port, "Got the Port API object");
|
||||||
|
ok(port.sender, "The port has a sender property");
|
||||||
|
equal(port.sender.id, extensionInfo.uuid,
|
||||||
|
"The port sender has the expected id property");
|
||||||
|
equal(port.sender.url, extensionInfo.bgURL,
|
||||||
|
"The port sender has the expected url property");
|
||||||
|
|
||||||
|
let waitPortMessage = new Promise(resolve => {
|
||||||
|
port.onMessage.addListener((msg) => {
|
||||||
|
resolve(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
port.postMessage("legacy_extension -> webextension port message");
|
||||||
|
|
||||||
|
let msg = yield waitPortMessage;
|
||||||
|
|
||||||
|
equal(msg, "webextension -> legacy_extension port message",
|
||||||
|
"LegacyExtensionContext received the expected message from the webextension");
|
||||||
|
|
||||||
|
let waitForDisconnect = new Promise(resolve => {
|
||||||
|
port.onDisconnect.addListener(resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
extension.shutdown();
|
||||||
|
|
||||||
|
yield waitForDisconnect;
|
||||||
|
|
||||||
|
do_print("Got the disconnect event on unload");
|
||||||
|
|
||||||
|
legacyContext.shutdown();
|
||||||
|
});
|
|
@ -54,6 +54,7 @@ skip-if = release_build
|
||||||
[test_ext_simple.js]
|
[test_ext_simple.js]
|
||||||
[test_ext_storage.js]
|
[test_ext_storage.js]
|
||||||
[test_getAPILevelForWindow.js]
|
[test_getAPILevelForWindow.js]
|
||||||
|
[test_ext_legacy_extension_context.js]
|
||||||
[test_locale_converter.js]
|
[test_locale_converter.js]
|
||||||
[test_locale_data.js]
|
[test_locale_data.js]
|
||||||
[test_native_messaging.js]
|
[test_native_messaging.js]
|
||||||
|
|
Загрузка…
Ссылка в новой задаче