summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionParent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/ExtensionParent.jsm')
-rw-r--r--toolkit/components/webextensions/ExtensionParent.jsm551
1 files changed, 551 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/ExtensionParent.jsm b/toolkit/components/webextensions/ExtensionParent.jsm
new file mode 100644
index 000000000..b88500d1e
--- /dev/null
+++ b/toolkit/components/webextensions/ExtensionParent.jsm
@@ -0,0 +1,551 @@
+/* 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 {
+ MessageManagerProxy,
+ 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);
+ },
+};
+
+/**
+ * The proxied parent side of a context in ExtensionChild.jsm, for the
+ * parent side of a proxied API.
+ */
+class ProxyContextParent 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.messageManagerProxy = new MessageManagerProxy(xulBrowser);
+
+ 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;
+ }
+
+ get xulBrowser() {
+ return this.messageManagerProxy.eventTarget;
+ }
+
+ get parentMessageManager() {
+ return this.messageManagerProxy.messageManager;
+ }
+
+ shutdown() {
+ this.unload();
+ }
+
+ unload() {
+ if (this.unloaded) {
+ return;
+ }
+ this.messageManagerProxy.dispose();
+ super.unload();
+ apiManager.emit("proxy-context-unload", this);
+ }
+}
+
+defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
+ let obj = {};
+ GlobalManager.injectInObject(this, false, obj);
+ return obj;
+});
+
+defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
+ return Cu.Sandbox(this.principal);
+});
+
+/**
+ * The parent side of proxied API context for extension content script
+ * running in ExtensionContent.jsm.
+ */
+class ContentScriptContextParent extends ProxyContextParent {
+}
+
+/**
+ * The parent side of proxied API context for extension page, such as a
+ * background script, a tab page, or a popup, running in
+ * ExtensionChild.jsm.
+ */
+class ExtensionPageContextParent extends ProxyContextParent {
+ constructor(envType, extension, params, xulBrowser) {
+ super(envType, extension, params, xulBrowser, extension.principal);
+
+ this.viewType = params.viewType;
+ }
+
+ // 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.parentMessageManager === 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 ExtensionPageContextParent(envType, extension, data, target);
+ } else if (envType == "content_parent") {
+ context = new ContentScriptContextParent(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.parentMessageManager !== target.messageManager) {
+ throw new Error("Got message on unexpected message manager");
+ }
+
+ let reply = result => {
+ if (!context.parentMessageManager) {
+ Cu.reportError("Cannot send function call result: other side closed connection");
+ return;
+ }
+
+ context.parentMessageManager.sendAsyncMessage(
+ "API:CallResult",
+ Object.assign({
+ childId: data.childId,
+ callId: data.callId,
+ }, result));
+ };
+
+ 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];
+
+ reply({result});
+ }, error => {
+ error = context.normalizeError(error);
+ reply({error: {message: error.message}});
+ });
+ }
+ } catch (e) {
+ if (data.callId) {
+ let error = context.normalizeError(e);
+ reply({error: {message: error.message}});
+ } else {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ addListener(data, target) {
+ let context = this.getContextById(data.childId);
+ if (context.parentMessageManager !== target.messageManager) {
+ throw new Error("Got message on unexpected message manager");
+ }
+
+ let {childId} = data;
+
+ function listener(...listenerArgs) {
+ return context.sendMessage(
+ context.parentMessageManager,
+ "API:RunListener",
+ {
+ childId,
+ listenerId: data.listenerId,
+ path: data.path,
+ args: listenerArgs,
+ },
+ {
+ recipient: {childId},
+ });
+ }
+
+ context.listenerProxies.set(data.listenerId, 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.listenerId);
+ 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,
+};