diff options
Diffstat (limited to 'toolkit/components/webextensions/ExtensionChild.jsm')
-rw-r--r-- | toolkit/components/webextensions/ExtensionChild.jsm | 1058 |
1 files changed, 0 insertions, 1058 deletions
diff --git a/toolkit/components/webextensions/ExtensionChild.jsm b/toolkit/components/webextensions/ExtensionChild.jsm deleted file mode 100644 index 5dc4e2277..000000000 --- a/toolkit/components/webextensions/ExtensionChild.jsm +++ /dev/null @@ -1,1058 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ExtensionChild"]; - -/* - * This file handles addon logic that is independent of the chrome process. - * When addons run out-of-process, this is the main entry point. - * Its primary function is managing addon globals. - * - * Don't put contentscript logic here, use ExtensionContent.jsm instead. - */ - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -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, "NativeApp", - "resource://gre/modules/NativeMessaging.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", - "resource://gre/modules/PromiseUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); - -const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon"; - -Cu.import("resource://gre/modules/ExtensionCommon.jsm"); -Cu.import("resource://gre/modules/ExtensionUtils.jsm"); - -const { - DefaultMap, - EventManager, - SingletonEventManager, - SpreadArgs, - defineLazyGetter, - getInnerWindowID, - getMessageManager, - getUniqueId, - injectAPI, -} = ExtensionUtils; - -const { - BaseContext, - LocalAPIImplementation, - SchemaAPIInterface, - SchemaAPIManager, -} = ExtensionCommon; - -var ExtensionChild; - -/** - * 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(); - } -} - -class NativePort extends Port { - postMessage(data) { - data = NativeApp.encodeMessage(this.context, data); - - return super.postMessage(data); - } -} - -/** - * 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); - } - - sendNativeMessage(messageManager, msg, recipient, responseCallback) { - msg = NativeApp.encodeMessage(this.context, msg); - return this.sendMessage(messageManager, msg, recipient, responseCallback); - } - - _onMessage(name, filter) { - 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 && - filter(sender, recipient)); - }, - - 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(); - } - - onMessage(name) { - return this._onMessage(name, sender => sender.id === this.sender.id); - } - - onMessageExternal(name) { - return this._onMessage(name, sender => sender.id !== this.sender.id); - } - - _connect(messageManager, port, recipient) { - let msg = { - name: port.name, - portId: port.id, - }; - - this._sendMessage(messageManager, "Extension:Connect", msg, recipient).catch(error => { - if (error.result === MessageChannel.RESULT_NO_HANDLER) { - error = {message: "Could not establish connection. Receiving end does not exist."}; - } else if (error.result === MessageChannel.RESULT_DISCONNECTED) { - error = null; - } - port.disconnectByOtherEnd(error); - }); - - return port.api(); - } - - connect(messageManager, name, recipient) { - let portId = getUniqueId(); - - let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient); - - return this._connect(messageManager, port, recipient); - } - - connectNative(messageManager, name, recipient) { - let portId = getUniqueId(); - - let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient); - - return this._connect(messageManager, port, recipient); - } - - _onConnect(name, filter) { - 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 && - filter(sender, recipient)); - }, - - receiveMessage: ({target, data: message, sender}) => { - let {name, portId} = message; - let mm = getMessageManager(target); - let recipient = Object.assign({}, sender); - if (recipient.tab) { - recipient.tabId = recipient.tab.id; - 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(); - } - - onConnect(name) { - return this._onConnect(name, sender => sender.id === this.sender.id); - } - - onConnectExternal(name) { - return this._onConnect(name, sender => sender.id !== this.sender.id); - } -} - -var apiManager = new class extends SchemaAPIManager { - constructor() { - super("addon"); - this.initialized = false; - } - - generateAPIs(...args) { - if (!this.initialized) { - this.initialized = true; - for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_ADDON)) { - this.loadScript(value); - } - } - return super.generateAPIs(...args); - } - - registerSchemaAPI(namespace, envType, getAPI) { - if (envType == "addon_child") { - super.registerSchemaAPI(namespace, envType, getAPI); - } - } -}(); - -/** - * 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 map = this.childApiManager.listeners.get(this.path); - - if (map.listeners.has(listener)) { - // TODO: Called with different args? - return; - } - - let id = getUniqueId(); - - map.ids.set(id, listener); - map.listeners.set(listener, id); - - this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", { - childId: this.childApiManager.id, - listenerId: id, - path: this.path, - args, - }); - } - - removeListener(listener) { - let map = this.childApiManager.listeners.get(this.path); - - if (!map.listeners.has(listener)) { - return; - } - - let id = map.listeners.get(listener); - map.listeners.delete(listener); - map.ids.delete(id); - - this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", { - childId: this.childApiManager.id, - listenerId: id, - path: this.path, - }); - } - - hasListener(listener) { - let map = this.childApiManager.listeners.get(this.path); - return map.listeners.has(listener); - } -} - -// 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 ExtensionParent.jsm. It -// handles asynchronous function calls as well as event listeners. -class ChildAPIManager { - constructor(context, messageManager, localApis, contextData) { - this.context = context; - this.messageManager = messageManager; - this.url = contextData.url; - - // 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; - - this.id = `${context.extension.id}.${context.contextId}`; - - MessageChannel.addListener(messageManager, "API:RunListener", this); - messageManager.addMessageListener("API:CallResult", this); - - this.messageFilterStrict = {childId: this.id}; - - this.listeners = new DefaultMap(() => ({ - ids: new Map(), - listeners: new Map(), - })); - - // Map[callId -> Deferred] - this.callPromises = new Map(); - - let params = { - childId: this.id, - extensionId: context.extension.id, - principal: context.principal, - }; - Object.assign(params, contextData); - - this.messageManager.sendAsyncMessage("API:CreateProxyContext", params); - } - - receiveMessage({name, messageName, data}) { - if (data.childId != this.id) { - return; - } - - switch (name || messageName) { - case "API:RunListener": - let map = this.listeners.get(data.path); - let listener = map.ids.get(data.listenerId); - - if (listener) { - return this.context.runSafe(listener, ...data.args); - } - - Cu.reportError(`Unknown listener at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n`); - 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 = getUniqueId(); - 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) { - path = path.split("."); - - let name = path.pop(); - let namespace = path.join("."); - - 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 obj = namespace.split(".").reduce( - (object, prop) => object && object[prop], - this.localApis); - - if (obj && name in obj) { - return new LocalAPIImplementation(obj, 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); - } -} - -class ExtensionPageContextChild extends BaseContext { - /** - * This ExtensionPageContextChild represents a privileged addon - * execution environment that has full access to the WebExtensions - * APIs (provided that the correct permissions have been requested). - * - * This is the child side of the ExtensionPageContextParent class - * defined in ExtensionParent.jsm. - * - * @param {BrowserExtensionContent} extension This context's owner. - * @param {object} params - * @param {nsIDOMWindow} params.contentWindow The window where the addon runs. - * @param {string} params.viewType One of "background", "popup" or "tab". - * "background" and "tab" are used by `browser.extension.getViews`. - * "popup" is only used internally to identify page action and browser - * action popups and options_ui pages. - * @param {number} [params.tabId] This tab's ID, used if viewType is "tab". - */ - constructor(extension, params) { - super("addon_child", extension); - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - // This check is temporary. It should be removed once the proxy creation - // is asynchronous. - throw new Error("ExtensionPageContextChild cannot be created in child processes"); - } - - let {viewType, uri, contentWindow, tabId} = params; - this.viewType = viewType; - this.uri = uri || extension.baseURI; - - this.setContentWindow(contentWindow); - - // This is the MessageSender property passed to extension. - // It can be augmented by the "page-open" hook. - let sender = {id: extension.id}; - if (viewType == "tab") { - sender.tabId = tabId; - this.tabId = tabId; - } - if (uri) { - sender.url = uri.spec; - } - this.sender = sender; - - Schemas.exportLazyGetter(contentWindow, "browser", () => { - let browserObj = Cu.createObjectIn(contentWindow); - Schemas.inject(browserObj, this.childManager); - return browserObj; - }); - - Schemas.exportLazyGetter(contentWindow, "chrome", () => { - let chromeApiWrapper = Object.create(this.childManager); - chromeApiWrapper.isChromeCompat = true; - - let chromeObj = Cu.createObjectIn(contentWindow); - Schemas.inject(chromeObj, chromeApiWrapper); - return chromeObj; - }); - - this.extension.views.add(this); - } - - get cloneScope() { - return this.contentWindow; - } - - get principal() { - return this.contentWindow.document.nodePrincipal; - } - - get windowId() { - if (this.viewType == "tab" || this.viewType == "popup") { - let globalView = ExtensionChild.contentGlobals.get(this.messageManager); - return globalView ? globalView.windowId : -1; - } - } - - // Called when the extension shuts down. - shutdown() { - this.unload(); - } - - // This method is called when an extension page navigates away or - // its tab is closed. - unload() { - // Note that without this guard, we end up running unload code - // multiple times for tab pages closed by the "page-unload" handlers - // triggered below. - if (this.unloaded) { - return; - } - - if (this.contentWindow) { - this.contentWindow.close(); - } - - super.unload(); - this.extension.views.delete(this); - } -} - -defineLazyGetter(ExtensionPageContextChild.prototype, "messenger", function() { - let filter = {extensionId: this.extension.id}; - let optionalFilter = {}; - // Addon-generated messages (not necessarily from the same process as the - // addon itself) are sent to the main process, which forwards them via the - // parent process message manager. Specific replies can be sent to the frame - // message manager. - return new Messenger(this, [Services.cpmm, this.messageManager], this.sender, - filter, optionalFilter); -}); - -defineLazyGetter(ExtensionPageContextChild.prototype, "childManager", function() { - let localApis = {}; - apiManager.generateAPIs(this, localApis); - - if (this.viewType == "background") { - apiManager.global.initializeBackgroundPage(this.contentWindow); - } - - let childManager = new ChildAPIManager(this, this.messageManager, localApis, { - envType: "addon_parent", - viewType: this.viewType, - url: this.uri.spec, - incognito: this.incognito, - }); - - this.callOnClose(childManager); - - return childManager; -}); - -// All subframes in a tab, background page, popup, etc. have the same view type. -// This class keeps track of such global state. -// Note that this is created even for non-extension tabs because at present we -// do not have a way to distinguish regular tabs from extension tabs at the -// initialization of a frame script. -class ContentGlobal { - /** - * @param {nsIContentFrameMessageManager} global The frame script's global. - */ - constructor(global) { - this.global = global; - // Unless specified otherwise assume that the extension page is in a tab, - // because the majority of all class instances are going to be a tab. Any - // special views (background page, extension popup) will immediately send an - // Extension:InitExtensionView message to change the viewType. - this.viewType = "tab"; - this.tabId = -1; - this.windowId = -1; - this.initialized = false; - this.global.addMessageListener("Extension:InitExtensionView", this); - this.global.addMessageListener("Extension:SetTabAndWindowId", this); - - this.initialDocuments = new WeakSet(); - } - - uninit() { - this.global.removeMessageListener("Extension:InitExtensionView", this); - this.global.removeMessageListener("Extension:SetTabAndWindowId", this); - this.global.removeEventListener("DOMContentLoaded", this); - } - - ensureInitialized() { - if (!this.initialized) { - // Request tab and window ID in case "Extension:InitExtensionView" is not - // sent (e.g. when `viewType` is "tab"). - let reply = this.global.sendSyncMessage("Extension:GetTabAndWindowId"); - this.handleSetTabAndWindowId(reply[0] || {}); - } - return this; - } - - receiveMessage({name, data}) { - switch (name) { - case "Extension:InitExtensionView": - // The view type is initialized once and then fixed. - this.global.removeMessageListener("Extension:InitExtensionView", this); - let {viewType, url} = data; - this.viewType = viewType; - this.global.addEventListener("DOMContentLoaded", this); - if (url) { - // TODO(robwu): Remove this check. It is only here because the popup - // implementation does not always load a URL at the initialization, - // and the logic is too complex to fix at once. - let {document} = this.global.content; - this.initialDocuments.add(document); - document.location.replace(url); - } - /* Falls through to allow these properties to be initialized at once */ - case "Extension:SetTabAndWindowId": - this.handleSetTabAndWindowId(data); - break; - } - } - - handleSetTabAndWindowId(data) { - let {tabId, windowId} = data; - if (tabId) { - // Tab IDs are not expected to change. - if (this.tabId !== -1 && tabId !== this.tabId) { - throw new Error("Attempted to change a tabId after it was set"); - } - this.tabId = tabId; - } - if (windowId !== undefined) { - // Window IDs may change if a tab is moved to a different location. - // Note: This is the ID of the browser window for the extension API. - // Do not confuse it with the innerWindowID of DOMWindows! - this.windowId = windowId; - } - this.initialized = true; - } - - // "DOMContentLoaded" event. - handleEvent(event) { - let {document} = this.global.content; - if (event.target === document) { - // If the document was still being loaded at the time of navigation, then - // the DOMContentLoaded event is fired for the old document. Ignore it. - if (this.initialDocuments.has(document)) { - this.initialDocuments.delete(document); - return; - } - this.global.removeEventListener("DOMContentLoaded", this); - this.global.sendAsyncMessage("Extension:ExtensionViewLoaded"); - } - } -} - -ExtensionChild = { - // Map<nsIContentFrameMessageManager, ContentGlobal> - contentGlobals: new Map(), - - // Map<innerWindowId, ExtensionPageContextChild> - extensionContexts: new Map(), - - initOnce() { - // This initializes the default message handler for messages targeted at - // an addon process, in case the addon process receives a message before - // its Messenger has been instantiated. For example, if a content script - // sends a message while there is no background page. - MessageChannel.setupMessageManagers([Services.cpmm]); - }, - - init(global) { - this.contentGlobals.set(global, new ContentGlobal(global)); - }, - - uninit(global) { - this.contentGlobals.get(global).uninit(); - this.contentGlobals.delete(global); - }, - - /** - * Create a privileged context at document-element-inserted. - * - * @param {BrowserExtensionContent} extension - * The extension for which the context should be created. - * @param {nsIDOMWindow} contentWindow The global of the page. - */ - createExtensionContext(extension, contentWindow) { - let windowId = getInnerWindowID(contentWindow); - let context = this.extensionContexts.get(windowId); - if (context) { - if (context.extension !== extension) { - // Oops. This should never happen. - Cu.reportError("A different extension context already exists in this frame!"); - } else { - // This should not happen either. - Cu.reportError("The extension context was already initialized in this frame."); - } - return; - } - - let mm = contentWindow - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - let {viewType, tabId} = this.contentGlobals.get(mm).ensureInitialized(); - - let uri = contentWindow.document.documentURIObject; - - context = new ExtensionPageContextChild(extension, {viewType, contentWindow, uri, tabId}); - this.extensionContexts.set(windowId, context); - }, - - /** - * Close the ExtensionPageContextChild belonging to the given window, if any. - * - * @param {number} windowId The inner window ID of the destroyed context. - */ - destroyExtensionContext(windowId) { - let context = this.extensionContexts.get(windowId); - if (context) { - context.unload(); - this.extensionContexts.delete(windowId); - } - }, - - shutdownExtension(extensionId) { - for (let [windowId, context] of this.extensionContexts) { - if (context.extension.id == extensionId) { - context.shutdown(); - this.extensionContexts.delete(windowId); - } - } - }, -}; - -// TODO(robwu): Change this condition when addons move to a separate process. -if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - Object.keys(ExtensionChild).forEach(function(key) { - if (typeof ExtensionChild[key] == "function") { - // :/ - ExtensionChild[key] = () => {}; - } - }); -} - -Object.assign(ExtensionChild, { - ChildAPIManager, - Messenger, - Port, -}); - |