summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionCommon.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/ExtensionCommon.jsm')
-rw-r--r--toolkit/components/webextensions/ExtensionCommon.jsm679
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,
-};