mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 08:15:31 +00:00
Bug 1316396: Part 1 - Reorganize parent, child, common, and test code into more appropriate modules. r=aswan
MozReview-Commit-ID: 5WMt69GoN3K --HG-- rename : toolkit/components/extensions/ExtensionUtils.jsm => toolkit/components/extensions/ExtensionCommon.jsm rename : toolkit/components/extensions/Extension.jsm => toolkit/components/extensions/ExtensionParent.jsm rename : toolkit/components/extensions/Extension.jsm => toolkit/components/extensions/ExtensionTestCommon.jsm extra : rebase_source : 43ddde9dccf5b89777cf07bacf71816076e7b57f
This commit is contained in:
parent
5ae299f0d8
commit
5669335dd9
@ -34,8 +34,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
|
|||||||
"resource://gre/modules/ExtensionAPI.jsm");
|
"resource://gre/modules/ExtensionAPI.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
|
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
|
||||||
"resource://gre/modules/ExtensionStorage.jsm");
|
"resource://gre/modules/ExtensionStorage.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
|
||||||
"resource://gre/modules/FileUtils.jsm");
|
"resource://testing-common/ExtensionTestCommon.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
|
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
|
||||||
"resource://gre/modules/Locale.jsm");
|
"resource://gre/modules/Locale.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Log",
|
XPCOMUtils.defineLazyModuleGetter(this, "Log",
|
||||||
@ -46,8 +46,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
|
|||||||
"resource://gre/modules/MatchPattern.jsm");
|
"resource://gre/modules/MatchPattern.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||||
"resource://gre/modules/MessageChannel.jsm");
|
"resource://gre/modules/MessageChannel.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
|
|
||||||
"resource://gre/modules/NativeMessaging.jsm");
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||||
"resource://gre/modules/NetUtil.jsm");
|
"resource://gre/modules/NetUtil.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
@ -65,31 +63,22 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
|||||||
|
|
||||||
Cu.import("resource://gre/modules/ExtensionContent.jsm");
|
Cu.import("resource://gre/modules/ExtensionContent.jsm");
|
||||||
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
|
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
|
||||||
|
Cu.import("resource://gre/modules/ExtensionParent.jsm");
|
||||||
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
|
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
|
||||||
"@mozilla.org/uuid-generator;1",
|
"@mozilla.org/uuid-generator;1",
|
||||||
"nsIUUIDGenerator");
|
"nsIUUIDGenerator");
|
||||||
|
|
||||||
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
|
|
||||||
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
|
|
||||||
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
|
|
||||||
|
|
||||||
let schemaURLs = new Set();
|
|
||||||
|
|
||||||
if (!AppConstants.RELEASE_OR_BETA) {
|
|
||||||
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
||||||
var {
|
var {
|
||||||
BaseContext,
|
GlobalManager,
|
||||||
|
ParentAPIManager,
|
||||||
|
apiManager: Management,
|
||||||
|
} = ExtensionParent;
|
||||||
|
|
||||||
|
const {
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
LocaleData,
|
LocaleData,
|
||||||
SchemaAPIManager,
|
|
||||||
SpreadArgs,
|
|
||||||
defineLazyGetter,
|
|
||||||
flushJarCache,
|
|
||||||
instanceOf,
|
|
||||||
} = ExtensionUtils;
|
} = ExtensionUtils;
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
|
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
|
||||||
@ -111,475 +100,6 @@ const COMMENT_REGEXP = new RegExp(String.raw`
|
|||||||
//.*
|
//.*
|
||||||
`.replace(/\s+/g, ""), "gm");
|
`.replace(/\s+/g, ""), "gm");
|
||||||
|
|
||||||
var GlobalManager;
|
|
||||||
var ParentAPIManager;
|
|
||||||
|
|
||||||
// This object loads the ext-*.js scripts that define the extension API.
|
|
||||||
var Management = new class extends SchemaAPIManager {
|
|
||||||
constructor() {
|
|
||||||
super("main");
|
|
||||||
this.initialized = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads all the ext-*.js scripts currently registered.
|
|
||||||
lazyInit() {
|
|
||||||
if (this.initialized) {
|
|
||||||
return this.initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load order matters here. The base manifest defines types which are
|
|
||||||
// extended by other schemas, so needs to be loaded first.
|
|
||||||
let promise = Schemas.load(BASE_SCHEMA).then(() => {
|
|
||||||
let promises = [];
|
|
||||||
for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
|
|
||||||
promises.push(Schemas.load(url));
|
|
||||||
}
|
|
||||||
for (let url of schemaURLs) {
|
|
||||||
promises.push(Schemas.load(url));
|
|
||||||
}
|
|
||||||
return Promise.all(promises);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
|
|
||||||
this.loadScript(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = promise;
|
|
||||||
return this.initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerSchemaAPI(namespace, envType, getAPI) {
|
|
||||||
if (envType == "addon_parent" || envType == "content_parent") {
|
|
||||||
super.registerSchemaAPI(namespace, envType, getAPI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
// Subscribes to messages related to the extension messaging API and forwards it
|
|
||||||
// to the relevant message manager. The "sender" field for the `onMessage` and
|
|
||||||
// `onConnect` events are updated if needed.
|
|
||||||
let ProxyMessenger = {
|
|
||||||
_initialized: false,
|
|
||||||
init() {
|
|
||||||
if (this._initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._initialized = true;
|
|
||||||
|
|
||||||
// TODO(robwu): When addons move to a separate process, we should use the
|
|
||||||
// parent process manager(s) of the addon process(es) instead of the
|
|
||||||
// in-process one.
|
|
||||||
let pipmm = Services.ppmm.getChildAt(0);
|
|
||||||
// Listen on the global frame message manager because content scripts send
|
|
||||||
// and receive extension messages via their frame.
|
|
||||||
// Listen on the parent process message manager because `runtime.connect`
|
|
||||||
// and `runtime.sendMessage` requests must be delivered to all frames in an
|
|
||||||
// addon process (by the API contract).
|
|
||||||
// And legacy addons are not associated with a frame, so that is another
|
|
||||||
// reason for having a parent process manager here.
|
|
||||||
let messageManagers = [Services.mm, pipmm];
|
|
||||||
|
|
||||||
MessageChannel.addListener(messageManagers, "Extension:Connect", this);
|
|
||||||
MessageChannel.addListener(messageManagers, "Extension:Message", this);
|
|
||||||
MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
|
|
||||||
MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
|
|
||||||
},
|
|
||||||
|
|
||||||
receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
|
|
||||||
if (recipient.toNativeApp) {
|
|
||||||
let {childId, toNativeApp} = recipient;
|
|
||||||
if (messageName == "Extension:Message") {
|
|
||||||
let context = ParentAPIManager.getContextById(childId);
|
|
||||||
return new NativeApp(context, toNativeApp).sendMessage(data);
|
|
||||||
}
|
|
||||||
if (messageName == "Extension:Connect") {
|
|
||||||
let context = ParentAPIManager.getContextById(childId);
|
|
||||||
NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
|
|
||||||
// native messages are handled by NativeApp.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let extension = GlobalManager.extensionMap.get(sender.extensionId);
|
|
||||||
let receiverMM = this._getMessageManagerForRecipient(recipient);
|
|
||||||
if (!extension || !receiverMM) {
|
|
||||||
return Promise.reject({
|
|
||||||
result: MessageChannel.RESULT_NO_HANDLER,
|
|
||||||
message: "No matching message handler for the given recipient.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((messageName == "Extension:Message" ||
|
|
||||||
messageName == "Extension:Connect") &&
|
|
||||||
Management.global.tabGetSender) {
|
|
||||||
// From ext-tabs.js, undefined on Android.
|
|
||||||
Management.global.tabGetSender(extension, target, sender);
|
|
||||||
}
|
|
||||||
return MessageChannel.sendMessage(receiverMM, messageName, data, {
|
|
||||||
sender,
|
|
||||||
recipient,
|
|
||||||
responseType,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {object} recipient An object that was passed to
|
|
||||||
* `MessageChannel.sendMessage`.
|
|
||||||
* @returns {object|null} The message manager matching the recipient if found.
|
|
||||||
*/
|
|
||||||
_getMessageManagerForRecipient(recipient) {
|
|
||||||
let {extensionId, tabId} = recipient;
|
|
||||||
// tabs.sendMessage / tabs.connect
|
|
||||||
if (tabId) {
|
|
||||||
// `tabId` being set implies that the tabs API is supported, so we don't
|
|
||||||
// need to check whether `TabManager` exists.
|
|
||||||
let tab = Management.global.TabManager.getTab(tabId, null, null);
|
|
||||||
return tab && tab.linkedBrowser.messageManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// runtime.sendMessage / runtime.connect
|
|
||||||
if (extensionId) {
|
|
||||||
// TODO(robwu): map the extensionId to the addon parent process's message
|
|
||||||
// manager when they run in a separate process.
|
|
||||||
return Services.ppmm.getChildAt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
class BrowserDocshellFollower {
|
|
||||||
/**
|
|
||||||
* Follows the <browser> belonging to the `xulBrowser`'s current docshell.
|
|
||||||
*
|
|
||||||
* @param {XULElement} xulBrowser A <browser> tag.
|
|
||||||
* @param {function} onBrowserChange Called when the <browser> changes.
|
|
||||||
*/
|
|
||||||
constructor(xulBrowser, onBrowserChange) {
|
|
||||||
this.xulBrowser = xulBrowser;
|
|
||||||
this.onBrowserChange = onBrowserChange;
|
|
||||||
|
|
||||||
xulBrowser.addEventListener("SwapDocShells", this);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.xulBrowser.removeEventListener("SwapDocShells", this);
|
|
||||||
this.xulBrowser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvent({detail: otherBrowser}) {
|
|
||||||
this.xulBrowser.removeEventListener("SwapDocShells", this);
|
|
||||||
this.xulBrowser = otherBrowser;
|
|
||||||
this.xulBrowser.addEventListener("SwapDocShells", this);
|
|
||||||
this.onBrowserChange(otherBrowser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProxyContext extends BaseContext {
|
|
||||||
constructor(envType, extension, params, xulBrowser, principal) {
|
|
||||||
super(envType, extension);
|
|
||||||
|
|
||||||
this.uri = NetUtil.newURI(params.url);
|
|
||||||
|
|
||||||
this.incognito = params.incognito;
|
|
||||||
|
|
||||||
// This message manager is used by ParentAPIManager to send messages and to
|
|
||||||
// close the ProxyContext if the underlying message manager closes. This
|
|
||||||
// message manager object may change when `xulBrowser` swaps docshells, e.g.
|
|
||||||
// when a tab is moved to a different window.
|
|
||||||
this.currentMessageManager = xulBrowser.messageManager;
|
|
||||||
this._docShellTracker = new BrowserDocshellFollower(
|
|
||||||
xulBrowser, this.onBrowserChange.bind(this));
|
|
||||||
|
|
||||||
Object.defineProperty(this, "principal", {
|
|
||||||
value: principal, enumerable: true, configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.listenerProxies = new Map();
|
|
||||||
|
|
||||||
Management.emit("proxy-context-load", this);
|
|
||||||
}
|
|
||||||
|
|
||||||
get cloneScope() {
|
|
||||||
return this.sandbox;
|
|
||||||
}
|
|
||||||
|
|
||||||
onBrowserChange(browser) {
|
|
||||||
// Make sure that the message manager is set. Otherwise the ProxyContext may
|
|
||||||
// never be destroyed because the ParentAPIManager would fail to detect that
|
|
||||||
// the message manager is closed.
|
|
||||||
if (!browser.messageManager) {
|
|
||||||
throw new Error("BrowserDocshellFollower: The new browser has no message manager");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentMessageManager = browser.messageManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
this.unload();
|
|
||||||
}
|
|
||||||
|
|
||||||
unload() {
|
|
||||||
if (this.unloaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._docShellTracker.destroy();
|
|
||||||
super.unload();
|
|
||||||
Management.emit("proxy-context-unload", this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineLazyGetter(ProxyContext.prototype, "apiObj", function() {
|
|
||||||
let obj = {};
|
|
||||||
GlobalManager.injectInObject(this, false, obj);
|
|
||||||
return obj;
|
|
||||||
});
|
|
||||||
|
|
||||||
defineLazyGetter(ProxyContext.prototype, "sandbox", function() {
|
|
||||||
return Cu.Sandbox(this.principal);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The parent ProxyContext of an ExtensionContext in ExtensionChild.jsm.
|
|
||||||
class ExtensionChildProxyContext extends ProxyContext {
|
|
||||||
constructor(envType, extension, params, xulBrowser) {
|
|
||||||
super(envType, extension, params, xulBrowser, extension.principal);
|
|
||||||
|
|
||||||
this.viewType = params.viewType;
|
|
||||||
// WARNING: The xulBrowser may change when docShells are swapped, e.g. when
|
|
||||||
// the tab moves to a different window.
|
|
||||||
this.xulBrowser = xulBrowser;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The window that contains this context. This may change due to moving tabs.
|
|
||||||
get xulWindow() {
|
|
||||||
return this.xulBrowser.ownerGlobal;
|
|
||||||
}
|
|
||||||
|
|
||||||
get windowId() {
|
|
||||||
if (!Management.global.WindowManager || this.viewType == "background") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// viewType popup or tab:
|
|
||||||
return Management.global.WindowManager.getId(this.xulWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId() {
|
|
||||||
if (!Management.global.TabManager) {
|
|
||||||
return; // Not yet supported on Android.
|
|
||||||
}
|
|
||||||
let {gBrowser} = this.xulBrowser.ownerGlobal;
|
|
||||||
let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
|
|
||||||
return tab && Management.global.TabManager.getId(tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
onBrowserChange(browser) {
|
|
||||||
super.onBrowserChange(browser);
|
|
||||||
this.xulBrowser = browser;
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
Management.emit("page-shutdown", this);
|
|
||||||
super.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPathInObject(obj, path, printErrors = true) {
|
|
||||||
let parent;
|
|
||||||
for (let elt of path.split(".")) {
|
|
||||||
if (!obj || !(elt in obj)) {
|
|
||||||
if (printErrors) {
|
|
||||||
Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
parent = obj;
|
|
||||||
obj = obj[elt];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof obj === "function") {
|
|
||||||
return obj.bind(parent);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
ParentAPIManager = {
|
|
||||||
proxyContexts: new Map(),
|
|
||||||
|
|
||||||
init() {
|
|
||||||
Services.obs.addObserver(this, "message-manager-close", false);
|
|
||||||
|
|
||||||
Services.mm.addMessageListener("API:CreateProxyContext", this);
|
|
||||||
Services.mm.addMessageListener("API:CloseProxyContext", this, true);
|
|
||||||
Services.mm.addMessageListener("API:Call", this);
|
|
||||||
Services.mm.addMessageListener("API:AddListener", this);
|
|
||||||
Services.mm.addMessageListener("API:RemoveListener", this);
|
|
||||||
},
|
|
||||||
|
|
||||||
// "message-manager-close" observer.
|
|
||||||
observe(subject, topic, data) {
|
|
||||||
let mm = subject;
|
|
||||||
for (let [childId, context] of this.proxyContexts) {
|
|
||||||
if (context.currentMessageManager == mm) {
|
|
||||||
this.closeProxyContext(childId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
shutdownExtension(extensionId) {
|
|
||||||
for (let [childId, context] of this.proxyContexts) {
|
|
||||||
if (context.extension.id == extensionId) {
|
|
||||||
context.shutdown();
|
|
||||||
this.proxyContexts.delete(childId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
receiveMessage({name, data, target}) {
|
|
||||||
switch (name) {
|
|
||||||
case "API:CreateProxyContext":
|
|
||||||
this.createProxyContext(data, target);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "API:CloseProxyContext":
|
|
||||||
this.closeProxyContext(data.childId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "API:Call":
|
|
||||||
this.call(data, target);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "API:AddListener":
|
|
||||||
this.addListener(data, target);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "API:RemoveListener":
|
|
||||||
this.removeListener(data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
createProxyContext(data, target) {
|
|
||||||
let {envType, extensionId, childId, principal} = data;
|
|
||||||
if (this.proxyContexts.has(childId)) {
|
|
||||||
throw new Error("A WebExtension context with the given ID already exists!");
|
|
||||||
}
|
|
||||||
|
|
||||||
let extension = GlobalManager.getExtension(extensionId);
|
|
||||||
if (!extension) {
|
|
||||||
throw new Error(`No WebExtension found with ID ${extensionId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let context;
|
|
||||||
if (envType == "addon_parent") {
|
|
||||||
// Privileged addon contexts can only be loaded in documents whose main
|
|
||||||
// frame is also the same addon.
|
|
||||||
if (principal.URI.prePath != extension.baseURI.prePath ||
|
|
||||||
!target.contentPrincipal.subsumes(principal)) {
|
|
||||||
throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
|
|
||||||
}
|
|
||||||
context = new ExtensionChildProxyContext(envType, extension, data, target);
|
|
||||||
} else if (envType == "content_parent") {
|
|
||||||
context = new ProxyContext(envType, extension, data, target, principal);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Invalid WebExtension context envType: ${envType}`);
|
|
||||||
}
|
|
||||||
this.proxyContexts.set(childId, context);
|
|
||||||
},
|
|
||||||
|
|
||||||
closeProxyContext(childId) {
|
|
||||||
let context = this.proxyContexts.get(childId);
|
|
||||||
if (!context) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.unload();
|
|
||||||
this.proxyContexts.delete(childId);
|
|
||||||
},
|
|
||||||
|
|
||||||
call(data, target) {
|
|
||||||
let context = this.getContextById(data.childId);
|
|
||||||
if (context.currentMessageManager !== target.messageManager) {
|
|
||||||
Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let args = Cu.cloneInto(data.args, context.sandbox);
|
|
||||||
let result = findPathInObject(context.apiObj, data.path)(...args);
|
|
||||||
|
|
||||||
if (data.callId) {
|
|
||||||
result = result || Promise.resolve();
|
|
||||||
|
|
||||||
result.then(result => {
|
|
||||||
result = result instanceof SpreadArgs ? [...result] : [result];
|
|
||||||
|
|
||||||
context.currentMessageManager.sendAsyncMessage("API:CallResult", {
|
|
||||||
childId: data.childId,
|
|
||||||
callId: data.callId,
|
|
||||||
result,
|
|
||||||
});
|
|
||||||
}, error => {
|
|
||||||
error = context.normalizeError(error);
|
|
||||||
context.currentMessageManager.sendAsyncMessage("API:CallResult", {
|
|
||||||
childId: data.childId,
|
|
||||||
callId: data.callId,
|
|
||||||
error: {message: error.message},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (data.callId) {
|
|
||||||
let error = context.normalizeError(e);
|
|
||||||
context.currentMessageManager.sendAsyncMessage("API:CallResult", {
|
|
||||||
childId: data.childId,
|
|
||||||
callId: data.callId,
|
|
||||||
error: {message: error.message},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Cu.reportError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addListener(data, target) {
|
|
||||||
let context = this.getContextById(data.childId);
|
|
||||||
if (context.currentMessageManager !== target.messageManager) {
|
|
||||||
Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
|
|
||||||
}
|
|
||||||
|
|
||||||
function listener(...listenerArgs) {
|
|
||||||
context.currentMessageManager.sendAsyncMessage("API:RunListener", {
|
|
||||||
childId: data.childId,
|
|
||||||
path: data.path,
|
|
||||||
args: listenerArgs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
context.listenerProxies.set(data.path, listener);
|
|
||||||
|
|
||||||
let args = Cu.cloneInto(data.args, context.sandbox);
|
|
||||||
findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeListener(data) {
|
|
||||||
let context = this.getContextById(data.childId);
|
|
||||||
let listener = context.listenerProxies.get(data.path);
|
|
||||||
findPathInObject(context.apiObj, data.path).removeListener(listener);
|
|
||||||
},
|
|
||||||
|
|
||||||
getContextById(childId) {
|
|
||||||
let context = this.proxyContexts.get(childId);
|
|
||||||
if (!context) {
|
|
||||||
let error = new Error("WebExtension context not found!");
|
|
||||||
Cu.reportError(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
ParentAPIManager.init();
|
|
||||||
|
|
||||||
// All moz-extension URIs use a machine-specific UUID rather than the
|
// All moz-extension URIs use a machine-specific UUID rather than the
|
||||||
// extension's own ID in the host component. This makes it more
|
// extension's own ID in the host component. This makes it more
|
||||||
// difficult for web pages to detect whether a user has a given add-on
|
// difficult for web pages to detect whether a user has a given add-on
|
||||||
@ -685,52 +205,7 @@ var UninstallObserver = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Responsible for loading extension APIs into the right globals.
|
UninstallObserver.init();
|
||||||
GlobalManager = {
|
|
||||||
// Map[extension ID -> Extension]. Determines which extension is
|
|
||||||
// responsible for content under a particular extension ID.
|
|
||||||
extensionMap: new Map(),
|
|
||||||
initialized: false,
|
|
||||||
|
|
||||||
init(extension) {
|
|
||||||
if (this.extensionMap.size == 0) {
|
|
||||||
UninstallObserver.init();
|
|
||||||
ProxyMessenger.init();
|
|
||||||
Management.on("extension-browser-inserted", this._onExtensionBrowser);
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.extensionMap.set(extension.id, extension);
|
|
||||||
},
|
|
||||||
|
|
||||||
uninit(extension) {
|
|
||||||
this.extensionMap.delete(extension.id);
|
|
||||||
|
|
||||||
if (this.extensionMap.size == 0 && this.initialized) {
|
|
||||||
Management.off("extension-browser-inserted", this._onExtensionBrowser);
|
|
||||||
this.initialized = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_onExtensionBrowser(type, browser) {
|
|
||||||
browser.messageManager.loadFrameScript(`data:,
|
|
||||||
Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
|
|
||||||
ExtensionContent.init(this);
|
|
||||||
addEventListener("unload", function() {
|
|
||||||
ExtensionContent.uninit(this);
|
|
||||||
});
|
|
||||||
`, false);
|
|
||||||
},
|
|
||||||
|
|
||||||
getExtension(extensionId) {
|
|
||||||
return this.extensionMap.get(extensionId);
|
|
||||||
},
|
|
||||||
|
|
||||||
injectInObject(context, isChromeCompat, dest) {
|
|
||||||
Management.generateAPIs(context, dest);
|
|
||||||
SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Represents the data contained in an extension, contained either
|
// Represents the data contained in an extension, contained either
|
||||||
// in a directory or a zip file, which may or may not be installed.
|
// in a directory or a zip file, which may or may not be installed.
|
||||||
@ -1087,95 +562,6 @@ this.ExtensionData = class {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A skeleton Extension-like object, used for testing, which installs an
|
|
||||||
* add-on via the add-on manager when startup() is called, and
|
|
||||||
* uninstalles it on shutdown().
|
|
||||||
*
|
|
||||||
* @param {string} id
|
|
||||||
* @param {nsIFile} file
|
|
||||||
* @param {nsIURI} rootURI
|
|
||||||
* @param {string} installType
|
|
||||||
*/
|
|
||||||
class MockExtension {
|
|
||||||
constructor(file, rootURI, installType) {
|
|
||||||
this.id = null;
|
|
||||||
this.file = file;
|
|
||||||
this.rootURI = rootURI;
|
|
||||||
this.installType = installType;
|
|
||||||
this.addon = null;
|
|
||||||
|
|
||||||
let promiseEvent = eventName => new Promise(resolve => {
|
|
||||||
let onstartup = (msg, extension) => {
|
|
||||||
if (this.addon && extension.id == this.addon.id) {
|
|
||||||
Management.off(eventName, onstartup);
|
|
||||||
|
|
||||||
this.id = extension.id;
|
|
||||||
this._extension = extension;
|
|
||||||
resolve(extension);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Management.on(eventName, onstartup);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._extension = null;
|
|
||||||
this._extensionPromise = promiseEvent("startup");
|
|
||||||
this._readyPromise = promiseEvent("ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
testMessage(...args) {
|
|
||||||
return this._extension.testMessage(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
on(...args) {
|
|
||||||
this._extensionPromise.then(extension => {
|
|
||||||
extension.on(...args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
off(...args) {
|
|
||||||
this._extensionPromise.then(extension => {
|
|
||||||
extension.off(...args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startup() {
|
|
||||||
if (this.installType == "temporary") {
|
|
||||||
return AddonManager.installTemporaryAddon(this.file).then(addon => {
|
|
||||||
this.addon = addon;
|
|
||||||
return this._readyPromise;
|
|
||||||
});
|
|
||||||
} else if (this.installType == "permanent") {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
AddonManager.getInstallForFile(this.file, install => {
|
|
||||||
let listener = {
|
|
||||||
onInstallFailed: reject,
|
|
||||||
onInstallEnded: (install, newAddon) => {
|
|
||||||
this.addon = newAddon;
|
|
||||||
resolve(this._readyPromise);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
install.addListener(listener);
|
|
||||||
install.install();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw new Error("installType must be one of: temporary, permanent");
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown() {
|
|
||||||
this.addon.uninstall();
|
|
||||||
return this.cleanupGeneratedFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupGeneratedFile() {
|
|
||||||
flushJarCache(this.file);
|
|
||||||
return OS.File.remove(this.file.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _browserUpdated = false;
|
let _browserUpdated = false;
|
||||||
|
|
||||||
// We create one instance of this class per extension. |addonData|
|
// We create one instance of this class per extension. |addonData|
|
||||||
@ -1221,207 +607,16 @@ this.Extension = class extends ExtensionData {
|
|||||||
return _browserUpdated;
|
return _browserUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This code is designed to make it easy to test a WebExtension
|
|
||||||
* without creating a bunch of files. Everything is contained in a
|
|
||||||
* single JSON blob.
|
|
||||||
*
|
|
||||||
* Properties:
|
|
||||||
* "background": "<JS code>"
|
|
||||||
* A script to be loaded as the background script.
|
|
||||||
* The "background" section of the "manifest" property is overwritten
|
|
||||||
* if this is provided.
|
|
||||||
* "manifest": {...}
|
|
||||||
* Contents of manifest.json
|
|
||||||
* "files": {"filename1": "contents1", ...}
|
|
||||||
* Data to be included as files. Can be referenced from the manifest.
|
|
||||||
* If a manifest file is provided here, it takes precedence over
|
|
||||||
* a generated one. Always use "/" as a directory separator.
|
|
||||||
* Directories should appear here only implicitly (as a prefix
|
|
||||||
* to file names)
|
|
||||||
*
|
|
||||||
* To make things easier, the value of "background" and "files"[] can
|
|
||||||
* be a function, which is converted to source that is run.
|
|
||||||
*
|
|
||||||
* The generated extension is stored in the system temporary directory,
|
|
||||||
* and an nsIFile object pointing to it is returned.
|
|
||||||
*
|
|
||||||
* @param {object} data
|
|
||||||
* @returns {nsIFile}
|
|
||||||
*/
|
|
||||||
static generateXPI(data) {
|
static generateXPI(data) {
|
||||||
let manifest = data.manifest;
|
return ExtensionTestCommon.generateXPI(data);
|
||||||
if (!manifest) {
|
|
||||||
manifest = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let files = data.files;
|
|
||||||
if (!files) {
|
|
||||||
files = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function provide(obj, keys, value, override = false) {
|
|
||||||
if (keys.length == 1) {
|
|
||||||
if (!(keys[0] in obj) || override) {
|
|
||||||
obj[keys[0]] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!(keys[0] in obj)) {
|
|
||||||
obj[keys[0]] = {};
|
|
||||||
}
|
|
||||||
provide(obj[keys[0]], keys.slice(1), value, override);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
provide(manifest, ["name"], "Generated extension");
|
|
||||||
provide(manifest, ["manifest_version"], 2);
|
|
||||||
provide(manifest, ["version"], "1.0");
|
|
||||||
|
|
||||||
if (data.background) {
|
|
||||||
let bgScript = uuidGen.generateUUID().number + ".js";
|
|
||||||
|
|
||||||
provide(manifest, ["background", "scripts"], [bgScript], true);
|
|
||||||
files[bgScript] = data.background;
|
|
||||||
}
|
|
||||||
|
|
||||||
provide(files, ["manifest.json"], manifest);
|
|
||||||
|
|
||||||
if (data.embedded) {
|
|
||||||
// Package this as a webextension embedded inside a legacy
|
|
||||||
// extension.
|
|
||||||
|
|
||||||
let xpiFiles = {
|
|
||||||
"install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
|
||||||
<Description about="urn:mozilla:install-manifest"
|
|
||||||
em:id="${manifest.applications.gecko.id}"
|
|
||||||
em:name="${manifest.name}"
|
|
||||||
em:type="2"
|
|
||||||
em:version="${manifest.version}"
|
|
||||||
em:description=""
|
|
||||||
em:hasEmbeddedWebExtension="true"
|
|
||||||
em:bootstrap="true">
|
|
||||||
|
|
||||||
<!-- Firefox -->
|
|
||||||
<em:targetApplication>
|
|
||||||
<Description
|
|
||||||
em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
|
|
||||||
em:minVersion="51.0a1"
|
|
||||||
em:maxVersion="*"/>
|
|
||||||
</em:targetApplication>
|
|
||||||
</Description>
|
|
||||||
</RDF>
|
|
||||||
`,
|
|
||||||
|
|
||||||
"bootstrap.js": `
|
|
||||||
function install() {}
|
|
||||||
function uninstall() {}
|
|
||||||
function shutdown() {}
|
|
||||||
|
|
||||||
function startup(data) {
|
|
||||||
data.webExtension.startup();
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let [path, data] of Object.entries(files)) {
|
|
||||||
xpiFiles[`webextension/${path}`] = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
files = xpiFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.generateZipFile(files);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static generateZipFile(files, baseName = "generated-extension.xpi") {
|
static generateZipFile(files, baseName = "generated-extension.xpi") {
|
||||||
let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
|
return ExtensionTestCommon.generateZipFile(files, baseName);
|
||||||
let zipW = new ZipWriter();
|
|
||||||
|
|
||||||
let file = FileUtils.getFile("TmpD", [baseName]);
|
|
||||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
|
||||||
|
|
||||||
const MODE_WRONLY = 0x02;
|
|
||||||
const MODE_TRUNCATE = 0x20;
|
|
||||||
zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
|
|
||||||
|
|
||||||
// Needs to be in microseconds for some reason.
|
|
||||||
let time = Date.now() * 1000;
|
|
||||||
|
|
||||||
function generateFile(filename) {
|
|
||||||
let components = filename.split("/");
|
|
||||||
let path = "";
|
|
||||||
for (let component of components.slice(0, -1)) {
|
|
||||||
path += component + "/";
|
|
||||||
if (!zipW.hasEntry(path)) {
|
|
||||||
zipW.addEntryDirectory(path, time, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let filename in files) {
|
|
||||||
let script = files[filename];
|
|
||||||
if (typeof(script) == "function") {
|
|
||||||
script = "(" + script.toString() + ")()";
|
|
||||||
} else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
|
|
||||||
script = JSON.stringify(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!instanceOf(script, "ArrayBuffer")) {
|
|
||||||
script = new TextEncoder("utf-8").encode(script).buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
|
|
||||||
stream.setData(script, 0, script.byteLength);
|
|
||||||
|
|
||||||
generateFile(filename);
|
|
||||||
zipW.addEntryStream(filename, time, 0, stream, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
zipW.close();
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a new extension using |Extension.generateXPI|, and initializes a
|
|
||||||
* new |Extension| instance which will execute it.
|
|
||||||
*
|
|
||||||
* @param {object} data
|
|
||||||
* @returns {Extension}
|
|
||||||
*/
|
|
||||||
static generate(data) {
|
static generate(data) {
|
||||||
let file = this.generateXPI(data);
|
return ExtensionTestCommon.generate(data);
|
||||||
|
|
||||||
flushJarCache(file);
|
|
||||||
Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
|
|
||||||
|
|
||||||
let fileURI = Services.io.newFileURI(file);
|
|
||||||
let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
|
|
||||||
|
|
||||||
// This may be "temporary" or "permanent".
|
|
||||||
if (data.useAddonManager) {
|
|
||||||
return new MockExtension(file, jarURI, data.useAddonManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
let id;
|
|
||||||
if (data.manifest) {
|
|
||||||
if (data.manifest.applications && data.manifest.applications.gecko) {
|
|
||||||
id = data.manifest.applications.gecko.id;
|
|
||||||
} else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
|
|
||||||
id = data.manifest.browser_specific_settings.gecko.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!id) {
|
|
||||||
id = uuidGen.generateUUID().number;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Extension({
|
|
||||||
id,
|
|
||||||
resourceURI: jarURI,
|
|
||||||
cleanupFile: file,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on(hook, f) {
|
on(hook, f) {
|
||||||
|
@ -19,34 +19,404 @@ const Cc = Components.classes;
|
|||||||
const Cu = Components.utils;
|
const Cu = Components.utils;
|
||||||
const Cr = Components.results;
|
const Cr = Components.results;
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
|
||||||
|
"resource://gre/modules/ExtensionParent.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||||
"resource://gre/modules/MessageChannel.jsm");
|
"resource://gre/modules/MessageChannel.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
||||||
|
"resource://gre/modules/PromiseUtils.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
||||||
"resource://gre/modules/Schemas.jsm");
|
"resource://gre/modules/Schemas.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "ParentAPIManager",
|
||||||
|
() => ExtensionParent.ParentAPIManager);
|
||||||
|
|
||||||
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
|
const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||||
var {
|
|
||||||
getInnerWindowID,
|
const {
|
||||||
BaseContext,
|
EventManager,
|
||||||
ChildAPIManager,
|
SingletonEventManager,
|
||||||
|
SpreadArgs,
|
||||||
defineLazyGetter,
|
defineLazyGetter,
|
||||||
LocalAPIImplementation,
|
findPathInObject,
|
||||||
Messenger,
|
getInnerWindowID,
|
||||||
SchemaAPIManager,
|
getMessageManager,
|
||||||
|
injectAPI,
|
||||||
} = ExtensionUtils;
|
} = ExtensionUtils;
|
||||||
|
|
||||||
// There is a circular dependency between Extension.jsm and us.
|
const {
|
||||||
// Long-term this file should not reference Extension.jsm (because they would
|
BaseContext,
|
||||||
// live in different processes), but for now use lazy getters.
|
LocalAPIImplementation,
|
||||||
XPCOMUtils.defineLazyGetter(this, "findPathInObject",
|
SchemaAPIInterface,
|
||||||
() => Cu.import("resource://gre/modules/Extension.jsm", {}).findPathInObject);
|
SchemaAPIManager,
|
||||||
XPCOMUtils.defineLazyGetter(this, "ParentAPIManager",
|
} = ExtensionCommon;
|
||||||
() => Cu.import("resource://gre/modules/Extension.jsm", {}).ParentAPIManager);
|
|
||||||
|
var ExtensionChild;
|
||||||
|
|
||||||
|
let gNextPortId = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction for a Port object in the extension API.
|
||||||
|
*
|
||||||
|
* @param {BaseContext} context The context that owns this port.
|
||||||
|
* @param {nsIMessageSender} senderMM The message manager to send messages to.
|
||||||
|
* @param {Array<nsIMessageListenerManager>} receiverMMs Message managers to
|
||||||
|
* listen on.
|
||||||
|
* @param {string} name Arbitrary port name as defined by the addon.
|
||||||
|
* @param {string} id An ID that uniquely identifies this port's channel.
|
||||||
|
* @param {object} sender The `port.sender` property.
|
||||||
|
* @param {object} recipient The recipient of messages sent from this port.
|
||||||
|
*/
|
||||||
|
class Port {
|
||||||
|
constructor(context, senderMM, receiverMMs, name, id, sender, recipient) {
|
||||||
|
this.context = context;
|
||||||
|
this.senderMM = senderMM;
|
||||||
|
this.receiverMMs = receiverMMs;
|
||||||
|
this.name = name;
|
||||||
|
this.id = id;
|
||||||
|
this.sender = sender;
|
||||||
|
this.recipient = recipient;
|
||||||
|
this.disconnected = false;
|
||||||
|
this.disconnectListeners = new Set();
|
||||||
|
this.unregisterMessageFuncs = new Set();
|
||||||
|
|
||||||
|
// Common options for onMessage and onDisconnect.
|
||||||
|
this.handlerBase = {
|
||||||
|
messageFilterStrict: {portId: id},
|
||||||
|
|
||||||
|
filterMessage: (sender, recipient) => {
|
||||||
|
return sender.contextId !== this.context.contextId;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.disconnectHandler = Object.assign({
|
||||||
|
receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
|
||||||
|
}, this.handlerBase);
|
||||||
|
|
||||||
|
MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
|
||||||
|
|
||||||
|
this.context.callOnClose(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
api() {
|
||||||
|
let portObj = Cu.createObjectIn(this.context.cloneScope);
|
||||||
|
|
||||||
|
let portError = null;
|
||||||
|
let publicAPI = {
|
||||||
|
name: this.name,
|
||||||
|
|
||||||
|
disconnect: () => {
|
||||||
|
this.disconnect();
|
||||||
|
},
|
||||||
|
|
||||||
|
postMessage: json => {
|
||||||
|
this.postMessage(json);
|
||||||
|
},
|
||||||
|
|
||||||
|
onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
|
||||||
|
return this.registerOnDisconnect(error => {
|
||||||
|
portError = error && this.context.normalizeError(error);
|
||||||
|
fire.withoutClone(portObj);
|
||||||
|
});
|
||||||
|
}).api(),
|
||||||
|
|
||||||
|
onMessage: new EventManager(this.context, "Port.onMessage", fire => {
|
||||||
|
return this.registerOnMessage(msg => {
|
||||||
|
msg = Cu.cloneInto(msg, this.context.cloneScope);
|
||||||
|
fire.withoutClone(msg, portObj);
|
||||||
|
});
|
||||||
|
}).api(),
|
||||||
|
|
||||||
|
get error() {
|
||||||
|
return portError;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.sender) {
|
||||||
|
publicAPI.sender = this.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectAPI(publicAPI, portObj);
|
||||||
|
return portObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage(json) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendMessage("Extension:Port:PostMessage", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a callback that is called when the port is disconnected by the
|
||||||
|
* *other* end. The callback is automatically unregistered when the port or
|
||||||
|
* context is closed.
|
||||||
|
*
|
||||||
|
* @param {function} callback Called when the other end disconnects the port.
|
||||||
|
* If the disconnect is caused by an error, the first parameter is an
|
||||||
|
* object with a "message" string property that describes the cause.
|
||||||
|
* @returns {function} Function to unregister the listener.
|
||||||
|
*/
|
||||||
|
registerOnDisconnect(callback) {
|
||||||
|
let listener = error => {
|
||||||
|
if (this.context.active && !this.disconnected) {
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.disconnectListeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
this.disconnectListeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a callback that is called when a message is received. The callback
|
||||||
|
* is automatically unregistered when the port or context is closed.
|
||||||
|
*
|
||||||
|
* @param {function} callback Called when a message is received.
|
||||||
|
* @returns {function} Function to unregister the listener.
|
||||||
|
*/
|
||||||
|
registerOnMessage(callback) {
|
||||||
|
let handler = Object.assign({
|
||||||
|
receiveMessage: ({data}) => {
|
||||||
|
if (this.context.active && !this.disconnected) {
|
||||||
|
callback(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, this.handlerBase);
|
||||||
|
|
||||||
|
let unregister = () => {
|
||||||
|
this.unregisterMessageFuncs.delete(unregister);
|
||||||
|
MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
|
||||||
|
};
|
||||||
|
MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
|
||||||
|
this.unregisterMessageFuncs.add(unregister);
|
||||||
|
return unregister;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendMessage(message, data) {
|
||||||
|
let options = {
|
||||||
|
recipient: Object.assign({}, this.recipient, {portId: this.id}),
|
||||||
|
responseType: MessageChannel.RESPONSE_NONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.context.sendMessage(this.senderMM, message, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnection() {
|
||||||
|
MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
|
||||||
|
for (let unregister of this.unregisterMessageFuncs) {
|
||||||
|
unregister();
|
||||||
|
}
|
||||||
|
this.context.forgetOnClose(this);
|
||||||
|
this.disconnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the port from the other end (which may not even exist).
|
||||||
|
*
|
||||||
|
* @param {Error|{message: string}} [error] The reason for disconnecting,
|
||||||
|
* if it is an abnormal disconnect.
|
||||||
|
*/
|
||||||
|
disconnectByOtherEnd(error = null) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let listener of this.disconnectListeners) {
|
||||||
|
listener(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleDisconnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the port from this end.
|
||||||
|
*
|
||||||
|
* @param {Error|{message: string}} [error] The reason for disconnecting,
|
||||||
|
* if it is an abnormal disconnect.
|
||||||
|
*/
|
||||||
|
disconnect(error = null) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
// disconnect() may be called without side effects even after the port is
|
||||||
|
// closed - https://developer.chrome.com/extensions/runtime#type-Port
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleDisconnection();
|
||||||
|
if (error) {
|
||||||
|
error = {message: this.context.normalizeError(error).message};
|
||||||
|
}
|
||||||
|
this._sendMessage("Extension:Port:Disconnect", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each extension context gets its own Messenger object. It handles the
|
||||||
|
* basics of sendMessage, onMessage, connect and onConnect.
|
||||||
|
*
|
||||||
|
* @param {BaseContext} context The context to which this Messenger is tied.
|
||||||
|
* @param {Array<nsIMessageListenerManager>} messageManagers
|
||||||
|
* The message managers used to receive messages (e.g. onMessage/onConnect
|
||||||
|
* requests).
|
||||||
|
* @param {object} sender Describes this sender to the recipient. This object
|
||||||
|
* is extended further by BaseContext's sendMessage method and appears as
|
||||||
|
* the `sender` object to `onConnect` and `onMessage`.
|
||||||
|
* Do not set the `extensionId`, `contextId` or `tab` properties. The former
|
||||||
|
* two are added by BaseContext's sendMessage, while `sender.tab` is set by
|
||||||
|
* the ProxyMessenger in the main process.
|
||||||
|
* @param {object} filter A recipient filter to apply to incoming messages from
|
||||||
|
* the broker. Messages are only handled by this Messenger if all key-value
|
||||||
|
* pairs match the `recipient` as specified by the sender of the message.
|
||||||
|
* In other words, this filter defines the required fields of `recipient`.
|
||||||
|
* @param {object} [optionalFilter] An additional filter to apply to incoming
|
||||||
|
* messages. Unlike `filter`, the keys from `optionalFilter` are allowed to
|
||||||
|
* be omitted from `recipient`. Only keys that are present in both
|
||||||
|
* `optionalFilter` and `recipient` are applied to filter incoming messages.
|
||||||
|
*/
|
||||||
|
class Messenger {
|
||||||
|
constructor(context, messageManagers, sender, filter, optionalFilter) {
|
||||||
|
this.context = context;
|
||||||
|
this.messageManagers = messageManagers;
|
||||||
|
this.sender = sender;
|
||||||
|
this.filter = filter;
|
||||||
|
this.optionalFilter = optionalFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendMessage(messageManager, message, data, recipient) {
|
||||||
|
let options = {
|
||||||
|
recipient,
|
||||||
|
sender: this.sender,
|
||||||
|
responseType: MessageChannel.RESPONSE_FIRST,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.context.sendMessage(messageManager, message, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(messageManager, msg, recipient, responseCallback) {
|
||||||
|
let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
|
||||||
|
.catch(error => {
|
||||||
|
if (error.result == MessageChannel.RESULT_NO_HANDLER) {
|
||||||
|
return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
|
||||||
|
} else if (error.result != MessageChannel.RESULT_NO_RESPONSE) {
|
||||||
|
return Promise.reject({message: error.message});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.context.wrapPromise(promise, responseCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(name) {
|
||||||
|
return new SingletonEventManager(this.context, name, callback => {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
receiveMessage: ({target, data: message, sender, recipient}) => {
|
||||||
|
if (!this.context.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sendResponse;
|
||||||
|
let response = undefined;
|
||||||
|
let promise = new Promise(resolve => {
|
||||||
|
sendResponse = value => {
|
||||||
|
resolve(value);
|
||||||
|
response = promise;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
message = Cu.cloneInto(message, this.context.cloneScope);
|
||||||
|
sender = Cu.cloneInto(sender, this.context.cloneScope);
|
||||||
|
sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
|
||||||
|
|
||||||
|
// Note: We intentionally do not use runSafe here so that any
|
||||||
|
// errors are propagated to the message sender.
|
||||||
|
let result = callback(message, sender, sendResponse);
|
||||||
|
if (result instanceof this.context.cloneScope.Promise) {
|
||||||
|
return result;
|
||||||
|
} else if (result === true) {
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
|
||||||
|
return () => {
|
||||||
|
MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
|
||||||
|
};
|
||||||
|
}).api();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectGetRawPort(messageManager, name, recipient) {
|
||||||
|
let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
|
||||||
|
let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
|
||||||
|
let msg = {name, portId};
|
||||||
|
this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
|
||||||
|
.catch(e => {
|
||||||
|
if (e.result === MessageChannel.RESULT_NO_HANDLER) {
|
||||||
|
e = {message: "Could not establish connection. Receiving end does not exist."};
|
||||||
|
} else if (e.result === MessageChannel.RESULT_DISCONNECTED) {
|
||||||
|
e = null;
|
||||||
|
}
|
||||||
|
port.disconnectByOtherEnd(e);
|
||||||
|
});
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(messageManager, name, recipient) {
|
||||||
|
let port = this.connectGetRawPort(messageManager, name, recipient);
|
||||||
|
return port.api();
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnect(name) {
|
||||||
|
return new SingletonEventManager(this.context, name, callback => {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
delete recipient.tab;
|
||||||
|
}
|
||||||
|
let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
|
||||||
|
this.context.runSafeWithoutClone(callback, port.api());
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
|
||||||
|
return () => {
|
||||||
|
MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
|
||||||
|
};
|
||||||
|
}).api();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var apiManager = new class extends SchemaAPIManager {
|
var apiManager = new class extends SchemaAPIManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -71,6 +441,252 @@ var apiManager = new class extends SchemaAPIManager {
|
|||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that runs an remote implementation of an API.
|
||||||
|
*/
|
||||||
|
class ProxyAPIImplementation extends SchemaAPIInterface {
|
||||||
|
/**
|
||||||
|
* @param {string} namespace The full path to the namespace that contains the
|
||||||
|
* `name` member. This may contain dots, e.g. "storage.local".
|
||||||
|
* @param {string} name The name of the method or property.
|
||||||
|
* @param {ChildAPIManager} childApiManager The owner of this implementation.
|
||||||
|
*/
|
||||||
|
constructor(namespace, name, childApiManager) {
|
||||||
|
super();
|
||||||
|
this.path = `${namespace}.${name}`;
|
||||||
|
this.childApiManager = childApiManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
callFunctionNoReturn(args) {
|
||||||
|
this.childApiManager.callParentFunctionNoReturn(this.path, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
callAsyncFunction(args, callback) {
|
||||||
|
return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener, args) {
|
||||||
|
let set = this.childApiManager.listeners.get(this.path);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
this.childApiManager.listeners.set(this.path, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
set.add(listener);
|
||||||
|
|
||||||
|
if (set.size == 1) {
|
||||||
|
args = args.slice(1);
|
||||||
|
|
||||||
|
this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
|
||||||
|
childId: this.childApiManager.id,
|
||||||
|
path: this.path,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(listener) {
|
||||||
|
let set = this.childApiManager.listeners.get(this.path);
|
||||||
|
if (!set) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set.delete(listener);
|
||||||
|
|
||||||
|
if (set.size == 0) {
|
||||||
|
this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
|
||||||
|
childId: this.childApiManager.id,
|
||||||
|
path: this.path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasListener(listener) {
|
||||||
|
let set = this.childApiManager.listeners.get(this.path);
|
||||||
|
return set ? set.has(listener) : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextId = 1;
|
||||||
|
|
||||||
|
// We create one instance of this class for every extension context
|
||||||
|
// that needs to use remote APIs. It uses the message manager to
|
||||||
|
// communicate with the ParentAPIManager singleton in
|
||||||
|
// Extension.jsm. It handles asynchronous function calls as well as
|
||||||
|
// event listeners.
|
||||||
|
class ChildAPIManager {
|
||||||
|
constructor(context, messageManager, localApis, contextData) {
|
||||||
|
this.context = context;
|
||||||
|
this.messageManager = messageManager;
|
||||||
|
|
||||||
|
// The root namespace of all locally implemented APIs. If an extension calls
|
||||||
|
// an API that does not exist in this object, then the implementation is
|
||||||
|
// delegated to the ParentAPIManager.
|
||||||
|
this.localApis = localApis;
|
||||||
|
|
||||||
|
let id = String(context.extension.id) + "." + String(context.contextId);
|
||||||
|
this.id = id;
|
||||||
|
|
||||||
|
let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
|
||||||
|
Object.assign(data, contextData);
|
||||||
|
|
||||||
|
messageManager.addMessageListener("API:RunListener", this);
|
||||||
|
messageManager.addMessageListener("API:CallResult", this);
|
||||||
|
|
||||||
|
// Map[path -> Set[listener]]
|
||||||
|
// path is, e.g., "runtime.onMessage".
|
||||||
|
this.listeners = new Map();
|
||||||
|
|
||||||
|
// Map[callId -> Deferred]
|
||||||
|
this.callPromises = new Map();
|
||||||
|
|
||||||
|
this.createProxyContextInConstructor(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
createProxyContextInConstructor(data) {
|
||||||
|
this.messageManager.sendAsyncMessage("API:CreateProxyContext", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveMessage({name, data}) {
|
||||||
|
if (data.childId != this.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case "API:RunListener":
|
||||||
|
let listeners = this.listeners.get(data.path);
|
||||||
|
for (let callback of listeners) {
|
||||||
|
this.context.runSafe(callback, ...data.args);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "API:CallResult":
|
||||||
|
let deferred = this.callPromises.get(data.callId);
|
||||||
|
if ("error" in data) {
|
||||||
|
deferred.reject(data.error);
|
||||||
|
} else {
|
||||||
|
deferred.resolve(new SpreadArgs(data.result));
|
||||||
|
}
|
||||||
|
this.callPromises.delete(data.callId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a function in the parent process and ignores its return value.
|
||||||
|
*
|
||||||
|
* @param {string} path The full name of the method, e.g. "tabs.create".
|
||||||
|
* @param {Array} args The parameters for the function.
|
||||||
|
*/
|
||||||
|
callParentFunctionNoReturn(path, args) {
|
||||||
|
this.messageManager.sendAsyncMessage("API:Call", {
|
||||||
|
childId: this.id,
|
||||||
|
path,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls a function in the parent process and returns its result
|
||||||
|
* asynchronously.
|
||||||
|
*
|
||||||
|
* @param {string} path The full name of the method, e.g. "tabs.create".
|
||||||
|
* @param {Array} args The parameters for the function.
|
||||||
|
* @param {function(*)} [callback] The callback to be called when the function
|
||||||
|
* completes.
|
||||||
|
* @returns {Promise|undefined} Must be void if `callback` is set, and a
|
||||||
|
* promise otherwise. The promise is resolved when the function completes.
|
||||||
|
*/
|
||||||
|
callParentAsyncFunction(path, args, callback) {
|
||||||
|
let callId = nextId++;
|
||||||
|
let deferred = PromiseUtils.defer();
|
||||||
|
this.callPromises.set(callId, deferred);
|
||||||
|
|
||||||
|
this.messageManager.sendAsyncMessage("API:Call", {
|
||||||
|
childId: this.id,
|
||||||
|
callId,
|
||||||
|
path,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.context.wrapPromise(deferred.promise, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a proxy for an event in the parent process. The returned event
|
||||||
|
* object shares its internal state with other instances. For instance, if
|
||||||
|
* `removeListener` is used on a listener that was added on another object
|
||||||
|
* through `addListener`, then the event is unregistered.
|
||||||
|
*
|
||||||
|
* @param {string} path The full name of the event, e.g. "tabs.onCreated".
|
||||||
|
* @returns {object} An object with the addListener, removeListener and
|
||||||
|
* hasListener methods. See SchemaAPIInterface for documentation.
|
||||||
|
*/
|
||||||
|
getParentEvent(path) {
|
||||||
|
let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
|
||||||
|
if (!parsed) {
|
||||||
|
throw new Error("getParentEvent: Invalid event name: " + path);
|
||||||
|
}
|
||||||
|
let [, namespace, name] = parsed;
|
||||||
|
let impl = new ProxyAPIImplementation(namespace, name, this);
|
||||||
|
return {
|
||||||
|
addListener: (listener, ...args) => impl.addListener(listener, args),
|
||||||
|
removeListener: (listener) => impl.removeListener(listener),
|
||||||
|
hasListener: (listener) => impl.hasListener(listener),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
|
||||||
|
}
|
||||||
|
|
||||||
|
get cloneScope() {
|
||||||
|
return this.context.cloneScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
get principal() {
|
||||||
|
return this.context.principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldInject(namespace, name, allowedContexts) {
|
||||||
|
// Do not generate content script APIs, unless explicitly allowed.
|
||||||
|
if (this.context.envType === "content_child" &&
|
||||||
|
!allowedContexts.includes("content")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (allowedContexts.includes("addon_parent_only")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getImplementation(namespace, name) {
|
||||||
|
let pathObj = this.localApis;
|
||||||
|
if (pathObj) {
|
||||||
|
for (let part of namespace.split(".")) {
|
||||||
|
pathObj = pathObj[part];
|
||||||
|
if (!pathObj) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pathObj && name in pathObj) {
|
||||||
|
return new LocalAPIImplementation(pathObj, name, this.context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getFallbackImplementation(namespace, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFallbackImplementation(namespace, name) {
|
||||||
|
// No local API found, defer implementation to the parent.
|
||||||
|
return new ProxyAPIImplementation(namespace, name, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPermission(permission) {
|
||||||
|
return this.context.extension.hasPermission(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// A class that behaves identical to a ChildAPIManager, except
|
// A class that behaves identical to a ChildAPIManager, except
|
||||||
// 1) creation of the ProxyContext in the parent is synchronous, and
|
// 1) creation of the ProxyContext in the parent is synchronous, and
|
||||||
// 2) APIs without a local implementation and marked as incompatible with the
|
// 2) APIs without a local implementation and marked as incompatible with the
|
||||||
@ -361,7 +977,7 @@ class ContentGlobal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ExtensionChild = {
|
ExtensionChild = {
|
||||||
// Map<nsIContentFrameMessageManager, ContentGlobal>
|
// Map<nsIContentFrameMessageManager, ContentGlobal>
|
||||||
contentGlobals: new Map(),
|
contentGlobals: new Map(),
|
||||||
|
|
||||||
@ -446,7 +1062,15 @@ this.ExtensionChild = {
|
|||||||
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
|
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
|
||||||
Object.keys(ExtensionChild).forEach(function(key) {
|
Object.keys(ExtensionChild).forEach(function(key) {
|
||||||
if (typeof ExtensionChild[key] == "function") {
|
if (typeof ExtensionChild[key] == "function") {
|
||||||
|
// :/
|
||||||
ExtensionChild[key] = () => {};
|
ExtensionChild[key] = () => {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.assign(ExtensionChild, {
|
||||||
|
ChildAPIManager,
|
||||||
|
Messenger,
|
||||||
|
Port,
|
||||||
|
});
|
||||||
|
|
||||||
|
683
toolkit/components/extensions/ExtensionCommon.jsm
Normal file
683
toolkit/components/extensions/ExtensionCommon.jsm
Normal file
@ -0,0 +1,683 @@
|
|||||||
|
/* 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 module contains utilities and base classes for logic which is
|
||||||
|
* common between the parent and child process, and in particular
|
||||||
|
* between ExtensionParent.jsm and ExtensionChild.jsm.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||||
|
|
||||||
|
/* exported ExtensionCommon */
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["ExtensionCommon"];
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||||
|
"resource://gre/modules/MessageChannel.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||||
|
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
||||||
|
"resource://gre/modules/Schemas.jsm");
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||||
|
|
||||||
|
var {
|
||||||
|
EventEmitter,
|
||||||
|
ExtensionError,
|
||||||
|
SpreadArgs,
|
||||||
|
getConsole,
|
||||||
|
getInnerWindowID,
|
||||||
|
runSafeSync,
|
||||||
|
runSafeSyncWithoutClone,
|
||||||
|
instanceOf,
|
||||||
|
} = ExtensionUtils;
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "console", getConsole);
|
||||||
|
|
||||||
|
let gContextId = 0;
|
||||||
|
|
||||||
|
class BaseContext {
|
||||||
|
constructor(envType, extension) {
|
||||||
|
this.envType = envType;
|
||||||
|
this.onClose = new Set();
|
||||||
|
this.checkedLastError = false;
|
||||||
|
this._lastError = null;
|
||||||
|
this.contextId = `${++gContextId}-${Services.appinfo.uniqueProcessID}`;
|
||||||
|
this.unloaded = false;
|
||||||
|
this.extension = extension;
|
||||||
|
this.jsonSandbox = null;
|
||||||
|
this.active = true;
|
||||||
|
this.incognito = null;
|
||||||
|
this.messageManager = null;
|
||||||
|
this.docShell = null;
|
||||||
|
this.contentWindow = null;
|
||||||
|
this.innerWindowID = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentWindow(contentWindow) {
|
||||||
|
let {document} = contentWindow;
|
||||||
|
let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||||
|
.getInterface(Ci.nsIDocShell);
|
||||||
|
|
||||||
|
this.innerWindowID = getInnerWindowID(contentWindow);
|
||||||
|
this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||||
|
.getInterface(Ci.nsIContentFrameMessageManager);
|
||||||
|
|
||||||
|
if (this.incognito == null) {
|
||||||
|
this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageChannel.setupMessageManagers([this.messageManager]);
|
||||||
|
|
||||||
|
let onPageShow = event => {
|
||||||
|
if (!event || event.target === document) {
|
||||||
|
this.docShell = docShell;
|
||||||
|
this.contentWindow = contentWindow;
|
||||||
|
this.active = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let onPageHide = event => {
|
||||||
|
if (!event || event.target === document) {
|
||||||
|
// Put this off until the next tick.
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
this.docShell = null;
|
||||||
|
this.contentWindow = null;
|
||||||
|
this.active = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onPageShow();
|
||||||
|
contentWindow.addEventListener("pagehide", onPageHide, true);
|
||||||
|
contentWindow.addEventListener("pageshow", onPageShow, true);
|
||||||
|
this.callOnClose({
|
||||||
|
close: () => {
|
||||||
|
onPageHide();
|
||||||
|
if (this.active) {
|
||||||
|
contentWindow.removeEventListener("pagehide", onPageHide, true);
|
||||||
|
contentWindow.removeEventListener("pageshow", onPageShow, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get cloneScope() {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
get principal() {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
runSafe(...args) {
|
||||||
|
if (this.unloaded) {
|
||||||
|
Cu.reportError("context.runSafe called after context unloaded");
|
||||||
|
} else if (!this.active) {
|
||||||
|
Cu.reportError("context.runSafe called while context is inactive");
|
||||||
|
} else {
|
||||||
|
return runSafeSync(this, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runSafeWithoutClone(...args) {
|
||||||
|
if (this.unloaded) {
|
||||||
|
Cu.reportError("context.runSafeWithoutClone called after context unloaded");
|
||||||
|
} else if (!this.active) {
|
||||||
|
Cu.reportError("context.runSafeWithoutClone called while context is inactive");
|
||||||
|
} else {
|
||||||
|
return runSafeSyncWithoutClone(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkLoadURL(url, options = {}) {
|
||||||
|
let ssm = Services.scriptSecurityManager;
|
||||||
|
|
||||||
|
let flags = ssm.STANDARD;
|
||||||
|
if (!options.allowScript) {
|
||||||
|
flags |= ssm.DISALLOW_SCRIPT;
|
||||||
|
}
|
||||||
|
if (!options.allowInheritsPrincipal) {
|
||||||
|
flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
|
||||||
|
}
|
||||||
|
if (options.dontReportErrors) {
|
||||||
|
flags |= ssm.DONT_REPORT_ERRORS;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely call JSON.stringify() on an object that comes from an
|
||||||
|
* extension.
|
||||||
|
*
|
||||||
|
* @param {array<any>} args Arguments for JSON.stringify()
|
||||||
|
* @returns {string} The stringified representation of obj
|
||||||
|
*/
|
||||||
|
jsonStringify(...args) {
|
||||||
|
if (!this.jsonSandbox) {
|
||||||
|
this.jsonSandbox = Cu.Sandbox(this.principal, {
|
||||||
|
sameZoneAs: this.cloneScope,
|
||||||
|
wantXrays: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
callOnClose(obj) {
|
||||||
|
this.onClose.add(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
forgetOnClose(obj) {
|
||||||
|
this.onClose.delete(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around MessageChannel.sendMessage which adds the extension ID
|
||||||
|
* to the recipient object, and ensures replies are not processed after the
|
||||||
|
* context has been unloaded.
|
||||||
|
*
|
||||||
|
* @param {nsIMessageManager} target
|
||||||
|
* @param {string} messageName
|
||||||
|
* @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.sender = options.sender || {};
|
||||||
|
|
||||||
|
// TODO(robwu): This should not unconditionally be overwritten once we
|
||||||
|
// support onMessageExternal / onConnectExternal (bugzil.la/1258360).
|
||||||
|
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;
|
||||||
|
return this._lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
set lastError(val) {
|
||||||
|
this.checkedLastError = false;
|
||||||
|
this._lastError = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the given error object for use by the target scope. If
|
||||||
|
* the target is an error object which belongs to that scope, it is
|
||||||
|
* returned as-is. If it is an ordinary object with a `message`
|
||||||
|
* property, it is converted into an error belonging to the target
|
||||||
|
* scope. If it is an Error object which does *not* belong to the
|
||||||
|
* clone scope, it is reported, and converted to an unexpected
|
||||||
|
* exception error.
|
||||||
|
*
|
||||||
|
* @param {Error|object} error
|
||||||
|
* @returns {Error}
|
||||||
|
*/
|
||||||
|
normalizeError(error) {
|
||||||
|
if (error instanceof this.cloneScope.Error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
let message;
|
||||||
|
if (instanceOf(error, "Object") || error instanceof ExtensionError) {
|
||||||
|
message = error.message;
|
||||||
|
} else if (typeof error == "object" &&
|
||||||
|
this.principal.subsumes(Cu.getObjectPrincipal(error))) {
|
||||||
|
message = error.message;
|
||||||
|
} else {
|
||||||
|
Cu.reportError(error);
|
||||||
|
}
|
||||||
|
message = message || "An unexpected error occurred";
|
||||||
|
return new this.cloneScope.Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value of `.lastError` to `error`, calls the given
|
||||||
|
* callback, and reports an error if the value has not been checked
|
||||||
|
* when the callback returns.
|
||||||
|
*
|
||||||
|
* @param {object} error An object with a `message` property. May
|
||||||
|
* optionally be an `Error` object belonging to the target scope.
|
||||||
|
* @param {function} callback The callback to call.
|
||||||
|
* @returns {*} The return value of callback.
|
||||||
|
*/
|
||||||
|
withLastError(error, callback) {
|
||||||
|
this.lastError = this.normalizeError(error);
|
||||||
|
try {
|
||||||
|
return callback();
|
||||||
|
} finally {
|
||||||
|
if (!this.checkedLastError) {
|
||||||
|
Cu.reportError(`Unchecked lastError value: ${this.lastError}`);
|
||||||
|
}
|
||||||
|
this.lastError = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the given promise so it can be safely returned to extension
|
||||||
|
* code in this context.
|
||||||
|
*
|
||||||
|
* If `callback` is provided, however, it is used as a completion
|
||||||
|
* function for the promise, and no promise is returned. In this case,
|
||||||
|
* the callback is called when the promise resolves or rejects. In the
|
||||||
|
* latter case, `lastError` is set to the rejection value, and the
|
||||||
|
* callback function must check `browser.runtime.lastError` or
|
||||||
|
* `extension.runtime.lastError` in order to prevent it being reported
|
||||||
|
* to the console.
|
||||||
|
*
|
||||||
|
* @param {Promise} promise The promise with which to wrap the
|
||||||
|
* callback. May resolve to a `SpreadArgs` instance, in which case
|
||||||
|
* each element will be used as a separate argument.
|
||||||
|
*
|
||||||
|
* Unless the promise object belongs to the cloneScope global, its
|
||||||
|
* resolution value is cloned into cloneScope prior to calling the
|
||||||
|
* `callback` function or resolving the wrapped promise.
|
||||||
|
*
|
||||||
|
* @param {function} [callback] The callback function to wrap
|
||||||
|
*
|
||||||
|
* @returns {Promise|undefined} If callback is null, a promise object
|
||||||
|
* belonging to the target scope. Otherwise, undefined.
|
||||||
|
*/
|
||||||
|
wrapPromise(promise, callback = null) {
|
||||||
|
let runSafe = this.runSafe.bind(this);
|
||||||
|
if (promise instanceof this.cloneScope.Promise) {
|
||||||
|
runSafe = this.runSafeWithoutClone.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
promise.then(
|
||||||
|
args => {
|
||||||
|
if (this.unloaded) {
|
||||||
|
dump(`Promise resolved after context unloaded\n`);
|
||||||
|
} else if (!this.active) {
|
||||||
|
dump(`Promise resolved while context is inactive\n`);
|
||||||
|
} else if (args instanceof SpreadArgs) {
|
||||||
|
runSafe(callback, ...args);
|
||||||
|
} else {
|
||||||
|
runSafe(callback, args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
this.withLastError(error, () => {
|
||||||
|
if (this.unloaded) {
|
||||||
|
dump(`Promise rejected after context unloaded\n`);
|
||||||
|
} else if (!this.active) {
|
||||||
|
dump(`Promise rejected while context is inactive\n`);
|
||||||
|
} else {
|
||||||
|
this.runSafeWithoutClone(callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new this.cloneScope.Promise((resolve, reject) => {
|
||||||
|
promise.then(
|
||||||
|
value => {
|
||||||
|
if (this.unloaded) {
|
||||||
|
dump(`Promise resolved after context unloaded\n`);
|
||||||
|
} else if (!this.active) {
|
||||||
|
dump(`Promise resolved while context is inactive\n`);
|
||||||
|
} else if (value instanceof SpreadArgs) {
|
||||||
|
runSafe(resolve, value.length == 1 ? value[0] : value);
|
||||||
|
} else {
|
||||||
|
runSafe(resolve, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value => {
|
||||||
|
if (this.unloaded) {
|
||||||
|
dump(`Promise rejected after context unloaded: ${value && value.message}\n`);
|
||||||
|
} else if (!this.active) {
|
||||||
|
dump(`Promise rejected while context is inactive: ${value && value.message}\n`);
|
||||||
|
} else {
|
||||||
|
this.runSafeWithoutClone(reject, this.normalizeError(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unload() {
|
||||||
|
this.unloaded = true;
|
||||||
|
|
||||||
|
MessageChannel.abortResponses({
|
||||||
|
extensionId: this.extension.id,
|
||||||
|
contextId: this.contextId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let obj of this.onClose) {
|
||||||
|
obj.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple proxy for unload(), for use with callOnClose().
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that runs the implementation of a schema API. Instantiations of
|
||||||
|
* this interfaces are used by Schemas.jsm.
|
||||||
|
*
|
||||||
|
* @interface
|
||||||
|
*/
|
||||||
|
class SchemaAPIInterface {
|
||||||
|
/**
|
||||||
|
* Calls this as a function that returns its return value.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {Array} args The parameters for the function.
|
||||||
|
* @returns {*} The return value of the invoked function.
|
||||||
|
*/
|
||||||
|
callFunction(args) {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls this as a function and ignores its return value.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {Array} args The parameters for the function.
|
||||||
|
*/
|
||||||
|
callFunctionNoReturn(args) {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls this as a function that completes asynchronously.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {Array} args The parameters for the function.
|
||||||
|
* @param {function(*)} [callback] The callback to be called when the function
|
||||||
|
* completes.
|
||||||
|
* @returns {Promise|undefined} Must be void if `callback` is set, and a
|
||||||
|
* promise otherwise. The promise is resolved when the function completes.
|
||||||
|
*/
|
||||||
|
callAsyncFunction(args, callback) {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the value of this as a property.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @returns {*} The value of the property.
|
||||||
|
*/
|
||||||
|
getProperty() {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the value to this as property.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {string} value The new value of the property.
|
||||||
|
*/
|
||||||
|
setProperty(value) {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a `listener` to this as an event.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {function} listener The callback to be called when the event fires.
|
||||||
|
* @param {Array} args Extra parameters for EventManager.addListener.
|
||||||
|
* @see EventManager.addListener
|
||||||
|
*/
|
||||||
|
addListener(listener, args) {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether `listener` is listening to this as an event.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {function} listener The event listener.
|
||||||
|
* @returns {boolean} Whether `listener` is registered with this as an event.
|
||||||
|
* @see EventManager.hasListener
|
||||||
|
*/
|
||||||
|
hasListener(listener) {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters `listener` from this as an event.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param {function} listener The event listener.
|
||||||
|
* @see EventManager.removeListener
|
||||||
|
*/
|
||||||
|
removeListener(listener) {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that runs a locally implemented API.
|
||||||
|
*/
|
||||||
|
class LocalAPIImplementation extends SchemaAPIInterface {
|
||||||
|
/**
|
||||||
|
* Constructs an implementation of the `name` method or property of `pathObj`.
|
||||||
|
*
|
||||||
|
* @param {object} pathObj The object containing the member with name `name`.
|
||||||
|
* @param {string} name The name of the implemented member.
|
||||||
|
* @param {BaseContext} context The context in which the schema is injected.
|
||||||
|
*/
|
||||||
|
constructor(pathObj, name, context) {
|
||||||
|
super();
|
||||||
|
this.pathObj = pathObj;
|
||||||
|
this.name = name;
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
callFunction(args) {
|
||||||
|
return this.pathObj[this.name](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
callFunctionNoReturn(args) {
|
||||||
|
this.pathObj[this.name](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
callAsyncFunction(args, callback) {
|
||||||
|
let promise;
|
||||||
|
try {
|
||||||
|
promise = this.pathObj[this.name](...args) || Promise.resolve();
|
||||||
|
} catch (e) {
|
||||||
|
promise = Promise.reject(e);
|
||||||
|
}
|
||||||
|
return this.context.wrapPromise(promise, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProperty() {
|
||||||
|
return this.pathObj[this.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
setProperty(value) {
|
||||||
|
this.pathObj[this.name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener, args) {
|
||||||
|
try {
|
||||||
|
this.pathObj[this.name].addListener.call(null, listener, ...args);
|
||||||
|
} catch (e) {
|
||||||
|
throw this.context.normalizeError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasListener(listener) {
|
||||||
|
return this.pathObj[this.name].hasListener.call(null, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeListener(listener) {
|
||||||
|
this.pathObj[this.name].removeListener.call(null, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object loads the ext-*.js scripts that define the extension API.
|
||||||
|
*
|
||||||
|
* This class instance is shared with the scripts that it loads, so that the
|
||||||
|
* ext-*.js scripts and the instantiator can communicate with each other.
|
||||||
|
*/
|
||||||
|
class SchemaAPIManager extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* @param {string} processType
|
||||||
|
* "main" - The main, one and only chrome browser process.
|
||||||
|
* "addon" - An addon process.
|
||||||
|
* "content" - A content process.
|
||||||
|
*/
|
||||||
|
constructor(processType) {
|
||||||
|
super();
|
||||||
|
this.processType = processType;
|
||||||
|
this.global = this._createExtGlobal();
|
||||||
|
this._scriptScopes = [];
|
||||||
|
this._schemaApis = {
|
||||||
|
addon_parent: [],
|
||||||
|
addon_child: [],
|
||||||
|
content_parent: [],
|
||||||
|
content_child: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a global object that is used as the shared global for all ext-*.js
|
||||||
|
* scripts that are loaded via `loadScript`.
|
||||||
|
*
|
||||||
|
* @returns {object} A sandbox that is used as the global by `loadScript`.
|
||||||
|
*/
|
||||||
|
_createExtGlobal() {
|
||||||
|
let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
|
||||||
|
wantXrays: false,
|
||||||
|
sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this});
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(global, "console", getConsole);
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(global, "require",
|
||||||
|
"resource://devtools/shared/Loader.jsm");
|
||||||
|
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load an ext-*.js script. The script runs in its own scope, if it wishes to
|
||||||
|
* share state with another script it can assign to the `global` variable. If
|
||||||
|
* it wishes to communicate with this API manager, use `extensions`.
|
||||||
|
*
|
||||||
|
* @param {string} scriptUrl The URL of the ext-*.js script.
|
||||||
|
*/
|
||||||
|
loadScript(scriptUrl) {
|
||||||
|
// Create the object in the context of the sandbox so that the script runs
|
||||||
|
// in the sandbox's context instead of here.
|
||||||
|
let scope = Cu.createObjectIn(this.global);
|
||||||
|
|
||||||
|
Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
|
||||||
|
|
||||||
|
// Save the scope to avoid it being garbage collected.
|
||||||
|
this._scriptScopes.push(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by an ext-*.js script to register an API.
|
||||||
|
*
|
||||||
|
* @param {string} namespace The API namespace.
|
||||||
|
* Intended to match the namespace of the generated API, but not used at
|
||||||
|
* the moment - see bugzil.la/1295774.
|
||||||
|
* @param {string} envType Restricts the API to contexts that run in the
|
||||||
|
* given environment. Must be one of the following:
|
||||||
|
* - "addon_parent" - addon APIs that runs in the main process.
|
||||||
|
* - "addon_child" - addon APIs that runs in an addon process.
|
||||||
|
* - "content_parent" - content script APIs that runs in the main process.
|
||||||
|
* - "content_child" - content script APIs that runs in a content process.
|
||||||
|
* @param {function(BaseContext)} getAPI A function that returns an object
|
||||||
|
* that will be merged with |chrome| and |browser|. The next example adds
|
||||||
|
* the create, update and remove methods to the tabs API.
|
||||||
|
*
|
||||||
|
* registerSchemaAPI("tabs", "addon_parent", (context) => ({
|
||||||
|
* tabs: { create, update },
|
||||||
|
* }));
|
||||||
|
* registerSchemaAPI("tabs", "addon_parent", (context) => ({
|
||||||
|
* tabs: { remove },
|
||||||
|
* }));
|
||||||
|
*/
|
||||||
|
registerSchemaAPI(namespace, envType, getAPI) {
|
||||||
|
this._schemaApis[envType].push({namespace, getAPI});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all registered scripts to `obj`.
|
||||||
|
*
|
||||||
|
* @param {BaseContext} context The context for which the API bindings are
|
||||||
|
* generated.
|
||||||
|
* @param {object} obj The destination of the API.
|
||||||
|
*/
|
||||||
|
generateAPIs(context, obj) {
|
||||||
|
let apis = this._schemaApis[context.envType];
|
||||||
|
if (!apis) {
|
||||||
|
Cu.reportError(`No APIs have been registered for ${context.envType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SchemaAPIManager.generateAPIs(context, apis, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mash together all the APIs from `apis` into `obj`.
|
||||||
|
*
|
||||||
|
* @param {BaseContext} context The context for which the API bindings are
|
||||||
|
* generated.
|
||||||
|
* @param {Array} apis A list of objects, see `registerSchemaAPI`.
|
||||||
|
* @param {object} obj The destination of the API.
|
||||||
|
*/
|
||||||
|
static generateAPIs(context, apis, obj) {
|
||||||
|
// Recursively copy properties from source to dest.
|
||||||
|
function copy(dest, source) {
|
||||||
|
for (let prop in source) {
|
||||||
|
let desc = Object.getOwnPropertyDescriptor(source, prop);
|
||||||
|
if (typeof(desc.value) == "object") {
|
||||||
|
if (!(prop in dest)) {
|
||||||
|
dest[prop] = {};
|
||||||
|
}
|
||||||
|
copy(dest[prop], source[prop]);
|
||||||
|
} else {
|
||||||
|
Object.defineProperty(dest, prop, desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let api of apis) {
|
||||||
|
if (Schemas.checkPermissions(api.namespace, context.extension)) {
|
||||||
|
api = api.getAPI(context);
|
||||||
|
copy(obj, api);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtensionCommon = {
|
||||||
|
BaseContext,
|
||||||
|
LocalAPIImplementation,
|
||||||
|
SchemaAPIInterface,
|
||||||
|
SchemaAPIManager,
|
||||||
|
};
|
@ -45,21 +45,28 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
|
|||||||
"resource://gre/modules/WebNavigationFrames.jsm");
|
"resource://gre/modules/WebNavigationFrames.jsm");
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/ExtensionChild.jsm");
|
Cu.import("resource://gre/modules/ExtensionChild.jsm");
|
||||||
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||||
var {
|
|
||||||
runSafeSyncWithoutClone,
|
const {
|
||||||
defineLazyGetter,
|
|
||||||
BaseContext,
|
|
||||||
LocaleData,
|
LocaleData,
|
||||||
Messenger,
|
defineLazyGetter,
|
||||||
flushJarCache,
|
flushJarCache,
|
||||||
getInnerWindowID,
|
getInnerWindowID,
|
||||||
promiseDocumentReady,
|
promiseDocumentReady,
|
||||||
ChildAPIManager,
|
runSafeSyncWithoutClone,
|
||||||
SchemaAPIManager,
|
|
||||||
} = ExtensionUtils;
|
} = ExtensionUtils;
|
||||||
|
|
||||||
|
const {
|
||||||
|
BaseContext,
|
||||||
|
SchemaAPIManager,
|
||||||
|
} = ExtensionCommon;
|
||||||
|
|
||||||
|
const {
|
||||||
|
ChildAPIManager,
|
||||||
|
Messenger,
|
||||||
|
} = ExtensionChild;
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
|
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
|
||||||
|
|
||||||
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
|
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
|
||||||
|
559
toolkit/components/extensions/ExtensionParent.jsm
Normal file
559
toolkit/components/extensions/ExtensionParent.jsm
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
/* 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 module contains code for managing APIs that need to run in the
|
||||||
|
* parent process, and handles the parent side of operations that need
|
||||||
|
* to be proxied from ExtensionChild.jsm.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||||
|
|
||||||
|
/* exported ExtensionParent */
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["ExtensionParent"];
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
|
||||||
|
"resource://gre/modules/AddonManager.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||||
|
"resource://gre/modules/AppConstants.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
||||||
|
"resource://gre/modules/MessageChannel.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
|
||||||
|
"resource://gre/modules/NativeMessaging.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||||
|
"resource://gre/modules/NetUtil.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
||||||
|
"resource://gre/modules/Schemas.jsm");
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||||
|
|
||||||
|
var {
|
||||||
|
BaseContext,
|
||||||
|
SchemaAPIManager,
|
||||||
|
} = ExtensionCommon;
|
||||||
|
|
||||||
|
var {
|
||||||
|
SpreadArgs,
|
||||||
|
defineLazyGetter,
|
||||||
|
findPathInObject,
|
||||||
|
} = ExtensionUtils;
|
||||||
|
|
||||||
|
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
|
||||||
|
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
|
||||||
|
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
|
||||||
|
|
||||||
|
let schemaURLs = new Set();
|
||||||
|
|
||||||
|
if (!AppConstants.RELEASE_OR_BETA) {
|
||||||
|
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
let GlobalManager;
|
||||||
|
let ParentAPIManager;
|
||||||
|
let ProxyMessenger;
|
||||||
|
|
||||||
|
// This object loads the ext-*.js scripts that define the extension API.
|
||||||
|
let apiManager = new class extends SchemaAPIManager {
|
||||||
|
constructor() {
|
||||||
|
super("main");
|
||||||
|
this.initialized = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads all the ext-*.js scripts currently registered.
|
||||||
|
lazyInit() {
|
||||||
|
if (this.initialized) {
|
||||||
|
return this.initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load order matters here. The base manifest defines types which are
|
||||||
|
// extended by other schemas, so needs to be loaded first.
|
||||||
|
let promise = Schemas.load(BASE_SCHEMA).then(() => {
|
||||||
|
let promises = [];
|
||||||
|
for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
|
||||||
|
promises.push(Schemas.load(url));
|
||||||
|
}
|
||||||
|
for (let url of schemaURLs) {
|
||||||
|
promises.push(Schemas.load(url));
|
||||||
|
}
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
|
||||||
|
this.loadScript(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = promise;
|
||||||
|
return this.initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerSchemaAPI(namespace, envType, getAPI) {
|
||||||
|
if (envType == "addon_parent" || envType == "content_parent") {
|
||||||
|
super.registerSchemaAPI(namespace, envType, getAPI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
// Subscribes to messages related to the extension messaging API and forwards it
|
||||||
|
// to the relevant message manager. The "sender" field for the `onMessage` and
|
||||||
|
// `onConnect` events are updated if needed.
|
||||||
|
ProxyMessenger = {
|
||||||
|
_initialized: false,
|
||||||
|
init() {
|
||||||
|
if (this._initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._initialized = true;
|
||||||
|
|
||||||
|
// TODO(robwu): When addons move to a separate process, we should use the
|
||||||
|
// parent process manager(s) of the addon process(es) instead of the
|
||||||
|
// in-process one.
|
||||||
|
let pipmm = Services.ppmm.getChildAt(0);
|
||||||
|
// Listen on the global frame message manager because content scripts send
|
||||||
|
// and receive extension messages via their frame.
|
||||||
|
// Listen on the parent process message manager because `runtime.connect`
|
||||||
|
// and `runtime.sendMessage` requests must be delivered to all frames in an
|
||||||
|
// addon process (by the API contract).
|
||||||
|
// And legacy addons are not associated with a frame, so that is another
|
||||||
|
// reason for having a parent process manager here.
|
||||||
|
let messageManagers = [Services.mm, pipmm];
|
||||||
|
|
||||||
|
MessageChannel.addListener(messageManagers, "Extension:Connect", this);
|
||||||
|
MessageChannel.addListener(messageManagers, "Extension:Message", this);
|
||||||
|
MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
|
||||||
|
MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
|
||||||
|
},
|
||||||
|
|
||||||
|
receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
|
||||||
|
if (recipient.toNativeApp) {
|
||||||
|
let {childId, toNativeApp} = recipient;
|
||||||
|
if (messageName == "Extension:Message") {
|
||||||
|
let context = ParentAPIManager.getContextById(childId);
|
||||||
|
return new NativeApp(context, toNativeApp).sendMessage(data);
|
||||||
|
}
|
||||||
|
if (messageName == "Extension:Connect") {
|
||||||
|
let context = ParentAPIManager.getContextById(childId);
|
||||||
|
NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
|
||||||
|
// native messages are handled by NativeApp.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let extension = GlobalManager.extensionMap.get(sender.extensionId);
|
||||||
|
let receiverMM = this._getMessageManagerForRecipient(recipient);
|
||||||
|
if (!extension || !receiverMM) {
|
||||||
|
return Promise.reject({
|
||||||
|
result: MessageChannel.RESULT_NO_HANDLER,
|
||||||
|
message: "No matching message handler for the given recipient.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((messageName == "Extension:Message" ||
|
||||||
|
messageName == "Extension:Connect") &&
|
||||||
|
apiManager.global.tabGetSender) {
|
||||||
|
// From ext-tabs.js, undefined on Android.
|
||||||
|
apiManager.global.tabGetSender(extension, target, sender);
|
||||||
|
}
|
||||||
|
return MessageChannel.sendMessage(receiverMM, messageName, data, {
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
responseType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} recipient An object that was passed to
|
||||||
|
* `MessageChannel.sendMessage`.
|
||||||
|
* @returns {object|null} The message manager matching the recipient if found.
|
||||||
|
*/
|
||||||
|
_getMessageManagerForRecipient(recipient) {
|
||||||
|
let {extensionId, tabId} = recipient;
|
||||||
|
// tabs.sendMessage / tabs.connect
|
||||||
|
if (tabId) {
|
||||||
|
// `tabId` being set implies that the tabs API is supported, so we don't
|
||||||
|
// need to check whether `TabManager` exists.
|
||||||
|
let tab = apiManager.global.TabManager.getTab(tabId, null, null);
|
||||||
|
return tab && tab.linkedBrowser.messageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// runtime.sendMessage / runtime.connect
|
||||||
|
if (extensionId) {
|
||||||
|
// TODO(robwu): map the extensionId to the addon parent process's message
|
||||||
|
// manager when they run in a separate process.
|
||||||
|
return Services.ppmm.getChildAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Responsible for loading extension APIs into the right globals.
|
||||||
|
GlobalManager = {
|
||||||
|
// Map[extension ID -> Extension]. Determines which extension is
|
||||||
|
// responsible for content under a particular extension ID.
|
||||||
|
extensionMap: new Map(),
|
||||||
|
initialized: false,
|
||||||
|
|
||||||
|
init(extension) {
|
||||||
|
if (this.extensionMap.size == 0) {
|
||||||
|
ProxyMessenger.init();
|
||||||
|
apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extensionMap.set(extension.id, extension);
|
||||||
|
},
|
||||||
|
|
||||||
|
uninit(extension) {
|
||||||
|
this.extensionMap.delete(extension.id);
|
||||||
|
|
||||||
|
if (this.extensionMap.size == 0 && this.initialized) {
|
||||||
|
apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onExtensionBrowser(type, browser) {
|
||||||
|
browser.messageManager.loadFrameScript(`data:,
|
||||||
|
Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
|
||||||
|
ExtensionContent.init(this);
|
||||||
|
addEventListener("unload", function() {
|
||||||
|
ExtensionContent.uninit(this);
|
||||||
|
});
|
||||||
|
`, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
getExtension(extensionId) {
|
||||||
|
return this.extensionMap.get(extensionId);
|
||||||
|
},
|
||||||
|
|
||||||
|
injectInObject(context, isChromeCompat, dest) {
|
||||||
|
apiManager.generateAPIs(context, dest);
|
||||||
|
SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class BrowserDocshellFollower {
|
||||||
|
/**
|
||||||
|
* Follows the <browser> belonging to the `xulBrowser`'s current docshell.
|
||||||
|
*
|
||||||
|
* @param {XULElement} xulBrowser A <browser> tag.
|
||||||
|
* @param {function} onBrowserChange Called when the <browser> changes.
|
||||||
|
*/
|
||||||
|
constructor(xulBrowser, onBrowserChange) {
|
||||||
|
this.xulBrowser = xulBrowser;
|
||||||
|
this.onBrowserChange = onBrowserChange;
|
||||||
|
|
||||||
|
xulBrowser.addEventListener("SwapDocShells", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.xulBrowser.removeEventListener("SwapDocShells", this);
|
||||||
|
this.xulBrowser = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent({detail: otherBrowser}) {
|
||||||
|
this.xulBrowser.removeEventListener("SwapDocShells", this);
|
||||||
|
this.xulBrowser = otherBrowser;
|
||||||
|
this.xulBrowser.addEventListener("SwapDocShells", this);
|
||||||
|
this.onBrowserChange(otherBrowser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProxyContext extends BaseContext {
|
||||||
|
constructor(envType, extension, params, xulBrowser, principal) {
|
||||||
|
super(envType, extension);
|
||||||
|
|
||||||
|
this.uri = NetUtil.newURI(params.url);
|
||||||
|
|
||||||
|
this.incognito = params.incognito;
|
||||||
|
|
||||||
|
// This message manager is used by ParentAPIManager to send messages and to
|
||||||
|
// close the ProxyContext if the underlying message manager closes. This
|
||||||
|
// message manager object may change when `xulBrowser` swaps docshells, e.g.
|
||||||
|
// when a tab is moved to a different window.
|
||||||
|
this.currentMessageManager = xulBrowser.messageManager;
|
||||||
|
this._docShellTracker = new BrowserDocshellFollower(
|
||||||
|
xulBrowser, this.onBrowserChange.bind(this));
|
||||||
|
|
||||||
|
Object.defineProperty(this, "principal", {
|
||||||
|
value: principal, enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.listenerProxies = new Map();
|
||||||
|
|
||||||
|
apiManager.emit("proxy-context-load", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get cloneScope() {
|
||||||
|
return this.sandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBrowserChange(browser) {
|
||||||
|
// Make sure that the message manager is set. Otherwise the ProxyContext may
|
||||||
|
// never be destroyed because the ParentAPIManager would fail to detect that
|
||||||
|
// the message manager is closed.
|
||||||
|
if (!browser.messageManager) {
|
||||||
|
throw new Error("BrowserDocshellFollower: The new browser has no message manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentMessageManager = browser.messageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
this.unload();
|
||||||
|
}
|
||||||
|
|
||||||
|
unload() {
|
||||||
|
if (this.unloaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._docShellTracker.destroy();
|
||||||
|
super.unload();
|
||||||
|
apiManager.emit("proxy-context-unload", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineLazyGetter(ProxyContext.prototype, "apiObj", function() {
|
||||||
|
let obj = {};
|
||||||
|
GlobalManager.injectInObject(this, false, obj);
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
defineLazyGetter(ProxyContext.prototype, "sandbox", function() {
|
||||||
|
return Cu.Sandbox(this.principal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The parent ProxyContext of an ExtensionContext in ExtensionChild.jsm.
|
||||||
|
class ExtensionChildProxyContext extends ProxyContext {
|
||||||
|
constructor(envType, extension, params, xulBrowser) {
|
||||||
|
super(envType, extension, params, xulBrowser, extension.principal);
|
||||||
|
|
||||||
|
this.viewType = params.viewType;
|
||||||
|
// WARNING: The xulBrowser may change when docShells are swapped, e.g. when
|
||||||
|
// the tab moves to a different window.
|
||||||
|
this.xulBrowser = xulBrowser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window that contains this context. This may change due to moving tabs.
|
||||||
|
get xulWindow() {
|
||||||
|
return this.xulBrowser.ownerGlobal;
|
||||||
|
}
|
||||||
|
|
||||||
|
get windowId() {
|
||||||
|
if (!apiManager.global.WindowManager || this.viewType == "background") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// viewType popup or tab:
|
||||||
|
return apiManager.global.WindowManager.getId(this.xulWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
get tabId() {
|
||||||
|
if (!apiManager.global.TabManager) {
|
||||||
|
return; // Not yet supported on Android.
|
||||||
|
}
|
||||||
|
let {gBrowser} = this.xulBrowser.ownerGlobal;
|
||||||
|
let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
|
||||||
|
return tab && apiManager.global.TabManager.getId(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBrowserChange(browser) {
|
||||||
|
super.onBrowserChange(browser);
|
||||||
|
this.xulBrowser = browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
apiManager.emit("page-shutdown", this);
|
||||||
|
super.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParentAPIManager = {
|
||||||
|
proxyContexts: new Map(),
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Services.obs.addObserver(this, "message-manager-close", false);
|
||||||
|
|
||||||
|
Services.mm.addMessageListener("API:CreateProxyContext", this);
|
||||||
|
Services.mm.addMessageListener("API:CloseProxyContext", this, true);
|
||||||
|
Services.mm.addMessageListener("API:Call", this);
|
||||||
|
Services.mm.addMessageListener("API:AddListener", this);
|
||||||
|
Services.mm.addMessageListener("API:RemoveListener", this);
|
||||||
|
},
|
||||||
|
|
||||||
|
observe(subject, topic, data) {
|
||||||
|
if (topic === "message-manager-close") {
|
||||||
|
let mm = subject;
|
||||||
|
for (let [childId, context] of this.proxyContexts) {
|
||||||
|
if (context.currentMessageManager === mm) {
|
||||||
|
this.closeProxyContext(childId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
shutdownExtension(extensionId) {
|
||||||
|
for (let [childId, context] of this.proxyContexts) {
|
||||||
|
if (context.extension.id == extensionId) {
|
||||||
|
context.shutdown();
|
||||||
|
this.proxyContexts.delete(childId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
receiveMessage({name, data, target}) {
|
||||||
|
switch (name) {
|
||||||
|
case "API:CreateProxyContext":
|
||||||
|
this.createProxyContext(data, target);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "API:CloseProxyContext":
|
||||||
|
this.closeProxyContext(data.childId);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "API:Call":
|
||||||
|
this.call(data, target);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "API:AddListener":
|
||||||
|
this.addListener(data, target);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "API:RemoveListener":
|
||||||
|
this.removeListener(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createProxyContext(data, target) {
|
||||||
|
let {envType, extensionId, childId, principal} = data;
|
||||||
|
if (this.proxyContexts.has(childId)) {
|
||||||
|
throw new Error("A WebExtension context with the given ID already exists!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let extension = GlobalManager.getExtension(extensionId);
|
||||||
|
if (!extension) {
|
||||||
|
throw new Error(`No WebExtension found with ID ${extensionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let context;
|
||||||
|
if (envType == "addon_parent") {
|
||||||
|
// Privileged addon contexts can only be loaded in documents whose main
|
||||||
|
// frame is also the same addon.
|
||||||
|
if (principal.URI.prePath !== extension.baseURI.prePath ||
|
||||||
|
!target.contentPrincipal.subsumes(principal)) {
|
||||||
|
throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
|
||||||
|
}
|
||||||
|
context = new ExtensionChildProxyContext(envType, extension, data, target);
|
||||||
|
} else if (envType == "content_parent") {
|
||||||
|
context = new ProxyContext(envType, extension, data, target, principal);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid WebExtension context envType: ${envType}`);
|
||||||
|
}
|
||||||
|
this.proxyContexts.set(childId, context);
|
||||||
|
},
|
||||||
|
|
||||||
|
closeProxyContext(childId) {
|
||||||
|
let context = this.proxyContexts.get(childId);
|
||||||
|
if (context) {
|
||||||
|
context.unload();
|
||||||
|
this.proxyContexts.delete(childId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
call(data, target) {
|
||||||
|
let context = this.getContextById(data.childId);
|
||||||
|
if (context.currentMessageManager !== target.messageManager) {
|
||||||
|
Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let args = Cu.cloneInto(data.args, context.sandbox);
|
||||||
|
let result = findPathInObject(context.apiObj, data.path)(...args);
|
||||||
|
|
||||||
|
if (data.callId) {
|
||||||
|
result = result || Promise.resolve();
|
||||||
|
|
||||||
|
result.then(result => {
|
||||||
|
result = result instanceof SpreadArgs ? [...result] : [result];
|
||||||
|
|
||||||
|
context.currentMessageManager.sendAsyncMessage("API:CallResult", {
|
||||||
|
childId: data.childId,
|
||||||
|
callId: data.callId,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
}, error => {
|
||||||
|
error = context.normalizeError(error);
|
||||||
|
context.currentMessageManager.sendAsyncMessage("API:CallResult", {
|
||||||
|
childId: data.childId,
|
||||||
|
callId: data.callId,
|
||||||
|
error: {message: error.message},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (data.callId) {
|
||||||
|
let error = context.normalizeError(e);
|
||||||
|
context.currentMessageManager.sendAsyncMessage("API:CallResult", {
|
||||||
|
childId: data.childId,
|
||||||
|
callId: data.callId,
|
||||||
|
error: {message: error.message},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Cu.reportError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addListener(data, target) {
|
||||||
|
let context = this.getContextById(data.childId);
|
||||||
|
if (context.currentMessageManager !== target.messageManager) {
|
||||||
|
Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function listener(...listenerArgs) {
|
||||||
|
context.currentMessageManager.sendAsyncMessage("API:RunListener", {
|
||||||
|
childId: data.childId,
|
||||||
|
path: data.path,
|
||||||
|
args: listenerArgs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
context.listenerProxies.set(data.path, listener);
|
||||||
|
|
||||||
|
let args = Cu.cloneInto(data.args, context.sandbox);
|
||||||
|
findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeListener(data) {
|
||||||
|
let context = this.getContextById(data.childId);
|
||||||
|
let listener = context.listenerProxies.get(data.path);
|
||||||
|
findPathInObject(context.apiObj, data.path).removeListener(listener);
|
||||||
|
},
|
||||||
|
|
||||||
|
getContextById(childId) {
|
||||||
|
let context = this.proxyContexts.get(childId);
|
||||||
|
if (!context) {
|
||||||
|
let error = new Error("WebExtension context not found!");
|
||||||
|
Cu.reportError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ParentAPIManager.init();
|
||||||
|
|
||||||
|
|
||||||
|
const ExtensionParent = {
|
||||||
|
GlobalManager,
|
||||||
|
ParentAPIManager,
|
||||||
|
apiManager,
|
||||||
|
};
|
343
toolkit/components/extensions/ExtensionTestCommon.jsm
Normal file
343
toolkit/components/extensions/ExtensionTestCommon.jsm
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
/* 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 module contains extension testing helper logic which is common
|
||||||
|
* between all test suites.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* exported ExtensionTestCommon, MockExtension */
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||||
|
|
||||||
|
Cu.importGlobalProperties(["TextEncoder"]);
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
|
||||||
|
"resource://gre/modules/AddonManager.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||||
|
"resource://gre/modules/AppConstants.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
|
||||||
|
"resource://gre/modules/Extension.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
|
||||||
|
"resource://gre/modules/ExtensionParent.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||||
|
"resource://gre/modules/FileUtils.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
|
"resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "apiManager",
|
||||||
|
() => ExtensionParent.apiManager);
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
|
||||||
|
"@mozilla.org/uuid-generator;1",
|
||||||
|
"nsIUUIDGenerator");
|
||||||
|
|
||||||
|
const {
|
||||||
|
flushJarCache,
|
||||||
|
instanceOf,
|
||||||
|
} = ExtensionUtils;
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A skeleton Extension-like object, used for testing, which installs an
|
||||||
|
* add-on via the add-on manager when startup() is called, and
|
||||||
|
* uninstalles it on shutdown().
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @param {nsIFile} file
|
||||||
|
* @param {nsIURI} rootURI
|
||||||
|
* @param {string} installType
|
||||||
|
*/
|
||||||
|
class MockExtension {
|
||||||
|
constructor(file, rootURI, installType) {
|
||||||
|
this.id = null;
|
||||||
|
this.file = file;
|
||||||
|
this.rootURI = rootURI;
|
||||||
|
this.installType = installType;
|
||||||
|
this.addon = null;
|
||||||
|
|
||||||
|
let promiseEvent = eventName => new Promise(resolve => {
|
||||||
|
let onstartup = (msg, extension) => {
|
||||||
|
if (this.addon && extension.id == this.addon.id) {
|
||||||
|
apiManager.off(eventName, onstartup);
|
||||||
|
|
||||||
|
this.id = extension.id;
|
||||||
|
this._extension = extension;
|
||||||
|
resolve(extension);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
apiManager.on(eventName, onstartup);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._extension = null;
|
||||||
|
this._extensionPromise = promiseEvent("startup");
|
||||||
|
this._readyPromise = promiseEvent("ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
testMessage(...args) {
|
||||||
|
return this._extension.testMessage(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(...args) {
|
||||||
|
this._extensionPromise.then(extension => {
|
||||||
|
extension.on(...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
off(...args) {
|
||||||
|
this._extensionPromise.then(extension => {
|
||||||
|
extension.off(...args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startup() {
|
||||||
|
if (this.installType == "temporary") {
|
||||||
|
return AddonManager.installTemporaryAddon(this.file).then(addon => {
|
||||||
|
this.addon = addon;
|
||||||
|
return this._readyPromise;
|
||||||
|
});
|
||||||
|
} else if (this.installType == "permanent") {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
AddonManager.getInstallForFile(this.file, install => {
|
||||||
|
let listener = {
|
||||||
|
onInstallFailed: reject,
|
||||||
|
onInstallEnded: (install, newAddon) => {
|
||||||
|
this.addon = newAddon;
|
||||||
|
resolve(this._readyPromise);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
install.addListener(listener);
|
||||||
|
install.install();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error("installType must be one of: temporary, permanent");
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
this.addon.uninstall();
|
||||||
|
return this.cleanupGeneratedFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupGeneratedFile() {
|
||||||
|
flushJarCache(this.file);
|
||||||
|
return OS.File.remove(this.file.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExtensionTestCommon {
|
||||||
|
/**
|
||||||
|
* This code is designed to make it easy to test a WebExtension
|
||||||
|
* without creating a bunch of files. Everything is contained in a
|
||||||
|
* single JSON blob.
|
||||||
|
*
|
||||||
|
* Properties:
|
||||||
|
* "background": "<JS code>"
|
||||||
|
* A script to be loaded as the background script.
|
||||||
|
* The "background" section of the "manifest" property is overwritten
|
||||||
|
* if this is provided.
|
||||||
|
* "manifest": {...}
|
||||||
|
* Contents of manifest.json
|
||||||
|
* "files": {"filename1": "contents1", ...}
|
||||||
|
* Data to be included as files. Can be referenced from the manifest.
|
||||||
|
* If a manifest file is provided here, it takes precedence over
|
||||||
|
* a generated one. Always use "/" as a directory separator.
|
||||||
|
* Directories should appear here only implicitly (as a prefix
|
||||||
|
* to file names)
|
||||||
|
*
|
||||||
|
* To make things easier, the value of "background" and "files"[] can
|
||||||
|
* be a function, which is converted to source that is run.
|
||||||
|
*
|
||||||
|
* The generated extension is stored in the system temporary directory,
|
||||||
|
* and an nsIFile object pointing to it is returned.
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @returns {nsIFile}
|
||||||
|
*/
|
||||||
|
static generateXPI(data) {
|
||||||
|
let manifest = data.manifest;
|
||||||
|
if (!manifest) {
|
||||||
|
manifest = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = data.files;
|
||||||
|
if (!files) {
|
||||||
|
files = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function provide(obj, keys, value, override = false) {
|
||||||
|
if (keys.length == 1) {
|
||||||
|
if (!(keys[0] in obj) || override) {
|
||||||
|
obj[keys[0]] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!(keys[0] in obj)) {
|
||||||
|
obj[keys[0]] = {};
|
||||||
|
}
|
||||||
|
provide(obj[keys[0]], keys.slice(1), value, override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(manifest, ["name"], "Generated extension");
|
||||||
|
provide(manifest, ["manifest_version"], 2);
|
||||||
|
provide(manifest, ["version"], "1.0");
|
||||||
|
|
||||||
|
if (data.background) {
|
||||||
|
let bgScript = uuidGen.generateUUID().number + ".js";
|
||||||
|
|
||||||
|
provide(manifest, ["background", "scripts"], [bgScript], true);
|
||||||
|
files[bgScript] = data.background;
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(files, ["manifest.json"], manifest);
|
||||||
|
|
||||||
|
if (data.embedded) {
|
||||||
|
// Package this as a webextension embedded inside a legacy
|
||||||
|
// extension.
|
||||||
|
|
||||||
|
let xpiFiles = {
|
||||||
|
"install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||||
|
<Description about="urn:mozilla:install-manifest"
|
||||||
|
em:id="${manifest.applications.gecko.id}"
|
||||||
|
em:name="${manifest.name}"
|
||||||
|
em:type="2"
|
||||||
|
em:version="${manifest.version}"
|
||||||
|
em:description=""
|
||||||
|
em:hasEmbeddedWebExtension="true"
|
||||||
|
em:bootstrap="true">
|
||||||
|
|
||||||
|
<!-- Firefox -->
|
||||||
|
<em:targetApplication>
|
||||||
|
<Description
|
||||||
|
em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
|
||||||
|
em:minVersion="51.0a1"
|
||||||
|
em:maxVersion="*"/>
|
||||||
|
</em:targetApplication>
|
||||||
|
</Description>
|
||||||
|
</RDF>
|
||||||
|
`,
|
||||||
|
|
||||||
|
"bootstrap.js": `
|
||||||
|
function install() {}
|
||||||
|
function uninstall() {}
|
||||||
|
function shutdown() {}
|
||||||
|
|
||||||
|
function startup(data) {
|
||||||
|
data.webExtension.startup();
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let [path, data] of Object.entries(files)) {
|
||||||
|
xpiFiles[`webextension/${path}`] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
files = xpiFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.generateZipFile(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateZipFile(files, baseName = "generated-extension.xpi") {
|
||||||
|
let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
|
||||||
|
let zipW = new ZipWriter();
|
||||||
|
|
||||||
|
let file = FileUtils.getFile("TmpD", [baseName]);
|
||||||
|
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
||||||
|
|
||||||
|
const MODE_WRONLY = 0x02;
|
||||||
|
const MODE_TRUNCATE = 0x20;
|
||||||
|
zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
|
||||||
|
|
||||||
|
// Needs to be in microseconds for some reason.
|
||||||
|
let time = Date.now() * 1000;
|
||||||
|
|
||||||
|
function generateFile(filename) {
|
||||||
|
let components = filename.split("/");
|
||||||
|
let path = "";
|
||||||
|
for (let component of components.slice(0, -1)) {
|
||||||
|
path += component + "/";
|
||||||
|
if (!zipW.hasEntry(path)) {
|
||||||
|
zipW.addEntryDirectory(path, time, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let filename in files) {
|
||||||
|
let script = files[filename];
|
||||||
|
if (typeof(script) == "function") {
|
||||||
|
script = "(" + script.toString() + ")()";
|
||||||
|
} else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
|
||||||
|
script = JSON.stringify(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instanceOf(script, "ArrayBuffer")) {
|
||||||
|
script = new TextEncoder("utf-8").encode(script).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
|
||||||
|
stream.setData(script, 0, script.byteLength);
|
||||||
|
|
||||||
|
generateFile(filename);
|
||||||
|
zipW.addEntryStream(filename, time, 0, stream, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
zipW.close();
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new extension using |Extension.generateXPI|, and initializes a
|
||||||
|
* new |Extension| instance which will execute it.
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @returns {Extension}
|
||||||
|
*/
|
||||||
|
static generate(data) {
|
||||||
|
let file = this.generateXPI(data);
|
||||||
|
|
||||||
|
flushJarCache(file);
|
||||||
|
Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
|
||||||
|
|
||||||
|
let fileURI = Services.io.newFileURI(file);
|
||||||
|
let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
|
||||||
|
|
||||||
|
// This may be "temporary" or "permanent".
|
||||||
|
if (data.useAddonManager) {
|
||||||
|
return new MockExtension(file, jarURI, data.useAddonManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
let id;
|
||||||
|
if (data.manifest) {
|
||||||
|
if (data.manifest.applications && data.manifest.applications.gecko) {
|
||||||
|
id = data.manifest.applications.gecko.id;
|
||||||
|
} else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
|
||||||
|
id = data.manifest.browser_specific_settings.gecko.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!id) {
|
||||||
|
id = uuidGen.generateUUID().number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Extension({
|
||||||
|
id,
|
||||||
|
resourceURI: jarURI,
|
||||||
|
cleanupFile: file,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -22,12 +22,16 @@ XPCOMUtils.defineLazyModuleGetter(this, "Extension",
|
|||||||
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
||||||
"resource://gre/modules/Services.jsm");
|
"resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
Cu.import("resource://gre/modules/ExtensionChild.jsm");
|
||||||
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
|
|
||||||
var {
|
var {
|
||||||
BaseContext,
|
BaseContext,
|
||||||
|
} = ExtensionCommon;
|
||||||
|
|
||||||
|
var {
|
||||||
Messenger,
|
Messenger,
|
||||||
} = ExtensionUtils;
|
} = ExtensionChild;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instances created from this class provide to a legacy extension
|
* Instances created from this class provide to a legacy extension
|
||||||
|
@ -17,8 +17,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
|||||||
"resource://gre/modules/AppConstants.jsm");
|
"resource://gre/modules/AppConstants.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||||
"resource://gre/modules/AsyncShutdown.jsm");
|
"resource://gre/modules/AsyncShutdown.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
|
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
|
||||||
"resource://gre/modules/ExtensionUtils.jsm");
|
"resource://gre/modules/ExtensionChild.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
"resource://gre/modules/osfile.jsm");
|
"resource://gre/modules/osfile.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
|
||||||
@ -231,7 +231,7 @@ this.NativeApp = class extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
static onConnectNative(context, messageManager, portId, sender, application) {
|
static onConnectNative(context, messageManager, portId, sender, application) {
|
||||||
let app = new NativeApp(context, application);
|
let app = new NativeApp(context, application);
|
||||||
let port = new ExtensionUtils.Port(context, messageManager, [messageManager], "", portId, sender, sender);
|
let port = new ExtensionChild.Port(context, messageManager, [messageManager], "", portId, sender, sender);
|
||||||
app.once("disconnect", (what, err) => port.disconnect(err));
|
app.once("disconnect", (what, err) => port.disconnect(err));
|
||||||
|
|
||||||
/* eslint-disable mozilla/balanced-listeners */
|
/* eslint-disable mozilla/balanced-listeners */
|
||||||
|
@ -8,8 +8,10 @@ EXTRA_JS_MODULES += [
|
|||||||
'Extension.jsm',
|
'Extension.jsm',
|
||||||
'ExtensionAPI.jsm',
|
'ExtensionAPI.jsm',
|
||||||
'ExtensionChild.jsm',
|
'ExtensionChild.jsm',
|
||||||
|
'ExtensionCommon.jsm',
|
||||||
'ExtensionContent.jsm',
|
'ExtensionContent.jsm',
|
||||||
'ExtensionManagement.jsm',
|
'ExtensionManagement.jsm',
|
||||||
|
'ExtensionParent.jsm',
|
||||||
'ExtensionStorage.jsm',
|
'ExtensionStorage.jsm',
|
||||||
'ExtensionUtils.jsm',
|
'ExtensionUtils.jsm',
|
||||||
'LegacyExtensionsUtils.jsm',
|
'LegacyExtensionsUtils.jsm',
|
||||||
@ -23,6 +25,7 @@ EXTRA_COMPONENTS += [
|
|||||||
]
|
]
|
||||||
|
|
||||||
TESTING_JS_MODULES += [
|
TESTING_JS_MODULES += [
|
||||||
|
'ExtensionTestCommon.jsm',
|
||||||
'ExtensionXPCShellUtils.jsm',
|
'ExtensionXPCShellUtils.jsm',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4,7 +4,11 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
let {ExtensionUtils: {SchemaAPIManager}} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
|
|
||||||
|
const {
|
||||||
|
SchemaAPIManager,
|
||||||
|
} = ExtensionCommon;
|
||||||
|
|
||||||
this.unknownvar = "Some module-global var";
|
this.unknownvar = "Some module-global var";
|
||||||
|
|
||||||
|
@ -4,9 +4,14 @@ const global = this;
|
|||||||
|
|
||||||
Cu.import("resource://gre/modules/Timer.jsm");
|
Cu.import("resource://gre/modules/Timer.jsm");
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
||||||
|
|
||||||
var {
|
var {
|
||||||
BaseContext,
|
BaseContext,
|
||||||
|
} = ExtensionCommon;
|
||||||
|
|
||||||
|
var {
|
||||||
EventManager,
|
EventManager,
|
||||||
SingletonEventManager,
|
SingletonEventManager,
|
||||||
} = ExtensionUtils;
|
} = ExtensionUtils;
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
Components.utils.import("resource://gre/modules/Schemas.jsm");
|
Components.utils.import("resource://gre/modules/Schemas.jsm");
|
||||||
Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
|
Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
|
||||||
Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
|
Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
|
|
||||||
let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionUtils;
|
let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon;
|
||||||
|
|
||||||
let json = [
|
let json = [
|
||||||
{namespace: "testing",
|
{namespace: "testing",
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
|
Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
Components.utils.import("resource://gre/modules/Schemas.jsm");
|
Components.utils.import("resource://gre/modules/Schemas.jsm");
|
||||||
|
|
||||||
let {
|
let {
|
||||||
BaseContext,
|
BaseContext,
|
||||||
SchemaAPIManager,
|
SchemaAPIManager,
|
||||||
} = ExtensionUtils;
|
} = ExtensionCommon;
|
||||||
|
|
||||||
let nestedNamespaceJson = [
|
let nestedNamespaceJson = [
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
|
Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
Components.utils.import("resource://gre/modules/Schemas.jsm");
|
Components.utils.import("resource://gre/modules/Schemas.jsm");
|
||||||
|
|
||||||
let {BaseContext, LocalAPIImplementation} = ExtensionUtils;
|
let {BaseContext, LocalAPIImplementation} = ExtensionCommon;
|
||||||
|
|
||||||
let schemaJson = [
|
let schemaJson = [
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
/* global OS, HostManifestManager, NativeApp */
|
/* global OS, HostManifestManager, NativeApp */
|
||||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||||
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
|
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
|
||||||
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
|
||||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||||
Cu.import("resource://gre/modules/Schemas.jsm");
|
Cu.import("resource://gre/modules/Schemas.jsm");
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
@ -84,7 +84,7 @@ let context = {
|
|||||||
forgetOnClose: () => {},
|
forgetOnClose: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
class MockContext extends ExtensionUtils.BaseContext {
|
class MockContext extends ExtensionCommon.BaseContext {
|
||||||
constructor(extensionId) {
|
constructor(extensionId) {
|
||||||
let fakeExtension = {id: extensionId};
|
let fakeExtension = {id: extensionId};
|
||||||
super("testEnv", fakeExtension);
|
super("testEnv", fakeExtension);
|
||||||
|
Loading…
Reference in New Issue
Block a user