forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			3080 lines
		
	
	
	
		
			92 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			3080 lines
		
	
	
	
		
			92 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| /* 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/. */
 | |
| 
 | |
| /**
 | |
|  * 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.
 | |
|  */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   ConsoleAPI: "resource://gre/modules/Console.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
|   SchemaRoot: "resource://gre/modules/Schemas.sys.mjs",
 | |
|   Schemas: "resource://gre/modules/Schemas.sys.mjs",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   lazy,
 | |
|   "styleSheetService",
 | |
|   "@mozilla.org/content/style-sheet-service;1",
 | |
|   "nsIStyleSheetService"
 | |
| );
 | |
| 
 | |
| const ScriptError = Components.Constructor(
 | |
|   "@mozilla.org/scripterror;1",
 | |
|   "nsIScriptError",
 | |
|   "initWithWindowID"
 | |
| );
 | |
| 
 | |
| import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | |
| 
 | |
| var {
 | |
|   DefaultMap,
 | |
|   DefaultWeakMap,
 | |
|   ExtensionError,
 | |
|   filterStack,
 | |
|   getInnerWindowID,
 | |
|   getUniqueId,
 | |
| } = ExtensionUtils;
 | |
| 
 | |
| function getConsole() {
 | |
|   return new lazy.ConsoleAPI({
 | |
|     maxLogLevelPref: "extensions.webextensions.log.level",
 | |
|     prefix: "WebExtensions",
 | |
|   });
 | |
| }
 | |
| 
 | |
| // Run a function and report exceptions.
 | |
