diff options
Diffstat (limited to 'toolkit/components/webextensions/ExtensionCommon.jsm')
-rw-r--r-- | toolkit/components/webextensions/ExtensionCommon.jsm | 679 |
1 files changed, 0 insertions, 679 deletions
diff --git a/toolkit/components/webextensions/ExtensionCommon.jsm b/toolkit/components/webextensions/ExtensionCommon.jsm deleted file mode 100644 index 9ec84b5c7..000000000 --- a/toolkit/components/webextensions/ExtensionCommon.jsm +++ /dev/null @@ -1,679 +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 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<any>} 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, -}; |