diff options
Diffstat (limited to 'toolkit/components/webextensions/ExtensionParent.jsm')
-rw-r--r-- | toolkit/components/webextensions/ExtensionParent.jsm | 551 |
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, +}; |