summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionChild.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/ExtensionChild.jsm')
-rw-r--r--toolkit/components/webextensions/ExtensionChild.jsm1040
1 files changed, 1040 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/ExtensionChild.jsm b/toolkit/components/webextensions/ExtensionChild.jsm
new file mode 100644
index 000000000..c953dd685
--- /dev/null
+++ b/toolkit/components/webextensions/ExtensionChild.jsm
@@ -0,0 +1,1040 @@
+/* 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) {
+ 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();
+ }
+
+ _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) {
+ 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 {
+ 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.uuid};
+ 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,
+});
+