| function runSafeSyncWithoutClone(f, ...args) {
 | |
|   try {
 | |
|     return f(...args);
 | |
|   } catch (e) {
 | |
|     // This method is called with `this` unbound and it doesn't have
 | |
|     // access to a BaseContext instance and so we can't check if `e`
 | |
|     // is an instance of the extension context's Error constructor
 | |
|     // (like we do in BaseContext applySafeWithoutClone method).
 | |
|     dump(
 | |
|       `Extension error: ${e} ${e?.fileName} ${
 | |
|         e?.lineNumber
 | |
|       }\n[[Exception stack\n${
 | |
|         e?.stack ? filterStack(e) : undefined
 | |
|       }Current stack\n${filterStack(Error())}]]\n`
 | |
|     );
 | |
|     Cu.reportError(e);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Return true if the given value is an instance of the given
 | |
| // native type.
 | |
| function instanceOf(value, type) {
 | |
|   return (
 | |
|     value &&
 | |
|     typeof value === "object" &&
 | |
|     ChromeUtils.getClassName(value) === type
 | |
|   );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Convert any of several different representations of a date/time to a Date object.
 | |
|  * Accepts several formats:
 | |
|  * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
 | |
|  * either a number or a string.
 | |
|  *
 | |
|  * @param {Date|string|number} date
 | |
|  *      The date to convert.
 | |
|  * @returns {Date}
 | |
|  *      A Date object
 | |
|  */
 | |
| function normalizeTime(date) {
 | |
|   // Of all the formats we accept the "number of milliseconds since the epoch as a string"
 | |
|   // is an outlier, everything else can just be passed directly to the Date constructor.
 | |
|   return new Date(
 | |
|     typeof date == "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date
 | |
|   );
 | |
| }
 | |
| 
 | |
| function withHandlingUserInput(window, callable) {
 | |
|   let handle = window.windowUtils.setHandlingUserInput(true);
 | |
|   try {
 | |
|     return callable();
 | |
|   } finally {
 | |
|     handle.destruct();
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Defines a lazy getter for the given property on the given object. The
 | |
|  * first time the property is accessed, the return value of the getter
 | |
|  * is defined on the current `this` object with the given property name.
 | |
|  * Importantly, this means that a lazy getter defined on an object
 | |
|  * prototype will be invoked separately for each object instance that
 | |
|  * it's accessed on.
 | |
|  *
 | |
|  * Note: for better type inference, prefer redefineGetter() below.
 | |
|  *
 | |
|  * @param {object} object
 | |
|  *        The prototype object on which to define the getter.
 | |
|  * @param {string | symbol} prop
 | |
|  *        The property name for which to define the getter.
 | |
|  * @param {callback} getter
 | |
|  *        The function to call in order to generate the final property
 | |
|  *        value.
 | |
|  */
 | |
| function defineLazyGetter(object, prop, getter) {
 | |
|   Object.defineProperty(object, prop, {
 | |
|     enumerable: true,
 | |
|     configurable: true,
 | |
|     get() {
 | |
|       return redefineGetter(this, prop, getter.call(this), true);
 | |
|     },
 | |
|     set(value) {
 | |
|       redefineGetter(this, prop, value, true);
 | |
|     },
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * A more type-inference friendly version of defineLazyGetter() above.
 | |
|  * Call it from a real getter (and setter) for your class or object.
 | |
|  * On first run, it will redefine the property with the final value.
 | |
|  *
 | |
|  * @template Value
 | |
|  * @param {object} object
 | |
|  * @param {string | symbol} key
 | |
|  * @param {Value} value
 | |
|  * @returns {Value}
 | |
|  */
 | |
| function redefineGetter(object, key, value, writable = false) {
 | |
|   Object.defineProperty(object, key, {
 | |
|     enumerable: true,
 | |
|     configurable: true,
 | |
|     writable,
 | |
|     value,
 | |
|   });
 | |
|   return value;
 | |
| }
 | |
| 
 | |
| function checkLoadURI(uri, principal, 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.checkLoadURIWithPrincipal(principal, uri, flags);
 | |
|   } catch (e) {
 | |
|     return false;
 | |
|   }
 | |
|   return true;
 | |
| }
 | |
| 
 | |
| function checkLoadURL(url, principal, options) {
 | |
|   try {
 | |
|     return checkLoadURI(Services.io.newURI(url), principal, options);
 | |
|   } catch (e) {
 | |
|     return false; // newURI threw.
 | |
|   }
 | |
| }
 | |
| 
 | |
| function makeWidgetId(id) {
 | |
|   id = id.toLowerCase();
 | |
|   // FIXME: This allows for collisions.
 | |
|   return id.replace(/[^a-z0-9_-]/g, "_");
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * A sentinel class to indicate that an array of values should be
 | |
|  * treated as an array when used as a promise resolution value, but as a
 | |
|  * spread expression (...args) when passed to a callback.
 | |
|  */
 | |
| class SpreadArgs extends Array {
 | |
|   constructor(args) {
 | |
|     super();
 | |
|     this.push(...args);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Like SpreadArgs, but also indicates that the array values already
 | |
|  * belong to the target compartment, and should not be cloned before
 | |
|  * being passed.
 | |
|  *
 | |
|  * The `unwrappedValues` property contains an Array object which belongs
 | |
|  * to the target compartment, and contains the same unwrapped values
 | |
|  * passed the NoCloneSpreadArgs constructor.
 | |
|  */
 | |
| class NoCloneSpreadArgs {
 | |
|   constructor(args) {
 | |
|     this.unwrappedValues = args;
 | |
|   }
 | |
| 
 | |
|   [Symbol.iterator]() {
 | |
|     return this.unwrappedValues[Symbol.iterator]();
 | |
|   }
 | |
| }
 | |
| 
 | |
| const LISTENERS = Symbol("listeners");
 | |
| const ONCE_MAP = Symbol("onceMap");
 | |
| 
 | |
| class EventEmitter {
 | |
|   constructor() {
 | |
|     this[LISTENERS] = new Map();
 | |
|     this[ONCE_MAP] = new WeakMap();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Checks whether there is some listener for the given event.
 | |
|    *
 | |
|    * @param {string} event
 | |
|    *       The name of the event to listen for.
 | |
|    * @returns {boolean}
 | |
|    */
 | |
|   has(event) {
 | |
|     return this[LISTENERS].has(event);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Adds the given function as a listener for the given event.
 | |
|    *
 | |
|    * The listener function may optionally return a Promise which
 | |
|    * resolves when it has completed all operations which event
 | |
|    * dispatchers may need to block on.
 | |
|    *
 | |
|    * @param {string} event
 | |
|    *       The name of the event to listen for.
 | |
|    * @param {function(string, ...any): any} listener
 | |
|    *        The listener to call when events are emitted.
 | |
|    */
 | |
|   on(event, listener) {
 | |
|     let listeners = this[LISTENERS].get(event);
 | |
|     if (!listeners) {
 | |
|       listeners = new Set();
 | |
|       this[LISTENERS].set(event, listeners);
 | |
|     }
 | |
| 
 | |
|     listeners.add(listener);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Removes the given function as a listener for the given event.
 | |
|    *
 | |
|    * @param {string} event
 | |
|    *       The name of the event to stop listening for.
 | |
|    * @param {function(string, ...any): any} listener
 | |
|    *        The listener function to remove.
 | |
|    */
 | |
|   off(event, listener) {
 | |
|     let set = this[LISTENERS].get(event);
 | |
|     if (set) {
 | |
|       set.delete(listener);
 | |
|       set.delete(this[ONCE_MAP].get(listener));
 | |
|       if (!set.size) {
 | |
|         this[LISTENERS].delete(event);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Adds the given function as a listener for the given event once.
 | |
|    *
 | |
|    * @param {string} event
 | |
|    *       The name of the event to listen for.
 | |
|    * @param {function(string, ...any): any} listener
 | |
|    *        The listener to call when events are emitted.
 | |
|    */
 | |
|   once(event, listener) {
 | |
|     let wrapper = (event, ...args) => {
 | |
|       this.off(event, wrapper);
 | |
|       this[ONCE_MAP].delete(listener);
 | |
| 
 | |
|       return listener(event, ...args);
 | |
|     };
 | |
|     this[ONCE_MAP].set(listener, wrapper);
 | |
| 
 | |
|     this.on(event, wrapper);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Triggers all listeners for the given event. If any listeners return
 | |
|    * a value, returns a promise which resolves when all returned
 | |
|    * promises have resolved. Otherwise, returns undefined.
 | |
|    *
 | |
|    * @param {string} event
 | |
|    *       The name of the event to emit.
 | |
|    * @param {any} args
 | |
|    *        Arbitrary arguments to pass to the listener functions, after
 | |
|    *        the event name.
 | |
|    * @returns {Promise?}
 | |
|    */
 | |
|   emit(event, ...args) {
 | |
|     let listeners = this[LISTENERS].get(event);
 | |
| 
 | |
|     if (listeners) {
 | |
|       let promises = [];
 | |
| 
 | |
|       for (let listener of listeners) {
 | |
|         try {
 | |
|           let result = listener(event, ...args);
 | |
|           if (result !== undefined) {
 | |
|             promises.push(result);
 | |
|           }
 | |
|         } catch (e) {
 | |
|           Cu.reportError(e);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (promises.length) {
 | |
|         return Promise.all(promises);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Base class for WebExtension APIs.  Each API creates a new class
 | |
|  * that inherits from this class, the derived class is instantiated
 | |
|  * once for each extension that uses the API.
 | |
|  */
 | |
| class ExtensionAPI extends EventEmitter {
 | |
|   constructor(extension) {
 | |
|     super();
 | |
| 
 | |
|     this.extension = extension;
 | |
| 
 | |
|     extension.once("shutdown", (what, isAppShutdown) => {
 | |
|       if (this.onShutdown) {
 | |
|         this.onShutdown(isAppShutdown);
 | |
|       }
 | |
|       this.extension = null;
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   destroy() {}
 | |
| 
 | |
|   /** @param {string} entryName */
 | |
|   onManifestEntry(entryName) {}
 | |
| 
 | |
|   /** @param {boolean} isAppShutdown */
 | |
|   onShutdown(isAppShutdown) {}
 | |
| 
 | |
|   /** @param {BaseContext} context */
 | |
|   getAPI(context) {
 | |
|     throw new Error("Not Implemented");
 | |
|   }
 | |
| 
 | |
|   /** @param {string} id */
 | |
|   static onDisable(id) {}
 | |
| 
 | |
|   /** @param {string} id */
 | |
|   static onUninstall(id) {}
 | |
| 
 | |
|   /**
 | |
|    * @param {string} id
 | |
|    * @param {Record<string, JSONValue>} manifest
 | |
|    */
 | |
|   static onUpdate(id, manifest) {}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Subclass to add APIs commonly used with persistent events.
 | |
|  * If a namespace uses events, it should use this subclass.
 | |
|  *
 | |
|  * this.apiNamespace = class extends ExtensionAPIPersistent {};
 | |
|  */
 | |
| class ExtensionAPIPersistent extends ExtensionAPI {
 | |
|   /** @type {Record<string, callback>} */
 | |
|   PERSISTENT_EVENTS;
 | |
| 
 | |
|   /**
 | |
|    * Check for event entry.
 | |
|    *
 | |
|    * @param {string} event The event name e.g. onStateChanged
 | |
|    * @returns {boolean}
 | |
|    */
 | |
|   hasEventRegistrar(event) {
 | |
|     return (
 | |
|       this.PERSISTENT_EVENTS && Object.hasOwn(this.PERSISTENT_EVENTS, event)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the event registration fuction
 | |
|    *
 | |
|    * @param {string} event The event name e.g. onStateChanged
 | |
|    * @returns {Function} register is used to start the listener
 | |
|    *                     register returns an object containing
 | |
|    *                     a convert and unregister function.
 | |
|    */
 | |
|   getEventRegistrar(event) {
 | |
|     if (this.hasEventRegistrar(event)) {
 | |
|       return this.PERSISTENT_EVENTS[event].bind(this);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Used when instantiating an EventManager instance to register the listener.
 | |
|    *
 | |
|    * @param {object}      options         Options used for event registration
 | |
|    * @param {BaseContext} options.context Extension Context passed when creating an EventManager instance.
 | |
|    * @param {string}      options.event   The eAPI vent name.
 | |
|    * @param {Function}    options.fire    The function passed to the listener to fire the event.
 | |
|    * @param {Array<any>}  params          An optional array of parameters received along with the
 | |
|    *                                      addListener request.
 | |
|    * @returns {Function}                  The unregister function used in the EventManager.
 | |
|    */
 | |
|   registerEventListener(options, params) {
 | |
|     const apiRegistar = this.getEventRegistrar(options.event);
 | |
|     return apiRegistar?.(options, params).unregister;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Used to prime a listener for when the background script is not running.
 | |
|    *
 | |
|    * @param {string} event The event name e.g. onStateChanged or captiveURL.onChange.
 | |
|    * @param {Function} fire The function passed to the listener to fire the event.
 | |
|    * @param {Array} params Params passed to the event listener.
 | |
|    * @param {boolean} isInStartup unused here but passed for subclass use.
 | |
|    * @returns {object} the unregister and convert functions used in the EventManager.
 | |
|    */
 | |
|   primeListener(event, fire, params, isInStartup) {
 | |
|     const apiRegistar = this.getEventRegistrar(event);
 | |
|     return apiRegistar?.({ fire, isInStartup }, params);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This class contains the information we have about an individual
 | |
|  * extension.  It is never instantiated directly, instead subclasses
 | |
|  * for each type of process extend this class and add members that are
 | |
|  * relevant for that process.
 | |
|  *
 | |
|  * @abstract
 | |
|  */
 | |
| class BaseContext {
 | |
|   /** @type {boolean} */
 | |
|   isTopContext;
 | |
|   /** @type {string} */
 | |
|   viewType;
 | |
| 
 | |
|   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.manifestVersion = extension.manifestVersion;
 | |
|     this.jsonSandbox = null;
 | |
|     this.active = true;
 | |
|     this.incognito = null;
 | |
|     this.messageManager = null;
 | |
|     this.contentWindow = null;
 | |
|     this.innerWindowID = 0;
 | |
| 
 | |
|     // These two properties are assigned in ContentScriptContextChild subclass
 | |
|     // to keep a copy of the content script sandbox Error and Promise globals
 | |
|     // (which are used by the WebExtensions internals) before any extension
 | |
|     // content script code had any chance to redefine them.
 | |
|     this.cloneScopeError = null;
 | |
|     this.cloneScopePromise = null;
 | |
|   }
 | |
| 
 | |
|   get isProxyContextParent() {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   get Error() {
 | |
|     // Return the copy stored in the context instance (when the context is an instance of
 | |
|     // ContentScriptContextChild or the global from extension page window otherwise).
 | |
|     return this.cloneScopeError || this.cloneScope.Error;
 | |
|   }
 | |
| 
 | |
|   get Promise() {
 | |
|     // Return the copy stored in the context instance (when the context is an instance of
 | |
|     // ContentScriptContextChild or the global from extension page window otherwise).
 | |
|     return this.cloneScopePromise || this.cloneScope.Promise;
 | |
|   }
 | |
| 
 | |
|   get privateBrowsingAllowed() {
 | |
|     return this.extension.privateBrowsingAllowed;
 | |
|   }
 | |
| 
 | |
|   get isBackgroundContext() {
 | |
|     if (this.viewType === "background") {
 | |
|       if (this.isProxyContextParent) {
 | |
|         return !!this.isTopContext; // Set in ExtensionPageContextParent.
 | |
|       }
 | |
|       const { contentWindow } = this;
 | |
|       return !!contentWindow && contentWindow.top === contentWindow;
 | |
|     }
 | |
|     return this.viewType === "background_worker";
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Whether the extension context is using the WebIDL bindings for the
 | |
|    * WebExtensions APIs.
 | |
|    * To be overridden in subclasses (e.g. WorkerContextChild) and to be
 | |
|    * optionally used in ExtensionAPI classes to customize the behavior of the
 | |
|    * API when the calls to the extension API are originated from the WebIDL
 | |
|    * bindings.
 | |
|    */
 | |
|   get useWebIDLBindings() {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   canAccessWindow(window) {
 | |
|     return this.extension.canAccessWindow(window);
 | |
|   }
 | |
| 
 | |
|   canAccessContainer(userContextId) {
 | |
|     return this.extension.canAccessContainer(userContextId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Opens a conduit linked to this context, populating related address fields.
 | |
|    * Only available in child contexts with an associated contentWindow.
 | |
|    *
 | |
|    * @param {object} subject
 | |
|    * @param {ConduitAddress} address
 | |
|    * @returns {import("ConduitsChild.sys.mjs").PointConduit}
 | |
|    * @type {ConduitOpen}
 | |
|    */
 | |
|   openConduit(subject, address) {
 | |
|     let wgc = this.contentWindow.windowGlobalChild;
 | |
|     let conduit = wgc.getActor("Conduits").openConduit(subject, {
 | |
|       id: subject.id || getUniqueId(),
 | |
|       extensionId: this.extension.id,
 | |
|       envType: this.envType,
 | |
|       ...address,
 | |
|     });
 | |
|     this.callOnClose(conduit);
 | |
|     conduit.setCloseCallback(() => {
 | |
|       this.forgetOnClose(conduit);
 | |
|     });
 | |
|     return conduit;
 | |
|   }
 | |
| 
 | |
|   setContentWindow(contentWindow) {
 | |
|     if (!this.canAccessWindow(contentWindow)) {
 | |
|       throw new Error(
 | |
|         "BaseContext attempted to load when extension is not allowed due to incognito settings."
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.innerWindowID = getInnerWindowID(contentWindow);
 | |
|     this.messageManager = contentWindow.docShell.messageManager;
 | |
| 
 | |
|     if (this.incognito == null) {
 | |
|       this.incognito =
 | |
|         lazy.PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
 | |
|     }
 | |
| 
 | |
|     let wgc = contentWindow.windowGlobalChild;
 | |
|     Object.defineProperty(this, "active", {
 | |
|       configurable: true,
 | |
|       enumerable: true,
 | |
|       get: () => wgc.isCurrentGlobal && !wgc.windowContext.isInBFCache,
 | |
|     });
 | |
|     Object.defineProperty(this, "contentWindow", {
 | |
|       configurable: true,
 | |
|       enumerable: true,
 | |
|       get: () => (this.active ? wgc.browsingContext.window : null),
 | |
|     });
 | |
|     this.callOnClose({
 | |
|       close: () => {
 | |
|         // Allow other "close" handlers to use these properties, until the next tick.
 | |
|         Promise.resolve().then(() => {
 | |
|           Object.defineProperty(this, "contentWindow", { value: null });
 | |
|           Object.defineProperty(this, "active", { value: false });
 | |
|           wgc = null;
 | |
|         });
 | |
|       },
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // All child contexts must implement logActivity.  This is handled if the child
 | |
|   // context subclasses ExtensionBaseContextChild.  ProxyContextParent overrides
 | |
|   // this with a noop for parent contexts.
 | |
|   logActivity(type, name, data) {
 | |
|     throw new Error(`Not implemented for ${this.envType}`);
 | |
|   }
 | |
| 
 | |
|   /** @type {object} */
 | |
|   get cloneScope() {
 | |
|     throw new Error("Not implemented");
 | |
|   }
 | |
| 
 | |
|   /** @type {nsIPrincipal} */
 | |
|   get principal() {
 | |
|     throw new Error("Not implemented");
 | |
|   }
 | |
| 
 | |
|   runSafe(callback, ...args) {
 | |
|     return this.applySafe(callback, args);
 | |
|   }
 | |
| 
 | |
|   runSafeWithoutClone(callback, ...args) {
 | |
|     return this.applySafeWithoutClone(callback, args);
 | |
|   }
 | |
| 
 | |
|   applySafe(callback, args, caller) {
 | |
|     if (this.unloaded) {
 | |
|       Cu.reportError("context.runSafe called after context unloaded", caller);
 | |
|     } else if (!this.active) {
 | |
|       Cu.reportError(
 | |
|         "context.runSafe called while context is inactive",
 | |
|         caller
 | |
|       );
 | |
|     } else {
 | |
|       try {
 | |
|         let { cloneScope } = this;
 | |
|         args = args.map(arg => Cu.cloneInto(arg, cloneScope));
 | |
|       } catch (e) {
 | |
|         Cu.reportError(e);
 | |
|         dump(
 | |
|           `runSafe failure: cloning into ${
 | |
|             this.cloneScope
 | |
|           }: ${e}\n\n${filterStack(Error())}`
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       return this.applySafeWithoutClone(callback, args, caller);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   applySafeWithoutClone(callback, args, caller) {
 | |
|     if (this.unloaded) {
 | |
|       Cu.reportError(
 | |
|         "context.runSafeWithoutClone called after context unloaded",
 | |
|         caller
 | |
|       );
 | |
|     } else if (!this.active) {
 | |
|       Cu.reportError(
 | |
|         "context.runSafeWithoutClone called while context is inactive",
 | |
|         caller
 | |
|       );
 | |
|     } else {
 | |
|       try {
 | |
|         return Reflect.apply(callback, null, args);
 | |
|       } catch (e) {
 | |
|         // An extension listener may as well be throwing an object that isn't
 | |
|         // an instance of Error, in that case we have to use fallbacks for the
 | |
|         // error message, fileName, lineNumber and columnNumber properties.
 | |
|         const isError = e instanceof this.Error;
 | |
|         let message;
 | |
|         let fileName;
 | |
|         let lineNumber;
 | |
|         let columnNumber;
 | |
| 
 | |
|         if (isError) {
 | |
|           message = `${e.name}: ${e.message}`;
 | |
|           lineNumber = e.lineNumber;
 | |
|           columnNumber = e.columnNumber;
 | |
|           fileName = e.fileName;
 | |
|         } else {
 | |
|           message = `uncaught exception: ${e}`;
 | |
| 
 | |
|           try {
 | |
|             // TODO(Bug 1810582): the following fallback logic may go away once
 | |
|             // we introduced a better way to capture and log the exception in
 | |
|             // the right window and in all cases (included when the extension
 | |
|             // code is raising undefined or an object that isn't an instance of
 | |
|             // the Error constructor).
 | |
|             //
 | |
|             // Fallbacks for the error location:
 | |
|             // - the callback location if it is registered directly from the
 | |
|             //   extension code (and not wrapped by the child/ext-APINAMe.js
 | |
|             //   implementation, like e.g. browser.storage, browser.devtools.network
 | |
|             //   are doing and browser.menus).
 | |
|             // - if the location of the extension callback is not directly
 | |
|             //   available (e.g. browser.storage onChanged events, and similarly
 | |
|             //   for browser.devtools.network and browser.menus events):
 | |
|             //   - the extension page url if the context is an extension page
 | |
|             //   - the extension base url if the context is a content script
 | |
|             const cbLoc = Cu.getFunctionSourceLocation(callback);
 | |
|             fileName = cbLoc.filename;
 | |
|             lineNumber = cbLoc.lineNumber ?? lineNumber;
 | |
| 
 | |
|             const extBaseUrl = this.extension.baseURI.resolve("/");
 | |
|             if (fileName.startsWith(extBaseUrl)) {
 | |
|               fileName = cbLoc.filename;
 | |
|               lineNumber = cbLoc.lineNumber ?? lineNumber;
 | |
|             } else {
 | |
|               fileName = this.contentWindow?.location?.href;
 | |
|               if (!fileName || !fileName.startsWith(extBaseUrl)) {
 | |
|                 fileName = extBaseUrl;
 | |
|               }
 | |
|             }
 | |
|           } catch {
 | |
|             // Ignore errors on retrieving the callback source location.
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         dump(
 | |
|           `Extension error: ${message} ${fileName} ${lineNumber}\n[[Exception stack\n${
 | |
|             isError ? filterStack(e) : undefined
 | |
|           }Current stack\n${filterStack(Error())}]]\n`
 | |
|         );
 | |
| 
 | |
|         // If the error is coming from an extension context associated
 | |
|         // to a window (e.g. an extension page or extension content script).
 | |
|         //
 | |
|         // TODO(Bug 1810574): for the background service worker we will need to do
 | |
|         // something similar, but not tied to the innerWindowID because there
 | |
|         // wouldn't be one set for extension contexts related to the
 | |
|         // background service worker.
 | |
|         //
 | |
|         // TODO(Bug 1810582): change the error associated to the innerWindowID to also
 | |
|         // include a full stack from the original error.
 | |
|         if (!this.isProxyContextParent && this.contentWindow) {
 | |
|           Services.console.logMessage(
 | |
|             new ScriptError(
 | |
|               message,
 | |
|               fileName,
 | |
|               null,
 | |
|               lineNumber,
 | |
|               columnNumber,
 | |
|               Ci.nsIScriptError.errorFlag,
 | |
|               "content javascript",
 | |
|               this.innerWindowID
 | |
|             )
 | |
|           );
 | |
|         }
 | |
|         // Also report the original error object (because it also includes
 | |
|         // the full error stack).
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   checkLoadURL(url, options = {}) {
 | |
|     // As an optimization, f the URL starts with the extension's base URL,
 | |
|     // don't do any further checks. It's always allowed to load it.
 | |
|     if (url.startsWith(this.extension.baseURL)) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     return checkLoadURL(url, this.principal, options);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Safely call JSON.stringify() on an object that comes from an
 | |
|    * extension.
 | |
|    *
 | |
|    * @param {[any, callback?, number?]} args 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);
 | |
|   }
 | |
| 
 | |
|   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
 | |
|    * @param {SavedFrame?} [caller]
 | |
|    * @returns {Error}
 | |
|    */
 | |
|   normalizeError(error, caller) {
 | |
|     if (error instanceof this.Error) {
 | |
|       return error;
 | |
|     }
 | |
|     let message, fileName;
 | |
|     if (error && typeof error === "object") {
 | |
|       const isPlain = ChromeUtils.getClassName(error) === "Object";
 | |
|       if (isPlain && error.mozWebExtLocation) {
 | |
|         caller = error.mozWebExtLocation;
 | |
|       }
 | |
|       if (isPlain && caller && (error.mozWebExtLocation || !error.fileName)) {
 | |
|         caller = Cu.cloneInto(caller, this.cloneScope);
 | |
|         return ChromeUtils.createError(error.message, caller);
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         isPlain ||
 | |
|         error instanceof ExtensionError ||
 | |
|         this.principal.subsumes(Cu.getObjectPrincipal(error))
 | |
|       ) {
 | |
|         message = error.message;
 | |
|         fileName = error.fileName;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!message) {
 | |
|       Cu.reportError(error);
 | |
|       message = "An unexpected error occurred";
 | |
|     }
 | |
|     return new this.Error(message, fileName);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 {SavedFrame?} caller
 | |
|    *        The optional caller frame which triggered this callback, to be used
 | |
|    *        in error reporting.
 | |
|    * @param {Function} callback The callback to call.
 | |
|    * @returns {*} The return value of callback.
 | |
|    */
 | |
|   withLastError(error, caller, callback) {
 | |
|     this.lastError = this.normalizeError(error);
 | |
|     try {
 | |
|       return callback();
 | |
|     } finally {
 | |
|       if (!this.checkedLastError) {
 | |
|         Cu.reportError(`Unchecked lastError value: ${this.lastError}`, caller);
 | |
|       }
 | |
|       this.lastError = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Captures the most recent stack frame which belongs to the extension.
 | |
|    *
 | |
|    * @returns {SavedFrame?}
 | |
|    */
 | |
|   getCaller() {
 | |
|     return ChromeUtils.getCallerLocation(this.principal);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 caller = this.getCaller();
 | |
|     let applySafe = this.applySafe.bind(this);
 | |
|     if (Cu.getGlobalForObject(promise) === this.cloneScope) {
 | |
|       applySafe = this.applySafeWithoutClone.bind(this);
 | |
|     }
 | |
| 
 | |
|     if (callback) {
 | |
|       promise.then(
 | |
|         args => {
 | |
|           if (this.unloaded) {
 | |
|             Cu.reportError(`Promise resolved after context unloaded\n`, caller);
 | |
|           } else if (!this.active) {
 | |
|             Cu.reportError(
 | |
|               `Promise resolved while context is inactive\n`,
 | |
|               caller
 | |
|             );
 | |
|           } else if (args instanceof NoCloneSpreadArgs) {
 | |
|             this.applySafeWithoutClone(callback, args.unwrappedValues, caller);
 | |
|           } else if (args instanceof SpreadArgs) {
 | |
|             applySafe(callback, args, caller);
 | |
|           } else {
 | |
|             applySafe(callback, [args], caller);
 | |
|           }
 | |
|         },
 | |
|         error => {
 | |
|           this.withLastError(error, caller, () => {
 | |
|             if (this.unloaded) {
 | |
|               Cu.reportError(
 | |
|                 `Promise rejected after context unloaded\n`,
 | |
|                 caller
 | |
|               );
 | |
|             } else if (!this.active) {
 | |
|               Cu.reportError(
 | |
|                 `Promise rejected while context is inactive\n`,
 | |
|                 caller
 | |
|               );
 | |
|             } else {
 | |
|               this.applySafeWithoutClone(callback, [], caller);
 | |
|             }
 | |
|           });
 | |
|         }
 | |
|       );
 | |
|     } else {
 | |
|       return new this.Promise((resolve, reject) => {
 | |
|         promise.then(
 | |
|           value => {
 | |
|             if (this.unloaded) {
 | |
|               Cu.reportError(
 | |
|                 `Promise resolved after context unloaded\n`,
 | |
|                 caller
 | |
|               );
 | |
|             } else if (!this.active) {
 | |
|               Cu.reportError(
 | |
|                 `Promise resolved while context is inactive\n`,
 | |
|                 caller
 | |
|               );
 | |
|             } else if (value instanceof NoCloneSpreadArgs) {
 | |
|               let values = value.unwrappedValues;
 | |
|               this.applySafeWithoutClone(
 | |
|                 resolve,
 | |
|                 values.length == 1 ? [values[0]] : [values],
 | |
|                 caller
 | |
|               );
 | |
|             } else if (value instanceof SpreadArgs) {
 | |
|               applySafe(resolve, value.length == 1 ? value : [value], caller);
 | |
|             } else {
 | |
|               applySafe(resolve, [value], caller);
 | |
|             }
 | |
|           },
 | |
|           value => {
 | |
|             if (this.unloaded) {
 | |
|               Cu.reportError(
 | |
|                 `Promise rejected after context unloaded: ${
 | |
|                   value && value.message
 | |
|                 }\n`,
 | |
|                 caller
 | |
|               );
 | |
|             } else if (!this.active) {
 | |
|               Cu.reportError(
 | |
|                 `Promise rejected while context is inactive: ${
 | |
|                   value && value.message
 | |
|                 }\n`,
 | |
|                 caller
 | |
|               );
 | |
|             } else {
 | |
|               this.applySafeWithoutClone(
 | |
|                 reject,
 | |
|                 [this.normalizeError(value, caller)],
 | |
|                 caller
 | |
|               );
 | |
|             }
 | |
|           }
 | |
|         );
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   unload() {
 | |
|     this.unloaded = true;
 | |
| 
 | |
|     for (let obj of this.onClose) {
 | |
|       obj.close();
 | |
|     }
 | |
|     this.onClose.clear();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 {callback} [callback] The callback to be called when the function
 | |
|    *     completes.
 | |
|    * @param {boolean} [requireUserInput=false] If true, the function should
 | |
|    *                  fail if the browser is not currently handling user input.
 | |
|    * @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, requireUserInput = false) {
 | |
|     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");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Revokes the implementation object, and prevents any further method
 | |
|    * calls from having external effects.
 | |
|    *
 | |
|    * @abstract
 | |
|    */
 | |
|   revoke() {
 | |
|     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;
 | |
|   }
 | |
| 
 | |
|   revoke() {
 | |
|     if (this.pathObj[this.name][lazy.Schemas.REVOKE]) {
 | |
|       this.pathObj[this.name][lazy.Schemas.REVOKE]();
 | |
|     }
 | |
| 
 | |
|     this.pathObj = null;
 | |
|     this.name = null;
 | |
|     this.context = null;
 | |
|   }
 | |
| 
 | |
|   callFunction(args) {
 | |
|     try {
 | |
|       return this.pathObj[this.name](...args);
 | |
|     } catch (e) {
 | |
|       throw this.context.normalizeError(e);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   callFunctionNoReturn(args) {
 | |
|     try {
 | |
|       this.pathObj[this.name](...args);
 | |
|     } catch (e) {
 | |
|       throw this.context.normalizeError(e);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   callAsyncFunction(args, callback, requireUserInput) {
 | |
|     let promise;
 | |
|     try {
 | |
|       if (requireUserInput) {
 | |
|         if (!this.context.contentWindow.windowUtils.isHandlingUserInput) {
 | |
|           throw new ExtensionError(
 | |
|             `${this.name} may only be called from a user input handler`
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|       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);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Recursively copy properties from source to dest.
 | |
| function deepCopy(dest, source) {
 | |
|   for (let prop in source) {
 | |
|     let desc = Object.getOwnPropertyDescriptor(source, prop);
 | |
|     if (typeof desc.value == "object") {
 | |
|       if (!(prop in dest)) {
 | |
|         dest[prop] = {};
 | |
|       }
 | |
|       deepCopy(dest[prop], source[prop]);
 | |
|     } else {
 | |
|       Object.defineProperty(dest, prop, desc);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function getChild(map, key) {
 | |
|   let child = map.children.get(key);
 | |
|   if (!child) {
 | |
|     child = {
 | |
|       modules: new Set(),
 | |
|       children: new Map(),
 | |
|     };
 | |
| 
 | |
|     map.children.set(key, child);
 | |
|   }
 | |
|   return child;
 | |
| }
 | |
| 
 | |
| function getPath(map, path) {
 | |
|   for (let key of path) {
 | |
|     map = getChild(map, key);
 | |
|   }
 | |
|   return map;
 | |
| }
 | |
| 
 | |
| function mergePaths(dest, source) {
 | |
|   for (let name of source.modules) {
 | |
|     dest.modules.add(name);
 | |
|   }
 | |
| 
 | |
|   for (let [name, child] of source.children.entries()) {
 | |
|     mergePaths(getChild(dest, name), child);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Manages loading and accessing a set of APIs for a specific extension
 | |
|  * context.
 | |
|  *
 | |
|  * @param {BaseContext} context
 | |
|  *        The context to manage APIs for.
 | |
|  * @param {SchemaAPIManager} apiManager
 | |
|  *        The API manager holding the APIs to manage.
 | |
|  * @param {object} root
 | |
|  *        The root object into which APIs will be injected.
 | |
|  */
 | |
| class CanOfAPIs {
 | |
|   constructor(context, apiManager, root) {
 | |
|     this.context = context;
 | |
|     this.scopeName = context.envType;
 | |
|     this.apiManager = apiManager;
 | |
|     this.root = root;
 | |
| 
 | |
|     this.apiPaths = new Map();
 | |
| 
 | |
|     this.apis = new Map();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Synchronously loads and initializes an ExtensionAPI instance.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The name of the API to load.
 | |
|    */
 | |
|   loadAPI(name) {
 | |
|     if (this.apis.has(name)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let { extension } = this.context;
 | |
| 
 | |
|     let api = this.apiManager.getAPI(name, extension, this.scopeName);
 | |
|     if (!api) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.apis.set(name, api);
 | |
| 
 | |
|     deepCopy(this.root, api.getAPI(this.context));
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Asynchronously loads and initializes an ExtensionAPI instance.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The name of the API to load.
 | |
|    */
 | |
|   async asyncLoadAPI(name) {
 | |
|     if (this.apis.has(name)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let { extension } = this.context;
 | |
|     if (!lazy.Schemas.checkPermissions(name, extension)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let api = await this.apiManager.asyncGetAPI(
 | |
|       name,
 | |
|       extension,
 | |
|       this.scopeName
 | |
|     );
 | |
|     // Check again, because async;
 | |
|     if (this.apis.has(name)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.apis.set(name, api);
 | |
| 
 | |
|     deepCopy(this.root, api.getAPI(this.context));
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Finds the API at the given path from the root object, and
 | |
|    * synchronously loads the API that implements it if it has not
 | |
|    * already been loaded.
 | |
|    *
 | |
|    * @param {string} path
 | |
|    *        The "."-separated path to find.
 | |
|    * @returns {*}
 | |
|    */
 | |
|   findAPIPath(path) {
 | |
|     if (this.apiPaths.has(path)) {
 | |
|       return this.apiPaths.get(path);
 | |
|     }
 | |
| 
 | |
|     let obj = this.root;
 | |
|     let modules = this.apiManager.modulePaths;
 | |
| 
 | |
|     let parts = path.split(".");
 | |
|     for (let [i, key] of parts.entries()) {
 | |
|       if (!obj) {
 | |
|         return;
 | |
|       }
 | |
|       modules = getChild(modules, key);
 | |
| 
 | |
|       for (let name of modules.modules) {
 | |
|         if (!this.apis.has(name)) {
 | |
|           this.loadAPI(name);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (!(key in obj) && i < parts.length - 1) {
 | |
|         obj[key] = {};
 | |
|       }
 | |
|       obj = obj[key];
 | |
|     }
 | |
| 
 | |
|     this.apiPaths.set(path, obj);
 | |
|     return obj;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Finds the API at the given path from the root object, and
 | |
|    * asynchronously loads the API that implements it if it has not
 | |
|    * already been loaded.
 | |
|    *
 | |
|    * @param {string} path
 | |
|    *        The "."-separated path to find.
 | |
|    * @returns {Promise<*>}
 | |
|    */
 | |
|   async asyncFindAPIPath(path) {
 | |
|     if (this.apiPaths.has(path)) {
 | |
|       return this.apiPaths.get(path);
 | |
|     }
 | |
| 
 | |
|     let obj = this.root;
 | |
|     let modules = this.apiManager.modulePaths;
 | |
| 
 | |
|     let parts = path.split(".");
 | |
|     for (let [i, key] of parts.entries()) {
 | |
|       if (!obj) {
 | |
|         return;
 | |
|       }
 | |
|       modules = getChild(modules, key);
 | |
| 
 | |
|       for (let name of modules.modules) {
 | |
|         if (!this.apis.has(name)) {
 | |
|           await this.asyncLoadAPI(name);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (!(key in obj) && i < parts.length - 1) {
 | |
|         obj[key] = {};
 | |
|       }
 | |
| 
 | |
|       if (typeof obj[key] === "function") {
 | |
|         obj = obj[key].bind(obj);
 | |
|       } else {
 | |
|         obj = obj[key];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.apiPaths.set(path, obj);
 | |
|     return obj;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @class APIModule
 | |
|  * @abstract
 | |
|  *
 | |
|  * @property {string} url
 | |
|  *       The URL of the script which contains the module's
 | |
|  *       implementation. This script must define a global property
 | |
|  *       matching the modules name, which must be a class constructor
 | |
|  *       which inherits from {@link ExtensionAPI}.
 | |
|  *
 | |
|  * @property {string} schema
 | |
|  *       The URL of the JSON schema which describes the module's API.
 | |
|  *
 | |
|  * @property {Array<string>} scopes
 | |
|  *       The list of scope names into which the API may be loaded.
 | |
|  *
 | |
|  * @property {Array<string>} manifest
 | |
|  *       The list of top-level manifest properties which will trigger
 | |
|  *       the module to be loaded, and its `onManifestEntry` method to be
 | |
|  *       called.
 | |
|  *
 | |
|  * @property {Array<string>} events
 | |
|  *       The list events which will trigger the module to be loaded, and
 | |
|  *       its appropriate event handler method to be called. Currently
 | |
|  *       only accepts "startup".
 | |
|  *
 | |
|  * @property {Array<string>} permissions
 | |
|  *       An optional list of permissions, any of which must be present
 | |
|  *       in order for the module to load.
 | |
|  *
 | |
|  * @property {Array<Array<string>>} paths
 | |
|  *       A list of paths from the root API object which, when accessed,
 | |
|  *       will cause the API module to be instantiated and injected.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * 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.
 | |
|    *     "devtools" - A devtools process.
 | |
|    * @param {import("Schemas.sys.mjs").SchemaRoot} [schema]
 | |
|    */
 | |
|   constructor(processType, schema) {
 | |
|     super();
 | |
|     this.processType = processType;
 | |
|     this.global = null;
 | |
|     if (schema) {
 | |
|       this.schema = schema;
 | |
|     }
 | |
| 
 | |
|     this.modules = new Map();
 | |
|     this.modulePaths = { children: new Map(), modules: new Set() };
 | |
|     this.manifestKeys = new Map();
 | |
|     this.eventModules = new DefaultMap(() => new Set());
 | |
|     this.settingsModules = new Set();
 | |
| 
 | |
|     this._modulesJSONLoaded = false;
 | |
| 
 | |
|     this.schemaURLs = new Map();
 | |
| 
 | |
|     this.apis = new DefaultWeakMap(() => new Map());
 | |
| 
 | |
|     this._scriptScopes = [];
 | |
|   }
 | |
| 
 | |
|   onStartup(extension) {
 | |
|     let promises = [];
 | |
|     for (let apiName of this.eventModules.get("startup")) {
 | |
|       promises.push(
 | |
|         extension.apiManager.asyncGetAPI(apiName, extension).then(api => {
 | |
|           if (api) {
 | |
|             api.onStartup();
 | |
|           }
 | |
|         })
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return Promise.all(promises);
 | |
|   }
 | |
| 
 | |
|   async loadModuleJSON(urls) {
 | |
|     let promises = urls.map(url => fetch(url).then(resp => resp.json()));
 | |
| 
 | |
|     return this.initModuleJSON(await Promise.all(promises));
 | |
|   }
 | |
| 
 | |
|   initModuleJSON(blobs) {
 | |
|     for (let json of blobs) {
 | |
|       this.registerModules(json);
 | |
|     }
 | |
| 
 | |
|     this._modulesJSONLoaded = true;
 | |
| 
 | |
|     return new StructuredCloneHolder("SchemaAPIManager/initModuleJSON", null, {
 | |
|       modules: this.modules,
 | |
|       modulePaths: this.modulePaths,
 | |
|       manifestKeys: this.manifestKeys,
 | |
|       eventModules: this.eventModules,
 | |
|       settingsModules: this.settingsModules,
 | |
|       schemaURLs: this.schemaURLs,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   initModuleData(moduleData) {
 | |
|     if (!this._modulesJSONLoaded) {
 | |
|       let data = moduleData.deserialize({}, true);
 | |
| 
 | |
|       this.modules = data.modules;
 | |
|       this.modulePaths = data.modulePaths;
 | |
|       this.manifestKeys = data.manifestKeys;
 | |
|       this.eventModules = new DefaultMap(() => new Set(), data.eventModules);
 | |
|       this.settingsModules = new Set(data.settingsModules);
 | |
|       this.schemaURLs = data.schemaURLs;
 | |
|     }
 | |
| 
 | |
|     this._modulesJSONLoaded = true;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Registers a set of ExtensionAPI modules to be lazily loaded and
 | |
|    * managed by this manager.
 | |
|    *
 | |
|    * @param {object} obj
 | |
|    *        An object containing property for eacy API module to be
 | |
|    *        registered. Each value should be an object implementing the
 | |
|    *        APIModule interface.
 | |
|    */
 | |
|   registerModules(obj) {
 | |
|     for (let [name, details] of Object.entries(obj)) {
 | |
|       details.namespaceName = name;
 | |
| 
 | |
|       if (this.modules.has(name)) {
 | |
|         throw new Error(`Module '${name}' already registered`);
 | |
|       }
 | |
|       this.modules.set(name, details);
 | |
| 
 | |
|       if (details.schema) {
 | |
|         let content =
 | |
|           details.scopes &&
 | |
|           (details.scopes.includes("content_parent") ||
 | |
|             details.scopes.includes("content_child"));
 | |
|         this.schemaURLs.set(details.schema, { content });
 | |
|       }
 | |
| 
 | |
|       for (let event of details.events || []) {
 | |
|         this.eventModules.get(event).add(name);
 | |
|       }
 | |
| 
 | |
|       if (details.settings) {
 | |
|         this.settingsModules.add(name);
 | |
|       }
 | |
| 
 | |
|       for (let key of details.manifest || []) {
 | |
|         if (this.manifestKeys.has(key)) {
 | |
|           throw new Error(
 | |
|             `Manifest key '${key}' already registered by '${this.manifestKeys.get(
 | |
|               key
 | |
|             )}'`
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         this.manifestKeys.set(key, name);
 | |
|       }
 | |
| 
 | |
|       for (let path of details.paths || []) {
 | |
|         getPath(this.modulePaths, path).modules.add(name);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emits an `onManifestEntry` event for the top-level manifest entry
 | |
|    * on all relevant {@link ExtensionAPI} instances for the given
 | |
|    * extension.
 | |
|    *
 | |
|    * The API modules will be synchronously loaded if they have not been
 | |
|    * loaded already.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    *        The extension for which to emit the events.
 | |
|    * @param {string} entry
 | |
|    *        The name of the top-level manifest entry.
 | |
|    *
 | |
|    * @returns {*}
 | |
|    */
 | |
|   emitManifestEntry(extension, entry) {
 | |
|     let apiName = this.manifestKeys.get(entry);
 | |
|     if (apiName) {
 | |
|       let api = extension.apiManager.getAPI(apiName, extension);
 | |
|       return api.onManifestEntry(entry);
 | |
|     }
 | |
|   }
 | |
|   /**
 | |
|    * Emits an `onManifestEntry` event for the top-level manifest entry
 | |
|    * on all relevant {@link ExtensionAPI} instances for the given
 | |
|    * extension.
 | |
|    *
 | |
|    * The API modules will be asynchronously loaded if they have not been
 | |
|    * loaded already.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    *        The extension for which to emit the events.
 | |
|    * @param {string} entry
 | |
|    *        The name of the top-level manifest entry.
 | |
|    *
 | |
|    * @returns {Promise<*>}
 | |
|    */
 | |
|   async asyncEmitManifestEntry(extension, entry) {
 | |
|     let apiName = this.manifestKeys.get(entry);
 | |
|     if (apiName) {
 | |
|       let api = await extension.apiManager.asyncGetAPI(apiName, extension);
 | |
|       return api.onManifestEntry(entry);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the {@link ExtensionAPI} instance for the given API module,
 | |
|    * for the given extension, in the given scope, synchronously loading
 | |
|    * and instantiating it if necessary.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The name of the API module to load.
 | |
|    * @param {Extension} extension
 | |
|    *        The extension for which to load the API.
 | |
|    * @param {string} [scope = null]
 | |
|    *        The scope type for which to retrieve the API, or null if not
 | |
|    *        being retrieved for a particular scope.
 | |
|    *
 | |
|    * @returns {ExtensionAPI?}
 | |
|    */
 | |
|   getAPI(name, extension, scope = null) {
 | |
|     if (!this._checkGetAPI(name, extension, scope)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let apis = this.apis.get(extension);
 | |
|     if (apis.has(name)) {
 | |
|       return apis.get(name);
 | |
|     }
 | |
| 
 | |
|     let module = this.loadModule(name);
 | |
| 
 | |
|     let api = new module(extension);
 | |
|     apis.set(name, api);
 | |
|     return api;
 | |
|   }
 | |
|   /**
 | |
|    * Returns the {@link ExtensionAPI} instance for the given API module,
 | |
|    * for the given extension, in the given scope, asynchronously loading
 | |
|    * and instantiating it if necessary.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The name of the API module to load.
 | |
|    * @param {Extension} extension
 | |
|    *        The extension for which to load the API.
 | |
|    * @param {string} [scope = null]
 | |
|    *        The scope type for which to retrieve the API, or null if not
 | |
|    *        being retrieved for a particular scope.
 | |
|    *
 | |
|    * @returns {Promise<ExtensionAPI>?}
 | |
|    */
 | |
|   async asyncGetAPI(name, extension, scope = null) {
 | |
|     if (!this._checkGetAPI(name, extension, scope)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let apis = this.apis.get(extension);
 | |
|     if (apis.has(name)) {
 | |
|       return apis.get(name);
 | |
|     }
 | |
| 
 | |
|     let module = await this.asyncLoadModule(name);
 | |
| 
 | |
|     // Check again, because async.
 | |
|     if (apis.has(name)) {
 | |
|       return apis.get(name);
 | |
|     }
 | |
| 
 | |
|     let api = new module(extension);
 | |
|     apis.set(name, api);
 | |
|     return api;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Synchronously loads an API module, if not already loaded, and
 | |
|    * returns its ExtensionAPI constructor.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The name of the module to load.
 | |
|    * @returns {typeof ExtensionAPI}
 | |
|    */
 | |
|   loadModule(name) {
 | |
|     let module = this.modules.get(name);
 | |
|     if (module.loaded) {
 | |
|       return this.global[name];
 | |
|     }
 | |
| 
 | |
|     this._checkLoadModule(module, name);
 | |
| 
 | |
|     this.initGlobal();
 | |
| 
 | |
|     Services.scriptloader.loadSubScript(module.url, this.global);
 | |
| 
 | |
|     module.loaded = true;
 | |
| 
 | |
|     return this.global[name];
 | |
|   }
 | |
|   /**
 | |
|    * aSynchronously loads an API module, if not already loaded, and
 | |
|    * returns its ExtensionAPI constructor.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The name of the module to load.
 | |
|    *
 | |
|    * @returns {Promise<typeof ExtensionAPI>}
 | |
|    */
 | |
|   asyncLoadModule(name) {
 | |
|     let module = this.modules.get(name);
 | |
|     if (module.loaded) {
 | |
|       return Promise.resolve(this.global[name]);
 | |
|     }
 | |
|     if (module.asyncLoaded) {
 | |
|       return module.asyncLoaded;
 | |
|     }
 | |
| 
 | |
|     this._checkLoadModule(module, name);
 | |
| 
 | |
|     module.asyncLoaded = ChromeUtils.compileScript(module.url).then(script => {
 | |
|       this.initGlobal();
 | |
|       script.executeInGlobal(this.global);
 | |
| 
 | |
|       module.loaded = true;
 | |
| 
 | |
|       return this.global[name];
 | |
|     });
 | |
| 
 | |
|     return module.asyncLoaded;
 | |
|   }
 | |
| 
 | |
|   asyncLoadSettingsModules() {
 | |
|     return Promise.all(
 | |
|       Array.from(this.settingsModules).map(apiName =>
 | |
|         this.asyncLoadModule(apiName)
 | |
|       )
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   getModule(name) {
 | |
|     return this.modules.get(name);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Checks whether the given API module may be loaded for the given
 | |
|    * extension, in the given scope.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The name of the API module to check.
 | |
|    * @param {Extension} extension
 | |
|    *        The extension for which to check the API.
 | |
|    * @param {string} [scope = null]
 | |
|    *        The scope type for which to check the API, or null if not
 | |
|    *        being checked for a particular scope.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *        Whether the module may be loaded.
 | |
|    */
 | |
|   _checkGetAPI(name, extension, scope = null) {
 | |
|     let module = this.getModule(name);
 | |
|     if (!module) {
 | |
|       // A module may not exist for a particular manifest version, but
 | |
|       // we allow keys in the manifest.  An example is pageAction.
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       module.permissions &&
 | |
|       !module.permissions.some(perm => extension.hasPermission(perm))
 | |
|     ) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (!scope) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     if (!module.scopes.includes(scope)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (!lazy.Schemas.checkPermissions(module.namespaceName, extension)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   _checkLoadModule(module, name) {
 | |
|     if (!module) {
 | |
|       throw new Error(`Module '${name}' does not exist`);
 | |
|     }
 | |
|     if (module.asyncLoaded) {
 | |
|       throw new Error(`Module '${name}' currently being lazily loaded`);
 | |
|     }
 | |
|     if (this.global && this.global[name]) {
 | |
|       throw new Error(
 | |
|         `Module '${name}' conflicts with existing global property`
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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,
 | |
|         wantGlobalProperties: ["ChromeUtils"],
 | |
|         sandboxName: `Namespace of ext-*.js scripts for ${this.processType} (from: resource://gre/modules/ExtensionCommon.jsm)`,
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     Object.assign(global, {
 | |
|       AppConstants,
 | |
|       Cc,
 | |
|       ChromeWorker,
 | |
|       Ci,
 | |
|       Cr,
 | |
|       Cu,
 | |
|       ExtensionAPI,
 | |
|       ExtensionAPIPersistent,
 | |
|       ExtensionCommon,
 | |
|       IOUtils,
 | |
|       MatchGlob,
 | |
|       MatchPattern,
 | |
|       MatchPatternSet,
 | |
|       PathUtils,
 | |
|       Services,
 | |
|       StructuredCloneHolder,
 | |
|       WebExtensionPolicy,
 | |
|       XPCOMUtils,
 | |
|       extensions: this,
 | |
|       global,
 | |
|     });
 | |
| 
 | |
|     ChromeUtils.defineLazyGetter(global, "console", getConsole);
 | |
|     // eslint-disable-next-line mozilla/lazy-getter-object-name
 | |
|     ChromeUtils.defineESModuleGetters(global, {
 | |
|       ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
 | |
|     });
 | |
| 
 | |
|     return global;
 | |
|   }
 | |
| 
 | |
|   initGlobal() {
 | |
|     if (!this.global) {
 | |
|       this.global = this._createExtGlobal();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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);
 | |
| 
 | |
|     // Save the scope to avoid it being garbage collected.
 | |
|     this._scriptScopes.push(scope);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class LazyAPIManager extends SchemaAPIManager {
 | |
|   constructor(processType, moduleData, schemaURLs) {
 | |
|     super(processType);
 | |
| 
 | |
|     /** @type {Promise | boolean} */
 | |
|     this.initialized = false;
 | |
| 
 | |
|     this.initModuleData(moduleData);
 | |
| 
 | |
|     this.schemaURLs = schemaURLs;
 | |
|   }
 | |
| 
 | |
|   lazyInit() {}
 | |
| }
 | |
| 
 | |
| defineLazyGetter(LazyAPIManager.prototype, "schema", function () {
 | |
|   let root = new lazy.SchemaRoot(lazy.Schemas.rootSchema, this.schemaURLs);
 | |
|   root.parseSchemas();
 | |
|   return root;
 | |
| });
 | |
| 
 | |
| class MultiAPIManager extends SchemaAPIManager {
 | |
|   constructor(processType, children) {
 | |
|     super(processType);
 | |
| 
 | |
|     this.initialized = false;
 | |
| 
 | |
|     this.children = children;
 | |
|   }
 | |
| 
 | |
|   async lazyInit() {
 | |
|     if (!this.initialized) {
 | |
|       this.initialized = true;
 | |
| 
 | |
|       for (let child of this.children) {
 | |
|         if (child.lazyInit) {
 | |
|           let res = child.lazyInit();
 | |
|           if (res && typeof res.then === "function") {
 | |
|             await res;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         mergePaths(this.modulePaths, child.modulePaths);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   onStartup(extension) {
 | |
|     return Promise.all(this.children.map(child => child.onStartup(extension)));
 | |
|   }
 | |
| 
 | |
|   getModule(name) {
 | |
|     for (let child of this.children) {
 | |
|       if (child.modules.has(name)) {
 | |
|         return child.modules.get(name);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   loadModule(name) {
 | |
|     for (let child of this.children) {
 | |
|       if (child.modules.has(name)) {
 | |
|         return child.loadModule(name);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   asyncLoadModule(name) {
 | |
|     for (let child of this.children) {
 | |
|       if (child.modules.has(name)) {
 | |
|         return child.asyncLoadModule(name);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| defineLazyGetter(MultiAPIManager.prototype, "schema", function () {
 | |
|   let bases = this.children.map(child => child.schema);
 | |
| 
 | |
|   // All API manager schema roots should derive from the global schema root,
 | |
|   // so it doesn't need its own entry.
 | |
|   if (bases[bases.length - 1] === lazy.Schemas) {
 | |
|     bases.pop();
 | |
|   }
 | |
| 
 | |
|   if (bases.length === 1) {
 | |
|     bases = bases[0];
 | |
|   }
 | |
|   return new lazy.SchemaRoot(bases, new Map());
 | |
| });
 | |
| 
 | |
| export function LocaleData(data) {
 | |
|   this.defaultLocale = data.defaultLocale;
 | |
|   this.selectedLocale = data.selectedLocale;
 | |
|   this.locales = data.locales || new Map();
 | |
|   this.warnedMissingKeys = new Set();
 | |
| 
 | |
|   // Map(locale-name -> Map(message-key -> localized-string))
 | |
|   //
 | |
|   // Contains a key for each loaded locale, each of which is a
 | |
|   // Map of message keys to their localized strings.
 | |
|   this.messages = data.messages || new Map();
 | |
| 
 | |
|   if (data.builtinMessages) {
 | |
|     this.messages.set(this.BUILTIN, data.builtinMessages);
 | |
|   }
 | |
| }
 | |
| 
 | |
| LocaleData.prototype = {
 | |
|   // Representation of the object to send to content processes. This
 | |
|   // should include anything the content process might need.
 | |
|   serialize() {
 | |
|     return {
 | |
|       defaultLocale: this.defaultLocale,
 | |
|       selectedLocale: this.selectedLocale,
 | |
|       messages: this.messages,
 | |
|       locales: this.locales,
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   BUILTIN: "@@BUILTIN_MESSAGES",
 | |
| 
 | |
|   has(locale) {
 | |
|     return this.messages.has(locale);
 | |
|   },
 | |
| 
 | |
|   // https://developer.chrome.com/extensions/i18n
 | |
|   localizeMessage(message, substitutions = [], options = {}) {
 | |
|     let defaultOptions = {
 | |
|       defaultValue: "",
 | |
|       cloneScope: null,
 | |
|     };
 | |
| 
 | |
|     let locales = this.availableLocales;
 | |
|     if (options.locale) {
 | |
|       locales = new Set(
 | |
|         [this.BUILTIN, options.locale, this.defaultLocale].filter(locale =>
 | |
|           this.messages.has(locale)
 | |
|         )
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     options = Object.assign(defaultOptions, options);
 | |
| 
 | |
|     // Message names are case-insensitive, so normalize them to lower-case.
 | |
|     message = message.toLowerCase();
 | |
|     for (let locale of locales) {
 | |
|       let messages = this.messages.get(locale);
 | |
|       if (messages.has(message)) {
 | |
|         let str = messages.get(message);
 | |
| 
 | |
|         if (!str.includes("$")) {
 | |
|           return str;
 | |
|         }
 | |
| 
 | |
|         if (!Array.isArray(substitutions)) {
 | |
|           substitutions = [substitutions];
 | |
|         }
 | |
| 
 | |
|         let replacer = (matched, index, dollarSigns) => {
 | |
|           if (index) {
 | |
|             // This is not quite Chrome-compatible. Chrome consumes any number
 | |
|             // of digits following the $, but only accepts 9 substitutions. We
 | |
|             // accept any number of substitutions.
 | |
|             index = parseInt(index, 10) - 1;
 | |
|             return index in substitutions ? substitutions[index] : "";
 | |
|           }
 | |
|           // For any series of contiguous `$`s, the first is dropped, and
 | |
|           // the rest remain in the output string.
 | |
|           return dollarSigns;
 | |
|         };
 | |
|         return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Check for certain pre-defined messages.
 | |
|     if (message == "@@ui_locale") {
 | |
|       return this.uiLocale;
 | |
|     } else if (message.startsWith("@@bidi_")) {
 | |
|       let rtl = Services.locale.isAppLocaleRTL;
 | |
| 
 | |
|       if (message == "@@bidi_dir") {
 | |
|         return rtl ? "rtl" : "ltr";
 | |
|       } else if (message == "@@bidi_reversed_dir") {
 | |
|         return rtl ? "ltr" : "rtl";
 | |
|       } else if (message == "@@bidi_start_edge") {
 | |
|         return rtl ? "right" : "left";
 | |
|       } else if (message == "@@bidi_end_edge") {
 | |
|         return rtl ? "left" : "right";
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!this.warnedMissingKeys.has(message)) {
 | |
|       let error = `Unknown localization message ${message}`;
 | |
|       if (options.cloneScope) {
 | |
|         error = new options.cloneScope.Error(error);
 | |
|       }
 | |
|       Cu.reportError(error);
 | |
|       this.warnedMissingKeys.add(message);
 | |
|     }
 | |
|     return options.defaultValue;
 | |
|   },
 | |
| 
 | |
|   // Localize a string, replacing all |__MSG_(.*)__| tokens with the
 | |
|   // matching string from the current locale, as determined by
 | |
|   // |this.selectedLocale|.
 | |
|   //
 | |
|   // This may not be called before calling either |initLocale| or
 | |
|   // |initAllLocales|.
 | |
|   localize(str, locale = this.selectedLocale) {
 | |
|     if (!str) {
 | |
|       return str;
 | |
|     }
 | |
| 
 | |
|     return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
 | |
|       return this.localizeMessage(message, [], {
 | |
|         locale,
 | |
|         defaultValue: matched,
 | |
|       });
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   // Validates the contents of a locale JSON file, normalizes the
 | |
|   // messages into a Map of message key -> localized string pairs.
 | |
|   addLocale(locale, messages, extension) {
 | |
|     let result = new Map();
 | |
| 
 | |
|     let isPlainObject = obj =>
 | |
|       obj &&
 | |
|       typeof obj === "object" &&
 | |
|       ChromeUtils.getClassName(obj) === "Object";
 | |
| 
 | |
|     // Chrome does not document the semantics of its localization
 | |
|     // system very well. It handles replacements by pre-processing
 | |
|     // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
 | |
|     // replacements. Later, it processes the resulting string for
 | |
|     // |$[0-9]| replacements.
 | |
|     //
 | |
|     // Again, it does not document this, but it accepts any number
 | |
|     // of sequential |$|s, and replaces them with that number minus
 | |
|     // 1. It also accepts |$| followed by any number of sequential
 | |
|     // digits, but refuses to process a localized string which
 | |
|     // provides more than 9 substitutions.
 | |
|     if (!isPlainObject(messages)) {
 | |
|       extension.packagingError(`Invalid locale data for ${locale}`);
 | |
|       return result;
 | |
|     }
 | |
| 
 | |
|     for (let key of Object.keys(messages)) {
 | |
|       let msg = messages[key];
 | |
| 
 | |
|       if (!isPlainObject(msg) || typeof msg.message != "string") {
 | |
|         extension.packagingError(
 | |
|           `Invalid locale message data for ${locale}, message ${JSON.stringify(
 | |
|             key
 | |
|           )}`
 | |
|         );
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       // Substitutions are case-insensitive, so normalize all of their names
 | |
|       // to lower-case.
 | |
|       let placeholders = new Map();
 | |
|       if ("placeholders" in msg && isPlainObject(msg.placeholders)) {
 | |
|         for (let key of Object.keys(msg.placeholders)) {
 | |
|           placeholders.set(key.toLowerCase(), msg.placeholders[key]);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       let replacer = (match, name) => {
 | |
|         let replacement = placeholders.get(name.toLowerCase());
 | |
|         if (isPlainObject(replacement) && "content" in replacement) {
 | |
|           return replacement.content;
 | |
|         }
 | |
|         return "";
 | |
|       };
 | |
| 
 | |
|       let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
 | |
| 
 | |
|       // Message names are also case-insensitive, so normalize them to lower-case.
 | |
|       result.set(key.toLowerCase(), value);
 | |
|     }
 | |
| 
 | |
|     this.messages.set(locale, result);
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   get acceptLanguages() {
 | |
|     let result = Services.prefs.getComplexValue(
 | |
|       "intl.accept_languages",
 | |
|       Ci.nsIPrefLocalizedString
 | |
|     ).data;
 | |
|     return result.split(/\s*,\s*/g);
 | |
|   },
 | |
| 
 | |
|   get uiLocale() {
 | |
|     return Services.locale.appLocaleAsBCP47;
 | |
|   },
 | |
| 
 | |
|   get availableLocales() {
 | |
|     const locales = [this.BUILTIN, this.selectedLocale, this.defaultLocale];
 | |
|     const value = new Set(locales.filter(locale => this.messages.has(locale)));
 | |
|     return redefineGetter(this, "availableLocales", value);
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * This is a generic class for managing event listeners.
 | |
|  *
 | |
|  * @example
 | |
|  * new EventManager({
 | |
|  *   context,
 | |
|  *   name: "api.subAPI",
 | |
|  *   register:  fire => {
 | |
|  *     let listener = (...) => {
 | |
|  *       // Fire any listeners registered with addListener.
 | |
|  *       fire.async(arg1, arg2);
 | |
|  *     };
 | |
|  *     // Register the listener.
 | |
|  *     SomehowRegisterListener(listener);
 | |
|  *     return () => {
 | |
|  *       // Return a way to unregister the listener.
 | |
|  *       SomehowUnregisterListener(listener);
 | |
|  *     };
 | |
|  *   }
 | |
|  * }).api()
 | |
|  *
 | |
|  * The result is an object with addListener, removeListener, and
 | |
|  * hasListener methods. `context` is an add-on scope (either an
 | |
|  * ExtensionContext in the chrome process or ExtensionContext in a
 | |
|  * content process).
 | |
|  */
 | |
| class EventManager {
 | |
|   /*
 | |
|    * A persistent event must provide module and name.  Additionally the
 | |
|    * module must implement primeListeners in the ExtensionAPI class.
 | |
|    *
 | |
|    * A startup blocking event must also add the startupBlocking flag in
 | |
|    * ext-toolkit.json or ext-browser.json.
 | |
|    *
 | |
|    * Listeners synchronously added from a background extension context
 | |
|    * will be persisted, for a persistent background script only the
 | |
|    * "startup blocking" events will be persisted.
 | |
|    *
 | |
|    * EventManager instances created in a child process can't persist any listener.
 | |
|    *
 | |
|    * @param {object} params
 | |
|    *        Parameters that control this EventManager.
 | |
|    * @param {BaseContext} params.context
 | |
|    *        An object representing the extension instance using this event.
 | |
|    * @param {string} params.module
 | |
|    *        The API module name, required for persistent events.
 | |
|    * @param {string} params.event
 | |
|    *        The API event name, required for persistent events.
 | |
|    * @param {ExtensionAPI} params.extensionApi
 | |
|    *        The API intance.  If the API uses the ExtensionAPIPersistent class, some simplification is
 | |
|    *        possible by passing the api (self or this) and the internal register function will be used.
 | |
|    * @param {string} [params.name]
 | |
|    *        A name used only for debugging.  If not provided, name is built from module and event.
 | |
|    * @param {functon} params.register
 | |
|    *        A function called whenever a new listener is added.
 | |
|    * @param {boolean} [params.inputHandling=false]
 | |
|    *        If true, the "handling user input" flag is set while handlers
 | |
|    *        for this event are executing.
 | |
|    */
 | |
|   constructor(params) {
 | |
|     let {
 | |
|       context,
 | |
|       module,
 | |
|       event,
 | |
|       name,
 | |
|       register,
 | |
|       extensionApi,
 | |
|       inputHandling = false,
 | |
|       resetIdleOnEvent = true,
 | |
|     } = params;
 | |
|     this.context = context;
 | |
|     this.module = module;
 | |
|     this.event = event;
 | |
|     this.name = name;
 | |
|     this.register = register;
 | |
|     this.inputHandling = inputHandling;
 | |
|     this.resetIdleOnEvent = resetIdleOnEvent;
 | |
| 
 | |
|     const isBackgroundParent =
 | |
|       this.context.envType === "addon_parent" &&
 | |
|       this.context.isBackgroundContext;
 | |
| 
 | |
|     // TODO(Bug 1844041): ideally we should restrict resetIdleOnEvent to
 | |
|     // EventManager instances that belongs to the event page, but along
 | |
|     // with that we should consider if calling sendMessage from an event
 | |
|     // page should also reset idle timer, and so in the shorter term
 | |
|     // here we are allowing listeners from other extension pages to
 | |
|     // also reset the idle timer.
 | |
|     const isAddonContext = ["addon_parent", "addon_child"].includes(
 | |
|       this.context.envType
 | |
|     );
 | |
| 
 | |
|     // Avoid resetIdleOnEvent overhead by only consider it when applicable.
 | |
|     if (!isAddonContext || context.extension.persistentBackground) {
 | |
|       this.resetIdleOnEvent = false;
 | |
|     }
 | |
| 
 | |
|     if (!name) {
 | |
|       this.name = `${module}.${event}`;
 | |
|     }
 | |
| 
 | |
|     if (!this.register && extensionApi instanceof ExtensionAPIPersistent) {
 | |
|       this.register = (fire, ...params) => {
 | |
|         return extensionApi.registerEventListener(
 | |
|           { context, event, fire },
 | |
|           params
 | |
|         );
 | |
|       };
 | |
|     }
 | |
|     if (!this.register) {
 | |
|       throw new Error(
 | |
|         `EventManager requires register method for ${this.name}.`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.canPersistEvents = module && event && isBackgroundParent;
 | |
| 
 | |
|     if (this.canPersistEvents) {
 | |
|       let { extension } = context;
 | |
|       if (extension.persistentBackground) {
 | |
|         // Persistent backgrounds will only persist startup blocking APIs.
 | |
|         let api_module = extension.apiManager.getModule(this.module);
 | |
|         if (!api_module?.startupBlocking) {
 | |
|           this.canPersistEvents = false;
 | |
|         }
 | |
|       } else {
 | |
|         // Event pages will persist all APIs that implement primeListener.
 | |
|         // The api is already loaded so this does not have performance effect.
 | |
|         let api = extension.apiManager.getAPI(
 | |
|           this.module,
 | |
|           extension,
 | |
|           "addon_parent"
 | |
|         );
 | |
| 
 | |
|         // If the api doesn't implement primeListener we do not persist the events.
 | |
|         if (!api?.primeListener) {
 | |
|           this.canPersistEvents = false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.unregister = new Map();
 | |
|     this.remove = new Map();
 | |
|   }
 | |
| 
 | |
|   /*
 | |
|    * Information about listeners to persistent events is associated with
 | |
|    * the extension to which they belong.  Any extension thas has such
 | |
|    * listeners has a property called `persistentListeners` that is a
 | |
|    * 3-level Map:
 | |
|    *
 | |
|    * - the first 2 keys are the module name (e.g., webRequest)
 | |
|    *   and the name of the event within the module (e.g., onBeforeRequest).
 | |
|    *
 | |
|    * - the third level of the map is used to track multiple listeners for
 | |
|    *   the same event, these listeners are distinguished by the extra arguments
 | |
|    *   passed to addListener()
 | |
|    *
 | |
|    * - for quick lookups, the key to the third Map is the result of calling
 | |
|    *   uneval() on the array of extra arguments.
 | |
|    *
 | |
|    * - the value stored in the Map or persistent listeners we keep in memory
 | |
|    *   is a plain object with:
 | |
|    *   - a property called `params` that is the original (ie, not uneval()ed)
 | |
|    *     extra arguments to addListener()
 | |
|    *   - and a property called `listeners` that is an array of plain object
 | |
|    *     each representing a listener to be primed and a `primeId` autoincremented
 | |
|    *     integer that represents each of the primed listeners that belongs to the
 | |
|    *     group listeners with the same set of extra params.
 | |
|    *   - a `nextPrimeId` property keeps track of the numeric primeId that should
 | |
|    *     be assigned to new persistent listeners added for the same event and
 | |
|    *     same set of extra params.
 | |
|    *
 | |
|    * For a primed listener (i.e., the stub listener created during browser startup
 | |
|    * before the extension background page is started, and after an event page is
 | |
|    * suspended on idle), the object will be later populated (by the callers of
 | |
|    * EventManager.primeListeners) with an additional `primed` property that serves
 | |
|    * as a placeholder listener, collecting all events that got emitted while the
 | |
|    * background page was not yet started, and eventually replaced by a callback
 | |
|    * registered from the extension code, once the background page scripts have been
 | |
|    * executed (or dropped if the background page scripts do not register the same
 | |
|    * listener anymore).
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    * @returns {boolean} True if the extension had any persistent listeners.
 | |
|    */
 | |
|   static _initPersistentListeners(extension) {
 | |
|     if (extension.persistentListeners) {
 | |
|       return !!extension.persistentListeners.size;
 | |
|     }
 | |
| 
 | |
|     let listeners = new DefaultMap(() => new DefaultMap(() => new Map()));
 | |
|     extension.persistentListeners = listeners;
 | |
| 
 | |
|     let persistentListeners = extension.startupData?.persistentListeners;
 | |
|     if (!persistentListeners) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     let found = false;
 | |
|     for (let [module, savedModuleEntry] of Object.entries(
 | |
|       persistentListeners
 | |
|     )) {
 | |
|       for (let [event, savedEventEntry] of Object.entries(savedModuleEntry)) {
 | |
|         for (let paramList of savedEventEntry) {
 | |
|           /* Before Bug 1795801 (Firefox < 113) each entry was related to a listener
 | |
|            * registered with a different set of extra params (and so only one listener
 | |
|            * could be persisted for the same set of extra params)
 | |
|            *
 | |
|            * After Bug 1795801 (Firefox >= 113) each entry still represents a listener
 | |
|            * registered for that event, but multiple listeners registered with the same
 | |
|            * set of extra params will be captured as multiple entries in the
 | |
|            * paramsList array.
 | |
|            *
 | |
|            * NOTE: persisted listeners are stored in the startupData part of the Addon DB
 | |
|            * and are expected to be preserved across Firefox and Addons upgrades and downgrades
 | |
|            * (unlike the WebExtensions startupCache data which is cleared when Firefox or the
 | |
|            * addon is updated) and so we are taking special care about forward and backward
 | |
|            * compatibility of the persistentListeners on-disk format:
 | |
|            *
 | |
|            * - forward compatibility: when this new version of this startupData loading logic
 | |
|            *   is loading the old persistentListeners on-disk format:
 | |
|            *   - on the first run only one listener will be primed for each of the extra params
 | |
|            *     recorded in the startupData (same as in older Firefox versions)
 | |
|            *     and Bug 1795801 will still be hit, but once the background
 | |
|            *     context is started once the startupData will be updated to
 | |
|            *     include each of the listeners (indipendently if the set of
 | |
|            *     extra params is the same as another listener already been
 | |
|            *     persisted).
 | |
|            *   - after the first run, all listeners will be primed separately, even if the extra
 | |
|            *     params are the same as other listeners already primed, and so
 | |
|            *     each of the listener will receive the pending events collected
 | |
|            *     by their related primed listener and Bug 1795801 not to be hit anymore.
 | |
|            *
 | |
|            * - backward compatibility: when the old version of this startupData loading logic
 | |
|            *   (https://searchfox.org/mozilla-central/rev/cd2121e7d8/toolkit/components/extensions/ExtensionCommon.jsm#2360-2371)
 | |
|            *   is loading the new persistentListeners on-disk format, the last
 | |
|            *   entry with the same set of extra params will be eventually overwritting the
 | |
|            *   entry for another primed listener with the same extra params, Bug 1795801 will still
 | |
|            *   be hit, but no actual change in behavior is expected.
 | |
|            */
 | |
|           let key = uneval(paramList);
 | |
|           const eventEntry = listeners.get(module).get(event);
 | |
| 
 | |
|           if (eventEntry.has(key)) {
 | |
|             const keyEntry = eventEntry.get(key);
 | |
|             let primeId = keyEntry.nextPrimeId;
 | |
|             keyEntry.listeners.push({ primeId });
 | |
|             keyEntry.nextPrimeId++;
 | |
|           } else {
 | |
|             eventEntry.set(key, {
 | |
|               params: paramList,
 | |
|               nextPrimeId: 1,
 | |
|               listeners: [{ primeId: 0 }],
 | |
|             });
 | |
|           }
 | |
|           found = true;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return found;
 | |
|   }
 | |
| 
 | |
|   // Extract just the information needed at startup for all persistent
 | |
|   // listeners, and arrange for it to be saved.  This should be called
 | |
|   // whenever the set of persistent listeners for an extension changes.
 | |
|   static _writePersistentListeners(extension) {
 | |
|     let startupListeners = {};
 | |
|     for (let [module, moduleEntry] of extension.persistentListeners) {
 | |
|       startupListeners[module] = {};
 | |
|       for (let [event, eventEntry] of moduleEntry) {
 | |
|         // Turn the per-event entries from the format they are being kept
 | |
|         // in memory:
 | |
|         //
 | |
|         //   [
 | |
|         //     { params: paramList1, listeners: [listener1, listener2, ...] },
 | |
|         //     { params: paramList2, listeners: [listener3, listener3, ...] },
 | |
|         //     ...
 | |
|         //   ]
 | |
|         //
 | |
|         // into the format used for storing them on disk (in the startupData),
 | |
|         // which is an array of the params for each listener (with the param list
 | |
|         // included as many times as many listeners are persisted for the same
 | |
|         // set of params):
 | |
|         //
 | |
|         //   [paramList1, paramList1, ..., paramList2, paramList2, ...]
 | |
|         //
 | |
|         // This format will also work as expected on older Firefox versions where
 | |
|         // only one listener was being persisted for each set of params.
 | |
|         startupListeners[module][event] = Array.from(
 | |
|           eventEntry.values()
 | |
|         ).flatMap(keyEntry => keyEntry.listeners.map(() => keyEntry.params));
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     extension.startupData.persistentListeners = startupListeners;
 | |
|     extension.saveStartupData();
 | |
|   }
 | |
| 
 | |
|   // Set up "primed" event listeners for any saved event listeners
 | |
|   // in an extension's startup data.
 | |
|   // This function is only called during browser startup, it stores details
 | |
|   // about all primed listeners in the extension's persistentListeners Map.
 | |
|   static primeListeners(extension, isInStartup = false) {
 | |
|     if (!EventManager._initPersistentListeners(extension)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     for (let [module, moduleEntry] of extension.persistentListeners) {
 | |
|       // If we're in startup, we only want to continue attempting to prime a
 | |
|       // subset of events that should be startup blocking.
 | |
|       if (isInStartup) {
 | |
|         let api_module = extension.apiManager.getModule(module);
 | |
|         if (!api_module.startupBlocking) {
 | |
|           continue;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       let api = extension.apiManager.getAPI(module, extension, "addon_parent");
 | |
| 
 | |
|       // If an extension is upgraded and a permission, such as webRequest, is
 | |
|       // removed, we will have been called but the API is no longer available.
 | |
|       if (!api?.primeListener) {
 | |
|         // The runtime module no longer implements primed listeners, drop them.
 | |
|         extension.persistentListeners.delete(module);
 | |
|         EventManager._writePersistentListeners(extension);
 | |
|         continue;
 | |
|       }
 | |
|       for (let [event, eventEntry] of moduleEntry) {
 | |
|         for (let [key, { params, listeners }] of eventEntry) {
 | |
|           for (let listener of listeners) {
 | |
|             // Reset the `listener.added` flag by setting it to `false` while
 | |
|             // re-priming the listeners because the event page has suspended
 | |
|             // and the previous converted listener is no longer listening.
 | |
|             const listenerWasAdded = listener.added;
 | |
|             listener.added = false;
 | |
|             listener.params = params;
 | |
|             let primed = { pendingEvents: [] };
 | |
| 
 | |
|             let fireEvent = (...args) =>
 | |
|               new Promise((resolve, reject) => {
 | |
|                 if (!listener.primed) {
 | |
|                   reject(
 | |
|                     new Error(
 | |
|                       `primed listener ${module}.${event} not re-registered`
 | |
|                     )
 | |
|                   );
 | |
|                   return;
 | |
|                 }
 | |
|                 primed.pendingEvents.push({ args, resolve, reject });
 | |
|                 extension.emit("background-script-event");
 | |
|               });
 | |
| 
 | |
|             let fire = {
 | |
|               wakeup: () => extension.wakeupBackground(),
 | |
|               sync: fireEvent,
 | |
|               async: fireEvent,
 | |
|               // fire.async for ProxyContextParent is already not cloning.
 | |
|               raw: fireEvent,
 | |
|             };
 | |
| 
 | |
|             try {
 | |
|               let handler = api.primeListener(
 | |
|                 event,
 | |
|                 fire,
 | |
|                 listener.params,
 | |
|                 isInStartup
 | |
|               );
 | |
|               if (handler) {
 | |
|                 listener.primed = primed;
 | |
|                 Object.assign(primed, handler);
 | |
|               }
 | |
|             } catch (e) {
 | |
|               Cu.reportError(
 | |
|                 `Error priming listener ${module}.${event}: ${e} :: ${e.stack}`
 | |
|               );
 | |
|               // Force this listener to be cleared.
 | |
|               listener.error = true;
 | |
|             }
 | |
| 
 | |
|             // If an attempt to prime a listener failed, ensure it is cleared now.
 | |
|             // If a module is a startup blocking module, not all listeners may
 | |
|             // get primed during early startup.  For that reason, we don't clear
 | |
|             // persisted listeners during early startup.  At the end of background
 | |
|             // execution any listeners that were not renewed will be cleared.
 | |
|             //
 | |
|             // TODO(Bug 1797474): consider priming runtime.onStartup and
 | |
|             // avoid to special handling it here.
 | |
|             if (
 | |
|               listener.error ||
 | |
|               (!isInStartup &&
 | |
|                 !(
 | |
|                   (`${module}.${event}` === "runtime.onStartup" &&
 | |
|                     listenerWasAdded) ||
 | |
|                   listener.primed
 | |
|                 ))
 | |
|             ) {
 | |
|               EventManager.clearPersistentListener(
 | |
|                 extension,
 | |
|                 module,
 | |
|                 event,
 | |
|                 key,
 | |
|                 listener.primeId
 | |
|               );
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * This is called as a result of background script startup-finished and shutdown.
 | |
|    *
 | |
|    * After startup, it removes any remaining primed listeners.  These exist if the
 | |
|    * listener was not renewed during startup.  In this case the persisted listener
 | |
|    * data is also removed.
 | |
|    *
 | |
|    * During shutdown, care should be taken to set clearPersistent to false.
 | |
|    * persisted listener data should NOT be cleared during shutdown.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    * @param {boolean} clearPersistent whether the persisted listener data should be cleared.
 | |
|    */
 | |
|   static clearPrimedListeners(extension, clearPersistent = true) {
 | |
|     if (!extension.persistentListeners) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     for (let [module, moduleEntry] of extension.persistentListeners) {
 | |
|       for (let [event, eventEntry] of moduleEntry) {
 | |
|         for (let [key, { listeners }] of eventEntry) {
 | |
|           for (let listener of listeners) {
 | |
|             let { primed, added, primeId } = listener;
 | |
|             // When a primed listener is added or renewed during initial
 | |
|             // background execution we set an added flag.  If it was primed
 | |
|             // when added, primed is set to null.
 | |
|             if (added) {
 | |
|               continue;
 | |
|             }
 | |
| 
 | |
|             if (primed) {
 | |
|               // When a primed listener was not renewed, primed will still be truthy.
 | |
|               // These need to be cleared on shutdown (important for event pages), but
 | |
|               // we only clear the persisted listener data after the startup of a background.
 | |
|               // Release any pending events and unregister the primed handler.
 | |
|               listener.primed = null;
 | |
| 
 | |
|               for (let evt of primed.pendingEvents) {
 | |
|                 evt.reject(new Error("listener not re-registered"));
 | |
|               }
 | |
|               primed.unregister();
 | |
|             }
 | |
| 
 | |
|             // Clear any persisted events that were not renewed, should typically
 | |
|             // only be done at the end of the background page load.
 | |
|             if (clearPersistent) {
 | |
|               EventManager.clearPersistentListener(
 | |
|                 extension,
 | |
|                 module,
 | |
|                 event,
 | |
|                 key,
 | |
|                 primeId
 | |
|               );
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Record the fact that there is a listener for the given event in
 | |
|   // the given extension.  `args` is an Array containing any extra
 | |
|   // arguments that were passed to addListener().
 | |
|   static savePersistentListener(extension, module, event, args = []) {
 | |
|     EventManager._initPersistentListeners(extension);
 | |
|     let key = uneval(args);
 | |
|     const eventEntry = extension.persistentListeners.get(module).get(event);
 | |
| 
 | |
|     let primeId;
 | |
|     if (!eventEntry.has(key)) {
 | |
|       // when writing, only args are written, other properties are dropped
 | |
|       primeId = 0;
 | |
|       eventEntry.set(key, {
 | |
|         params: args,
 | |
|         listeners: [{ added: true, primeId }],
 | |
|         nextPrimeId: 1,
 | |
|       });
 | |
|     } else {
 | |
|       const keyEntry = eventEntry.get(key);
 | |
|       primeId = keyEntry.nextPrimeId;
 | |
|       keyEntry.listeners.push({ added: true, primeId });
 | |
|       keyEntry.nextPrimeId = primeId + 1;
 | |
|     }
 | |
| 
 | |
|     EventManager._writePersistentListeners(extension);
 | |
|     return [module, event, key, primeId];
 | |
|   }
 | |
| 
 | |
|   // Remove the record for the given event listener from the extension's
 | |
|   // startup data.  `key` must be a string, the result of calling uneval()
 | |
|   // on the array of extra arguments originally passed to addListener().
 | |
|   static clearPersistentListener(
 | |
|     extension,
 | |
|     module,
 | |
|     event,
 | |
|     key = uneval([]),
 | |
|     primeId = undefined
 | |
|   ) {
 | |
|     let eventEntry = extension.persistentListeners.get(module).get(event);
 | |
| 
 | |
|     let keyEntry = eventEntry.get(key);
 | |
| 
 | |
|     if (primeId != undefined && keyEntry) {
 | |
|       keyEntry.listeners = keyEntry.listeners.filter(
 | |
|         listener => listener.primeId !== primeId
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (primeId == undefined || keyEntry?.listeners.length === 0) {
 | |
|       eventEntry.delete(key);
 | |
|       if (eventEntry.size == 0) {
 | |
|         let moduleEntry = extension.persistentListeners.get(module);
 | |
|         moduleEntry.delete(event);
 | |
|         if (moduleEntry.size == 0) {
 | |
|           extension.persistentListeners.delete(module);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     EventManager._writePersistentListeners(extension);
 | |
|   }
 | |
| 
 | |
|   addListener(callback, ...args) {
 | |
|     if (this.unregister.has(callback)) {
 | |
|       return;
 | |
|     }
 | |
|     this.context.logActivity("api_call", `${this.name}.addListener`, { args });
 | |
| 
 | |
|     let shouldFire = () => {
 | |
|       if (this.context.unloaded) {
 | |
|         dump(`${this.name} event fired after context unloaded.\n`);
 | |
|       } else if (!this.context.active) {
 | |
|         dump(`${this.name} event fired while context is inactive.\n`);
 | |
|       } else if (this.unregister.has(callback)) {
 | |
|         return true;
 | |
|       }
 | |
|       return false;
 | |
|     };
 | |
| 
 | |
|     let { extension } = this.context;
 | |
|     const resetIdle = () => {
 | |
|       if (this.resetIdleOnEvent) {
 | |
|         extension?.emit("background-script-reset-idle", {
 | |
|           reason: "event",
 | |
|           eventName: this.name,
 | |
|         });
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     let fire = {
 | |
|       // Bug 1754866 fire.sync doesn't match documentation.
 | |
|       sync: (...args) => {
 | |
|         if (shouldFire()) {
 | |
|           resetIdle();
 | |
|           let result = this.context.applySafe(callback, args);
 | |
|           this.context.logActivity("api_event", this.name, { args, result });
 | |
|           return result;
 | |
|         }
 | |
|       },
 | |
|       async: (...args) => {
 | |
|         return Promise.resolve().then(() => {
 | |
|           if (shouldFire()) {
 | |
|             resetIdle();
 | |
|             let result = this.context.applySafe(callback, args);
 | |
|             this.context.logActivity("api_event", this.name, { args, result });
 | |
|             return result;
 | |
|           }
 | |
|         });
 | |
|       },
 | |
|       raw: (...args) => {
 | |
|         if (!shouldFire()) {
 | |
|           throw new Error("Called raw() on unloaded/inactive context");
 | |
|         }
 | |
|         resetIdle();
 | |
|         let result = Reflect.apply(callback, null, args);
 | |
|         this.context.logActivity("api_event", this.name, { args, result });
 | |
|         return result;
 | |
|       },
 | |
|       asyncWithoutClone: (...args) => {
 | |
|         return Promise.resolve().then(() => {
 | |
|           if (shouldFire()) {
 | |
|             resetIdle();
 | |
|             let result = this.context.applySafeWithoutClone(callback, args);
 | |
|             this.context.logActivity("api_event", this.name, { args, result });
 | |
|             return result;
 | |
|           }
 | |
|         });
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     let { module, event } = this;
 | |
| 
 | |
|     let unregister = null;
 | |
|     let recordStartupData = false;
 | |
| 
 | |
|     // If this is a persistent event, check for a listener that was already
 | |
|     // created during startup.  If there is one, use it and don't create a
 | |
|     // new one.
 | |
|     if (this.canPersistEvents) {
 | |
|       // Once a background is started, listenerPromises is set to null. At
 | |
|       // that point, we stop recording startup data.
 | |
|       recordStartupData = !!this.context.listenerPromises;
 | |
| 
 | |
|       let key = uneval(args);
 | |
|       EventManager._initPersistentListeners(extension);
 | |
|       let keyEntry = extension.persistentListeners
 | |
|         .get(module)
 | |
|         .get(event)
 | |
|         .get(key);
 | |
| 
 | |
|       // Get the first persistent listener which matches the module, event and extra arguments
 | |
|       // and not added back by the extension yet, the persistent listener found may be either
 | |
|       // primed or not (in particular API Events that belongs to APIs that should not be blocking
 | |
|       // startup may have persistent listeners that are not primed during the first execution
 | |
|       // of the background context happening as part of the applications startup, whereas they
 | |
|       // will be primed when the background context will be suspended on the idle timeout).
 | |
|       let listener = keyEntry?.listeners.find(listener => !listener.added);
 | |
|       if (listener) {
 | |
|         // During startup only a subset of persisted listeners are primed.  As
 | |
|         // well, each API determines whether to prime a specific listener.
 | |
|         let { primed } = listener;
 | |
|         if (primed) {
 | |
|           listener.primed = null;
 | |
| 
 | |
|           primed.convert(fire, this.context);
 | |
|           unregister = primed.unregister;
 | |
| 
 | |
|           for (let evt of primed.pendingEvents) {
 | |
|             evt.resolve(fire.async(...evt.args));
 | |
|           }
 | |
|         }
 | |
|         listener.added = true;
 | |
| 
 | |
|         recordStartupData = false;
 | |
|         this.remove.set(callback, () => {
 | |
|           EventManager.clearPersistentListener(
 | |
|             extension,
 | |
|             module,
 | |
|             event,
 | |
|             uneval(args),
 | |
|             listener.primeId
 | |
|           );
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!unregister) {
 | |
|       unregister = this.register(fire, ...args);
 | |
|     }
 | |
| 
 | |
|     this.unregister.set(callback, unregister);
 | |
|     this.context.callOnClose(this);
 | |
| 
 | |
|     // If this is a new listener for a persistent event, record
 | |
|     // the details for subsequent startups.
 | |
|     if (recordStartupData) {
 | |
|       const [, , , /* _module */ /* _event */ /* _key */ primeId] =
 | |
|         EventManager.savePersistentListener(extension, module, event, args);
 | |
|       this.remove.set(callback, () => {
 | |
|         EventManager.clearPersistentListener(
 | |
|           extension,
 | |
|           module,
 | |
|           event,
 | |
|           uneval(args),
 | |
|           primeId
 | |
|         );
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   removeListener(callback, clearPersistentListener = true) {
 | |
|     if (!this.unregister.has(callback)) {
 | |
|       return;
 | |
|     }
 | |
|     this.context.logActivity("api_call", `${this.name}.removeListener`, {
 | |
|       args: [],
 | |
|     });
 | |
| 
 | |
|     let unregister = this.unregister.get(callback);
 | |
|     this.unregister.delete(callback);
 | |
|     try {
 | |
|       unregister();
 | |
|     } catch (e) {
 | |
|       Cu.reportError(e);
 | |
|     }
 | |
| 
 | |
|     if (clearPersistentListener && this.remove.has(callback)) {
 | |
|       let cleanup = this.remove.get(callback);
 | |
|       this.remove.delete(callback);
 | |
|       cleanup();
 | |
|     }
 | |
| 
 | |
|     if (this.unregister.size == 0) {
 | |
|       this.context.forgetOnClose(this);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   hasListener(callback) {
 | |
|     return this.unregister.has(callback);
 | |
|   }
 | |
| 
 | |
|   revoke() {
 | |
|     for (let callback of this.unregister.keys()) {
 | |
|       this.removeListener(callback, false);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   close() {
 | |
|     this.revoke();
 | |
|   }
 | |
| 
 | |
|   api() {
 | |
|     return {
 | |
|       addListener: (...args) => this.addListener(...args),
 | |
|       removeListener: (...args) => this.removeListener(...args),
 | |
|       hasListener: (...args) => this.hasListener(...args),
 | |
|       setUserInput: this.inputHandling,
 | |
|       [lazy.Schemas.REVOKE]: () => this.revoke(),
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Simple API for event listeners where events never fire.
 | |
| function ignoreEvent(context, name) {
 | |
|   return {
 | |
|     addListener: function (callback) {
 | |
|       let id = context.extension.id;
 | |
|       let frame = Components.stack.caller;
 | |
|       let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
 | |
|       let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
 | |
|         Ci.nsIScriptError
 | |
|       );
 | |
|       scriptError.init(
 | |
|         msg,
 | |
|         frame.filename,
 | |
|         null,
 | |
|         frame.lineNumber,
 | |
|         frame.columnNumber,
 | |
|         Ci.nsIScriptError.warningFlag,
 | |
|         "content javascript"
 | |
|       );
 | |
|       Services.console.logMessage(scriptError);
 | |
|     },
 | |
|     removeListener: function (callback) {},
 | |
|     hasListener: function (callback) {},
 | |
|   };
 | |
| }
 | |
| 
 | |
| const stylesheetMap = new DefaultMap(url => {
 | |
|   let uri = Services.io.newURI(url);
 | |
|   return lazy.styleSheetService.preloadSheet(
 | |
|     uri,
 | |
|     lazy.styleSheetService.AGENT_SHEET
 | |
|   );
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Updates the in-memory representation of extension host permissions, i.e.
 | |
|  * policy.allowedOrigins.
 | |
|  *
 | |
|  * @param {WebExtensionPolicy} policy
 | |
|  *        A policy. All MatchPattern instances in policy.allowedOrigins are
 | |
|  *        expected to have been constructed with ignorePath: true.
 | |
|  * @param {string[]} origins
 | |
|  *        A list of already-normalized origins, equivalent to using the
 | |
|  *        MatchPattern constructor with ignorePath: true.
 | |
|  * @param {boolean} isAdd
 | |
|  *        Whether to add instead of removing the host permissions.
 | |
|  */
 | |
| function updateAllowedOrigins(policy, origins, isAdd) {
 | |
|   if (!origins.length) {
 | |
|     // Nothing to modify.
 | |
|     return;
 | |
|   }
 | |
|   let patternMap = new Map();
 | |
|   for (let pattern of policy.allowedOrigins.patterns) {
 | |
|     patternMap.set(pattern.pattern, pattern);
 | |
|   }
 | |
|   if (!isAdd) {
 | |
|     for (let origin of origins) {
 | |
|       patternMap.delete(origin);
 | |
|     }
 | |
|   } else {
 | |
|     // In the parent process, policy.extension.restrictSchemes is available.
 | |
|     // In the content process, we need to check the mozillaAddons permission,
 | |
|     // which is only available if approved by the parent.
 | |
|     const restrictSchemes =
 | |
|       policy.extension?.restrictSchemes ??
 | |
|       policy.hasPermission("mozillaAddons");
 | |
|     for (let origin of origins) {
 | |
|       if (patternMap.has(origin)) {
 | |
|         continue;
 | |
|       }
 | |
|       patternMap.set(
 | |
|         origin,
 | |
|         new MatchPattern(origin, { restrictSchemes, ignorePath: true })
 | |
|       );
 | |
|     }
 | |
|   }
 | |
|   // patternMap contains only MatchPattern instances, so we don't need to set
 | |
|   // the options parameter (with restrictSchemes, etc.) since that is only used
 | |
|   // if the input is a string.
 | |
|   policy.allowedOrigins = new MatchPatternSet(Array.from(patternMap.values()));
 | |
| }
 | |
| 
 | |
| export var ExtensionCommon = {
 | |
|   BaseContext,
 | |
|   CanOfAPIs,
 | |
|   EventManager,
 | |
|   ExtensionAPI,
 | |
|   ExtensionAPIPersistent,
 | |
|   EventEmitter,
 | |
|   LocalAPIImplementation,
 | |
|   LocaleData,
 | |
|   NoCloneSpreadArgs,
 | |
|   SchemaAPIInterface,
 | |
|   SchemaAPIManager,
 | |
|   SpreadArgs,
 | |
|   checkLoadURI,
 | |
|   checkLoadURL,
 | |
|   defineLazyGetter,
 | |
|   redefineGetter,
 | |
|   getConsole,
 | |
|   ignoreEvent,
 | |
|   instanceOf,
 | |
|   makeWidgetId,
 | |
|   normalizeTime,
 | |
|   runSafeSyncWithoutClone,
 | |
|   stylesheetMap,
 | |
|   updateAllowedOrigins,
 | |
|   withHandlingUserInput,
 | |
| 
 | |
|   MultiAPIManager,
 | |
|   LazyAPIManager,
 | |
| };
 | 
