/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /** * This module contains utilities and base classes for logic which is * common between the parent and child process, and in particular * between ExtensionParent.jsm and ExtensionChild.jsm. */ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /* exported ExtensionCommon */ this.EXPORTED_SYMBOLS = ["ExtensionCommon"]; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", "resource://gre/modules/MessageChannel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Schemas", "resource://gre/modules/Schemas.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { EventEmitter, ExtensionError, SpreadArgs, getConsole, getInnerWindowID, getUniqueId, runSafeSync, runSafeSyncWithoutClone, instanceOf, } = ExtensionUtils; XPCOMUtils.defineLazyGetter(this, "console", getConsole); class BaseContext { constructor(envType, extension) { this.envType = envType; this.onClose = new Set(); this.checkedLastError = false; this._lastError = null; this.contextId = getUniqueId(); this.unloaded = false; this.extension = extension; this.jsonSandbox = null; this.active = true; this.incognito = null; this.messageManager = null; this.docShell = null; this.contentWindow = null; this.innerWindowID = 0; } setContentWindow(contentWindow) { let {document} = contentWindow; let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell); this.innerWindowID = getInnerWindowID(contentWindow); this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIContentFrameMessageManager); if (this.incognito == null) { this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow); } MessageChannel.setupMessageManagers([this.messageManager]); let onPageShow = event => { if (!event || event.target === document) { this.docShell = docShell; this.contentWindow = contentWindow; this.active = true; } }; let onPageHide = event => { if (!event || event.target === document) { // Put this off until the next tick. Promise.resolve().then(() => { this.docShell = null; this.contentWindow = null; this.active = false; }); } }; onPageShow(); contentWindow.addEventListener("pagehide", onPageHide, true); contentWindow.addEventListener("pageshow", onPageShow, true); this.callOnClose({ close: () => { onPageHide(); if (this.active) { contentWindow.removeEventListener("pagehide", onPageHide, true); contentWindow.removeEventListener("pageshow", onPageShow, true); } }, }); } get cloneScope() { throw new Error("Not implemented"); } get principal() { throw new Error("Not implemented"); } runSafe(...args) { if (this.unloaded) { Cu.reportError("context.runSafe called after context unloaded"); } else if (!this.active) { Cu.reportError("context.runSafe called while context is inactive"); } else { return runSafeSync(this, ...args); } } runSafeWithoutClone(...args) { if (this.unloaded) { Cu.reportError("context.runSafeWithoutClone called after context unloaded"); } else if (!this.active) { Cu.reportError("context.runSafeWithoutClone called while context is inactive"); } else { return runSafeSyncWithoutClone(...args); } } checkLoadURL(url, options = {}) { let ssm = Services.scriptSecurityManager; let flags = ssm.STANDARD; if (!options.allowScript) { flags |= ssm.DISALLOW_SCRIPT; } if (!options.allowInheritsPrincipal) { flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; } if (options.dontReportErrors) { flags |= ssm.DONT_REPORT_ERRORS; } try { ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags); } catch (e) { return false; } return true; } /** * Safely call JSON.stringify() on an object that comes from an * extension. * * @param {array} args Arguments for JSON.stringify() * @returns {string} The stringified representation of obj */ jsonStringify(...args) { if (!this.jsonSandbox) { this.jsonSandbox = Cu.Sandbox(this.principal, { sameZoneAs: this.cloneScope, wantXrays: false, }); } return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args); } callOnClose(obj) { this.onClose.add(obj); } forgetOnClose(obj) { this.onClose.delete(obj); } /** * A wrapper around MessageChannel.sendMessage which adds the extension ID * to the recipient object, and ensures replies are not processed after the * context has been unloaded. * * @param {nsIMessageManager} target * @param {string} messageName * @param {object} data * @param {object} [options] * @param {object} [options.sender] * @param {object} [options.recipient] * * @returns {Promise} */ sendMessage(target, messageName, data, options = {}) { options.recipient = Object.assign({extensionId: this.extension.id}, options.recipient); options.sender = options.sender || {}; options.sender.extensionId = this.extension.id; options.sender.contextId = this.contextId; return MessageChannel.sendMessage(target, messageName, data, options); } get lastError() { this.checkedLastError = true; return this._lastError; } set lastError(val) { this.checkedLastError = false; this._lastError = val; } /** * Normalizes the given error object for use by the target scope. If * the target is an error object which belongs to that scope, it is * returned as-is. If it is an ordinary object with a `message` * property, it is converted into an error belonging to the target * scope. If it is an Error object which does *not* belong to the * clone scope, it is reported, and converted to an unexpected * exception error. * * @param {Error|object} error * @returns {Error} */ normalizeError(error) { if (error instanceof this.cloneScope.Error) { return error; } let message; if (instanceOf(error, "Object") || error instanceof ExtensionError) { message = error.message; } else if (typeof error == "object" && this.principal.subsumes(Cu.getObjectPrincipal(error))) { message = error.message; } else { Cu.reportError(error); } message = message || "An unexpected error occurred"; return new this.cloneScope.Error(message); } /** * Sets the value of `.lastError` to `error`, calls the given * callback, and reports an error if the value has not been checked * when the callback returns. * * @param {object} error An object with a `message` property. May * optionally be an `Error` object belonging to the target scope. * @param {function} callback The callback to call. * @returns {*} The return value of callback. */ withLastError(error, callback) { this.lastError = this.normalizeError(error); try { return callback(); } finally { if (!this.checkedLastError) { Cu.reportError(`Unchecked lastError value: ${this.lastError}`); } this.lastError = null; } } /** * Wraps the given promise so it can be safely returned to extension * code in this context. * * If `callback` is provided, however, it is used as a completion * function for the promise, and no promise is returned. In this case, * the callback is called when the promise resolves or rejects. In the * latter case, `lastError` is set to the rejection value, and the * callback function must check `browser.runtime.lastError` or * `extension.runtime.lastError` in order to prevent it being reported * to the console. * * @param {Promise} promise The promise with which to wrap the * callback. May resolve to a `SpreadArgs` instance, in which case * each element will be used as a separate argument. * * Unless the promise object belongs to the cloneScope global, its * resolution value is cloned into cloneScope prior to calling the * `callback` function or resolving the wrapped promise. * * @param {function} [callback] The callback function to wrap * * @returns {Promise|undefined} If callback is null, a promise object * belonging to the target scope. Otherwise, undefined. */ wrapPromise(promise, callback = null) { let runSafe = this.runSafe.bind(this); if (promise instanceof this.cloneScope.Promise) { runSafe = this.runSafeWithoutClone.bind(this); } if (callback) { promise.then( args => { if (this.unloaded) { dump(`Promise resolved after context unloaded\n`); } else if (!this.active) { dump(`Promise resolved while context is inactive\n`); } else if (args instanceof SpreadArgs) { runSafe(callback, ...args); } else { runSafe(callback, args); } }, error => { this.withLastError(error, () => { if (this.unloaded) { dump(`Promise rejected after context unloaded\n`); } else if (!this.active) { dump(`Promise rejected while context is inactive\n`); } else { this.runSafeWithoutClone(callback); } }); }); } else { return new this.cloneScope.Promise((resolve, reject) => { promise.then( value => { if (this.unloaded) { dump(`Promise resolved after context unloaded\n`); } else if (!this.active) { dump(`Promise resolved while context is inactive\n`); } else if (value instanceof SpreadArgs) { runSafe(resolve, value.length == 1 ? value[0] : value); } else { runSafe(resolve, value); } }, value => { if (this.unloaded) { dump(`Promise rejected after context unloaded: ${value && value.message}\n`); } else if (!this.active) { dump(`Promise rejected while context is inactive: ${value && value.message}\n`); } else { this.runSafeWithoutClone(reject, this.normalizeError(value)); } }); }); } } unload() { this.unloaded = true; MessageChannel.abortResponses({ extensionId: this.extension.id, contextId: this.contextId, }); for (let obj of this.onClose) { obj.close(); } } /** * A simple proxy for unload(), for use with callOnClose(). */ close() { this.unload(); } } /** * An object that runs the implementation of a schema API. Instantiations of * this interfaces are used by Schemas.jsm. * * @interface */ class SchemaAPIInterface { /** * Calls this as a function that returns its return value. * * @abstract * @param {Array} args The parameters for the function. * @returns {*} The return value of the invoked function. */ callFunction(args) { throw new Error("Not implemented"); } /** * Calls this as a function and ignores its return value. * * @abstract * @param {Array} args The parameters for the function. */ callFunctionNoReturn(args) { throw new Error("Not implemented"); } /** * Calls this as a function that completes asynchronously. * * @abstract * @param {Array} args The parameters for the function. * @param {function(*)} [callback] The callback to be called when the function * completes. * @returns {Promise|undefined} Must be void if `callback` is set, and a * promise otherwise. The promise is resolved when the function completes. */ callAsyncFunction(args, callback) { throw new Error("Not implemented"); } /** * Retrieves the value of this as a property. * * @abstract * @returns {*} The value of the property. */ getProperty() { throw new Error("Not implemented"); } /** * Assigns the value to this as property. * * @abstract * @param {string} value The new value of the property. */ setProperty(value) { throw new Error("Not implemented"); } /** * Registers a `listener` to this as an event. * * @abstract * @param {function} listener The callback to be called when the event fires. * @param {Array} args Extra parameters for EventManager.addListener. * @see EventManager.addListener */ addListener(listener, args) { throw new Error("Not implemented"); } /** * Checks whether `listener` is listening to this as an event. * * @abstract * @param {function} listener The event listener. * @returns {boolean} Whether `listener` is registered with this as an event. * @see EventManager.hasListener */ hasListener(listener) { throw new Error("Not implemented"); } /** * Unregisters `listener` from this as an event. * * @abstract * @param {function} listener The event listener. * @see EventManager.removeListener */ removeListener(listener) { throw new Error("Not implemented"); } } /** * An object that runs a locally implemented API. */ class LocalAPIImplementation extends SchemaAPIInterface { /** * Constructs an implementation of the `name` method or property of `pathObj`. * * @param {object} pathObj The object containing the member with name `name`. * @param {string} name The name of the implemented member. * @param {BaseContext} context The context in which the schema is injected. */ constructor(pathObj, name, context) { super(); this.pathObj = pathObj; this.name = name; this.context = context; } callFunction(args) { return this.pathObj[this.name](...args); } callFunctionNoReturn(args) { this.pathObj[this.name](...args); } callAsyncFunction(args, callback) { let promise; try { promise = this.pathObj[this.name](...args) || Promise.resolve(); } catch (e) { promise = Promise.reject(e); } return this.context.wrapPromise(promise, callback); } getProperty() { return this.pathObj[this.name]; } setProperty(value) { this.pathObj[this.name] = value; } addListener(listener, args) { try { this.pathObj[this.name].addListener.call(null, listener, ...args); } catch (e) { throw this.context.normalizeError(e); } } hasListener(listener) { return this.pathObj[this.name].hasListener.call(null, listener); } removeListener(listener) { this.pathObj[this.name].removeListener.call(null, listener); } } /** * This object loads the ext-*.js scripts that define the extension API. * * This class instance is shared with the scripts that it loads, so that the * ext-*.js scripts and the instantiator can communicate with each other. */ class SchemaAPIManager extends EventEmitter { /** * @param {string} processType * "main" - The main, one and only chrome browser process. * "addon" - An addon process. * "content" - A content process. */ constructor(processType) { super(); this.processType = processType; this.global = this._createExtGlobal(); this._scriptScopes = []; this._schemaApis = { addon_parent: [], addon_child: [], content_parent: [], content_child: [], }; } /** * Create a global object that is used as the shared global for all ext-*.js * scripts that are loaded via `loadScript`. * * @returns {object} A sandbox that is used as the global by `loadScript`. */ _createExtGlobal() { let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { wantXrays: false, sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`, }); Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this}); XPCOMUtils.defineLazyGetter(global, "console", getConsole); XPCOMUtils.defineLazyModuleGetter(global, "require", "resource://devtools/shared/Loader.jsm"); return global; } /** * Load an ext-*.js script. The script runs in its own scope, if it wishes to * share state with another script it can assign to the `global` variable. If * it wishes to communicate with this API manager, use `extensions`. * * @param {string} scriptUrl The URL of the ext-*.js script. */ loadScript(scriptUrl) { // Create the object in the context of the sandbox so that the script runs // in the sandbox's context instead of here. let scope = Cu.createObjectIn(this.global); Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8"); // Save the scope to avoid it being garbage collected. this._scriptScopes.push(scope); } /** * Called by an ext-*.js script to register an API. * * @param {string} namespace The API namespace. * Intended to match the namespace of the generated API, but not used at * the moment - see bugzil.la/1295774. * @param {string} envType Restricts the API to contexts that run in the * given environment. Must be one of the following: * - "addon_parent" - addon APIs that runs in the main process. * - "addon_child" - addon APIs that runs in an addon process. * - "content_parent" - content script APIs that runs in the main process. * - "content_child" - content script APIs that runs in a content process. * @param {function(BaseContext)} getAPI A function that returns an object * that will be merged with |chrome| and |browser|. The next example adds * the create, update and remove methods to the tabs API. * * registerSchemaAPI("tabs", "addon_parent", (context) => ({ * tabs: { create, update }, * })); * registerSchemaAPI("tabs", "addon_parent", (context) => ({ * tabs: { remove }, * })); */ registerSchemaAPI(namespace, envType, getAPI) { this._schemaApis[envType].push({namespace, getAPI}); } /** * Exports all registered scripts to `obj`. * * @param {BaseContext} context The context for which the API bindings are * generated. * @param {object} obj The destination of the API. */ generateAPIs(context, obj) { let apis = this._schemaApis[context.envType]; if (!apis) { Cu.reportError(`No APIs have been registered for ${context.envType}`); return; } SchemaAPIManager.generateAPIs(context, apis, obj); } /** * Mash together all the APIs from `apis` into `obj`. * * @param {BaseContext} context The context for which the API bindings are * generated. * @param {Array} apis A list of objects, see `registerSchemaAPI`. * @param {object} obj The destination of the API. */ static generateAPIs(context, apis, obj) { // Recursively copy properties from source to dest. function copy(dest, source) { for (let prop in source) { let desc = Object.getOwnPropertyDescriptor(source, prop); if (typeof(desc.value) == "object") { if (!(prop in dest)) { dest[prop] = {}; } copy(dest[prop], source[prop]); } else { Object.defineProperty(dest, prop, desc); } } } for (let api of apis) { if (Schemas.checkPermissions(api.namespace, context.extension)) { api = api.getAPI(context); copy(obj, api); } } } } const ExtensionCommon = { BaseContext, LocalAPIImplementation, SchemaAPIInterface, SchemaAPIManager, };