1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 * This module contains utilities and base classes for logic which is
9 * common between the parent and child process, and in particular
10 * between ExtensionParent.jsm and ExtensionChild.jsm.
13 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
15 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
19 ChromeUtils.defineESModuleGetters(lazy, {
20 ConsoleAPI: "resource://gre/modules/Console.sys.mjs",
21 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
22 SchemaRoot: "resource://gre/modules/Schemas.sys.mjs",
23 Schemas: "resource://gre/modules/Schemas.sys.mjs",
26 XPCOMUtils.defineLazyServiceGetter(
29 "@mozilla.org/content/style-sheet-service;1",
30 "nsIStyleSheetService"
33 const ScriptError = Components.Constructor(
34 "@mozilla.org/scripterror;1",
39 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
50 function getConsole() {
51 return new lazy.ConsoleAPI({
52 maxLogLevelPref: "extensions.webextensions.log.level",
53 prefix: "WebExtensions",
57 // Run a function and report exceptions.
58 function runSafeSyncWithoutClone(f, ...args) {
62 // This method is called with `this` unbound and it doesn't have
63 // access to a BaseContext instance and so we can't check if `e`
64 // is an instance of the extension context's Error constructor
65 // (like we do in BaseContext applySafeWithoutClone method).
67 `Extension error: ${e} ${e?.fileName} ${
69 }\n[[Exception stack\n${
70 e?.stack ? filterStack(e) : undefined
71 }Current stack\n${filterStack(Error())}]]\n`
77 // Return true if the given value is an instance of the given
79 function instanceOf(value, type) {
82 typeof value === "object" &&
83 ChromeUtils.getClassName(value) === type
88 * Convert any of several different representations of a date/time to a Date object.
89 * Accepts several formats:
90 * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
91 * either a number or a string.
93 * @param {Date|string|number} date
94 * The date to convert.
98 function normalizeTime(date) {
99 // Of all the formats we accept the "number of milliseconds since the epoch as a string"
100 // is an outlier, everything else can just be passed directly to the Date constructor.
102 typeof date == "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date
106 function withHandlingUserInput(window, callable) {
107 let handle = window.windowUtils.setHandlingUserInput(true);
116 * Defines a lazy getter for the given property on the given object. The
117 * first time the property is accessed, the return value of the getter
118 * is defined on the current `this` object with the given property name.
119 * Importantly, this means that a lazy getter defined on an object
120 * prototype will be invoked separately for each object instance that
123 * Note: for better type inference, prefer redefineGetter() below.
125 * @param {object} object
126 * The prototype object on which to define the getter.
127 * @param {string | symbol} prop
128 * The property name for which to define the getter.
129 * @param {callback} getter
130 * The function to call in order to generate the final property
133 function defineLazyGetter(object, prop, getter) {
134 Object.defineProperty(object, prop, {
138 return redefineGetter(this, prop, getter.call(this), true);
141 redefineGetter(this, prop, value, true);
147 * A more type-inference friendly version of defineLazyGetter() above.
148 * Call it from a real getter (and setter) for your class or object.
149 * On first run, it will redefine the property with the final value.
152 * @param {object} object
153 * @param {string | symbol} key
154 * @param {Value} value
157 function redefineGetter(object, key, value, writable = false) {
158 Object.defineProperty(object, key, {
167 function checkLoadURI(uri, principal, options) {
168 let ssm = Services.scriptSecurityManager;
170 let flags = ssm.STANDARD;
171 if (!options.allowScript) {
172 flags |= ssm.DISALLOW_SCRIPT;
174 if (!options.allowInheritsPrincipal) {
175 flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
177 if (options.dontReportErrors) {
178 flags |= ssm.DONT_REPORT_ERRORS;
182 ssm.checkLoadURIWithPrincipal(principal, uri, flags);
189 function checkLoadURL(url, principal, options) {
191 return checkLoadURI(Services.io.newURI(url), principal, options);
193 return false; // newURI threw.
197 function makeWidgetId(id) {
198 id = id.toLowerCase();
199 // FIXME: This allows for collisions.
200 return id.replace(/[^a-z0-9_-]/g, "_");
204 * A sentinel class to indicate that an array of values should be
205 * treated as an array when used as a promise resolution value, but as a
206 * spread expression (...args) when passed to a callback.
208 class SpreadArgs extends Array {
216 * Like SpreadArgs, but also indicates that the array values already
217 * belong to the target compartment, and should not be cloned before
220 * The `unwrappedValues` property contains an Array object which belongs
221 * to the target compartment, and contains the same unwrapped values
222 * passed the NoCloneSpreadArgs constructor.
224 class NoCloneSpreadArgs {
226 this.unwrappedValues = args;
229 [Symbol.iterator]() {
230 return this.unwrappedValues[Symbol.iterator]();
234 const LISTENERS = Symbol("listeners");
235 const ONCE_MAP = Symbol("onceMap");
239 this[LISTENERS] = new Map();
240 this[ONCE_MAP] = new WeakMap();
244 * Checks whether there is some listener for the given event.
246 * @param {string} event
247 * The name of the event to listen for.
251 return this[LISTENERS].has(event);
255 * Adds the given function as a listener for the given event.
257 * The listener function may optionally return a Promise which
258 * resolves when it has completed all operations which event
259 * dispatchers may need to block on.
261 * @param {string} event
262 * The name of the event to listen for.
263 * @param {function(string, ...any): any} listener
264 * The listener to call when events are emitted.
266 on(event, listener) {
267 let listeners = this[LISTENERS].get(event);
269 listeners = new Set();
270 this[LISTENERS].set(event, listeners);
273 listeners.add(listener);
277 * Removes the given function as a listener for the given event.
279 * @param {string} event
280 * The name of the event to stop listening for.
281 * @param {function(string, ...any): any} listener
282 * The listener function to remove.
284 off(event, listener) {
285 let set = this[LISTENERS].get(event);
287 set.delete(listener);
288 set.delete(this[ONCE_MAP].get(listener));
290 this[LISTENERS].delete(event);
296 * Adds the given function as a listener for the given event once.
298 * @param {string} event
299 * The name of the event to listen for.
300 * @param {function(string, ...any): any} listener
301 * The listener to call when events are emitted.
303 once(event, listener) {
304 let wrapper = (event, ...args) => {
305 this.off(event, wrapper);
306 this[ONCE_MAP].delete(listener);
308 return listener(event, ...args);
310 this[ONCE_MAP].set(listener, wrapper);
312 this.on(event, wrapper);
316 * Triggers all listeners for the given event. If any listeners return
317 * a value, returns a promise which resolves when all returned
318 * promises have resolved. Otherwise, returns undefined.
320 * @param {string} event
321 * The name of the event to emit.
323 * Arbitrary arguments to pass to the listener functions, after
325 * @returns {Promise?}
327 emit(event, ...args) {
328 let listeners = this[LISTENERS].get(event);
333 for (let listener of listeners) {
335 let result = listener(event, ...args);
336 if (result !== undefined) {
337 promises.push(result);
344 if (promises.length) {
345 return Promise.all(promises);
352 * Base class for WebExtension APIs. Each API creates a new class
353 * that inherits from this class, the derived class is instantiated
354 * once for each extension that uses the API.
356 class ExtensionAPI extends EventEmitter {
357 constructor(extension) {
360 this.extension = extension;
362 extension.once("shutdown", (what, isAppShutdown) => {
363 if (this.onShutdown) {
364 this.onShutdown(isAppShutdown);
366 this.extension = null;
372 /** @param {string} entryName */
373 onManifestEntry(entryName) {}
375 /** @param {boolean} isAppShutdown */
376 onShutdown(isAppShutdown) {}
378 /** @param {BaseContext} context */
380 throw new Error("Not Implemented");
383 /** @param {string} id */
384 static onDisable(id) {}
386 /** @param {string} id */
387 static onUninstall(id) {}
391 * @param {Record<string, JSONValue>} manifest
393 static onUpdate(id, manifest) {}
397 * Subclass to add APIs commonly used with persistent events.
398 * If a namespace uses events, it should use this subclass.
400 * this.apiNamespace = class extends ExtensionAPIPersistent {};
402 class ExtensionAPIPersistent extends ExtensionAPI {
403 /** @type {Record<string, callback>} */
407 * Check for event entry.
409 * @param {string} event The event name e.g. onStateChanged
412 hasEventRegistrar(event) {
414 this.PERSISTENT_EVENTS && Object.hasOwn(this.PERSISTENT_EVENTS, event)
419 * Get the event registration fuction
421 * @param {string} event The event name e.g. onStateChanged
422 * @returns {Function} register is used to start the listener
423 * register returns an object containing
424 * a convert and unregister function.
426 getEventRegistrar(event) {
427 if (this.hasEventRegistrar(event)) {
428 return this.PERSISTENT_EVENTS[event].bind(this);
433 * Used when instantiating an EventManager instance to register the listener.
435 * @param {object} options Options used for event registration
436 * @param {BaseContext} options.context Extension Context passed when creating an EventManager instance.
437 * @param {string} options.event The eAPI vent name.
438 * @param {Function} options.fire The function passed to the listener to fire the event.
439 * @param {Array<any>} params An optional array of parameters received along with the
440 * addListener request.
441 * @returns {Function} The unregister function used in the EventManager.
443 registerEventListener(options, params) {
444 const apiRegistar = this.getEventRegistrar(options.event);
445 return apiRegistar?.(options, params).unregister;
449 * Used to prime a listener for when the background script is not running.
451 * @param {string} event The event name e.g. onStateChanged or captiveURL.onChange.
452 * @param {Function} fire The function passed to the listener to fire the event.
453 * @param {Array} params Params passed to the event listener.
454 * @param {boolean} isInStartup unused here but passed for subclass use.
455 * @returns {object} the unregister and convert functions used in the EventManager.
457 primeListener(event, fire, params, isInStartup) {
458 const apiRegistar = this.getEventRegistrar(event);
459 return apiRegistar?.({ fire, isInStartup }, params);
464 * This class contains the information we have about an individual
465 * extension. It is never instantiated directly, instead subclasses
466 * for each type of process extend this class and add members that are
467 * relevant for that process.
472 /** @type {boolean} */
474 /** @type {string} */
477 constructor(envType, extension) {
478 this.envType = envType;
479 this.onClose = new Set();
480 this.checkedLastError = false;
481 this._lastError = null;
482 this.contextId = getUniqueId();
483 this.unloaded = false;
484 this.extension = extension;
485 this.manifestVersion = extension.manifestVersion;
486 this.jsonSandbox = null;
488 this.incognito = null;
489 this.messageManager = null;
490 this.contentWindow = null;
491 this.innerWindowID = 0;
493 // These two properties are assigned in ContentScriptContextChild subclass
494 // to keep a copy of the content script sandbox Error and Promise globals
495 // (which are used by the WebExtensions internals) before any extension
496 // content script code had any chance to redefine them.
497 this.cloneScopeError = null;
498 this.cloneScopePromise = null;
501 get isProxyContextParent() {
506 // Return the copy stored in the context instance (when the context is an instance of
507 // ContentScriptContextChild or the global from extension page window otherwise).
508 return this.cloneScopeError || this.cloneScope.Error;
512 // Return the copy stored in the context instance (when the context is an instance of
513 // ContentScriptContextChild or the global from extension page window otherwise).
514 return this.cloneScopePromise || this.cloneScope.Promise;
517 get privateBrowsingAllowed() {
518 return this.extension.privateBrowsingAllowed;
521 get isBackgroundContext() {
522 if (this.viewType === "background") {
523 if (this.isProxyContextParent) {
524 return !!this.isTopContext; // Set in ExtensionPageContextParent.
526 const { contentWindow } = this;
527 return !!contentWindow && contentWindow.top === contentWindow;
529 return this.viewType === "background_worker";
533 * Whether the extension context is using the WebIDL bindings for the
534 * WebExtensions APIs.
535 * To be overridden in subclasses (e.g. WorkerContextChild) and to be
536 * optionally used in ExtensionAPI classes to customize the behavior of the
537 * API when the calls to the extension API are originated from the WebIDL
540 get useWebIDLBindings() {
544 canAccessWindow(window) {
545 return this.extension.canAccessWindow(window);
548 canAccessContainer(userContextId) {
549 return this.extension.canAccessContainer(userContextId);
553 * Opens a conduit linked to this context, populating related address fields.
554 * Only available in child contexts with an associated contentWindow.
556 * @param {object} subject
557 * @param {ConduitAddress} address
558 * @returns {import("ConduitsChild.sys.mjs").PointConduit}
559 * @type {ConduitOpen}
561 openConduit(subject, address) {
562 let wgc = this.contentWindow.windowGlobalChild;
563 let conduit = wgc.getActor("Conduits").openConduit(subject, {
564 id: subject.id || getUniqueId(),
565 extensionId: this.extension.id,
566 envType: this.envType,
569 this.callOnClose(conduit);
570 conduit.setCloseCallback(() => {
571 this.forgetOnClose(conduit);
576 setContentWindow(contentWindow) {
577 if (!this.canAccessWindow(contentWindow)) {
579 "BaseContext attempted to load when extension is not allowed due to incognito settings."
583 this.innerWindowID = getInnerWindowID(contentWindow);
584 this.messageManager = contentWindow.docShell.messageManager;
586 if (this.incognito == null) {
588 lazy.PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
591 let wgc = contentWindow.windowGlobalChild;
592 Object.defineProperty(this, "active", {
595 get: () => wgc.isCurrentGlobal && !wgc.windowContext.isInBFCache,
597 Object.defineProperty(this, "contentWindow", {
600 get: () => (this.active ? wgc.browsingContext.window : null),
604 // Allow other "close" handlers to use these properties, until the next tick.
605 Promise.resolve().then(() => {
606 Object.defineProperty(this, "contentWindow", { value: null });
607 Object.defineProperty(this, "active", { value: false });
614 // All child contexts must implement logActivity. This is handled if the child
615 // context subclasses ExtensionBaseContextChild. ProxyContextParent overrides
616 // this with a noop for parent contexts.
617 logActivity(type, name, data) {
618 throw new Error(`Not implemented for ${this.envType}`);
621 /** @type {object} */
623 throw new Error("Not implemented");
626 /** @type {nsIPrincipal} */
628 throw new Error("Not implemented");
631 runSafe(callback, ...args) {
632 return this.applySafe(callback, args);
635 runSafeWithoutClone(callback, ...args) {
636 return this.applySafeWithoutClone(callback, args);
639 applySafe(callback, args, caller) {
641 Cu.reportError("context.runSafe called after context unloaded", caller);
642 } else if (!this.active) {
644 "context.runSafe called while context is inactive",
649 let { cloneScope } = this;
650 args = args.map(arg => Cu.cloneInto(arg, cloneScope));
654 `runSafe failure: cloning into ${
656 }: ${e}\n\n${filterStack(Error())}`
660 return this.applySafeWithoutClone(callback, args, caller);
664 applySafeWithoutClone(callback, args, caller) {
667 "context.runSafeWithoutClone called after context unloaded",
670 } else if (!this.active) {
672 "context.runSafeWithoutClone called while context is inactive",
677 return Reflect.apply(callback, null, args);
679 // An extension listener may as well be throwing an object that isn't
680 // an instance of Error, in that case we have to use fallbacks for the
681 // error message, fileName, lineNumber and columnNumber properties.
682 const isError = e instanceof this.Error;
689 message = `${e.name}: ${e.message}`;
690 lineNumber = e.lineNumber;
691 columnNumber = e.columnNumber;
692 fileName = e.fileName;
694 message = `uncaught exception: ${e}`;
697 // TODO(Bug 1810582): the following fallback logic may go away once
698 // we introduced a better way to capture and log the exception in
699 // the right window and in all cases (included when the extension
700 // code is raising undefined or an object that isn't an instance of
701 // the Error constructor).
703 // Fallbacks for the error location:
704 // - the callback location if it is registered directly from the
705 // extension code (and not wrapped by the child/ext-APINAMe.js
706 // implementation, like e.g. browser.storage, browser.devtools.network
707 // are doing and browser.menus).
708 // - if the location of the extension callback is not directly
709 // available (e.g. browser.storage onChanged events, and similarly
710 // for browser.devtools.network and browser.menus events):
711 // - the extension page url if the context is an extension page
712 // - the extension base url if the context is a content script
713 const cbLoc = Cu.getFunctionSourceLocation(callback);
714 fileName = cbLoc.filename;
715 lineNumber = cbLoc.lineNumber ?? lineNumber;
717 const extBaseUrl = this.extension.baseURI.resolve("/");
718 if (fileName.startsWith(extBaseUrl)) {
719 fileName = cbLoc.filename;
720 lineNumber = cbLoc.lineNumber ?? lineNumber;
722 fileName = this.contentWindow?.location?.href;
723 if (!fileName || !fileName.startsWith(extBaseUrl)) {
724 fileName = extBaseUrl;
728 // Ignore errors on retrieving the callback source location.
733 `Extension error: ${message} ${fileName} ${lineNumber}\n[[Exception stack\n${
734 isError ? filterStack(e) : undefined
735 }Current stack\n${filterStack(Error())}]]\n`
738 // If the error is coming from an extension context associated
739 // to a window (e.g. an extension page or extension content script).
741 // TODO(Bug 1810574): for the background service worker we will need to do
742 // something similar, but not tied to the innerWindowID because there
743 // wouldn't be one set for extension contexts related to the
744 // background service worker.
746 // TODO(Bug 1810582): change the error associated to the innerWindowID to also
747 // include a full stack from the original error.
748 if (!this.isProxyContextParent && this.contentWindow) {
749 Services.console.logMessage(
756 Ci.nsIScriptError.errorFlag,
757 "content javascript",
762 // Also report the original error object (because it also includes
763 // the full error stack).
769 checkLoadURL(url, options = {}) {
770 // As an optimization, f the URL starts with the extension's base URL,
771 // don't do any further checks. It's always allowed to load it.
772 if (url.startsWith(this.extension.baseURL)) {
776 return checkLoadURL(url, this.principal, options);
780 * Safely call JSON.stringify() on an object that comes from an
783 * @param {[any, callback?, number?]} args for JSON.stringify()
784 * @returns {string} The stringified representation of obj
786 jsonStringify(...args) {
787 if (!this.jsonSandbox) {
788 this.jsonSandbox = Cu.Sandbox(this.principal, {
789 sameZoneAs: this.cloneScope,
794 return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
798 this.onClose.add(obj);
802 this.onClose.delete(obj);
806 this.checkedLastError = true;
807 return this._lastError;
811 this.checkedLastError = false;
812 this._lastError = val;
816 * Normalizes the given error object for use by the target scope. If
817 * the target is an error object which belongs to that scope, it is
818 * returned as-is. If it is an ordinary object with a `message`
819 * property, it is converted into an error belonging to the target
820 * scope. If it is an Error object which does *not* belong to the
821 * clone scope, it is reported, and converted to an unexpected
824 * @param {Error|object} error
825 * @param {SavedFrame?} [caller]
828 normalizeError(error, caller) {
829 if (error instanceof this.Error) {
832 let message, fileName;
833 if (error && typeof error === "object") {
834 const isPlain = ChromeUtils.getClassName(error) === "Object";
835 if (isPlain && error.mozWebExtLocation) {
836 caller = error.mozWebExtLocation;
838 if (isPlain && caller && (error.mozWebExtLocation || !error.fileName)) {
839 caller = Cu.cloneInto(caller, this.cloneScope);
840 return ChromeUtils.createError(error.message, caller);
845 error instanceof ExtensionError ||
846 this.principal.subsumes(Cu.getObjectPrincipal(error))
848 message = error.message;
849 fileName = error.fileName;
854 Cu.reportError(error);
855 message = "An unexpected error occurred";
857 return new this.Error(message, fileName);
861 * Sets the value of `.lastError` to `error`, calls the given
862 * callback, and reports an error if the value has not been checked
863 * when the callback returns.
865 * @param {object} error An object with a `message` property. May
866 * optionally be an `Error` object belonging to the target scope.
867 * @param {SavedFrame?} caller
868 * The optional caller frame which triggered this callback, to be used
869 * in error reporting.
870 * @param {Function} callback The callback to call.
871 * @returns {*} The return value of callback.
873 withLastError(error, caller, callback) {
874 this.lastError = this.normalizeError(error);
878 if (!this.checkedLastError) {
879 Cu.reportError(`Unchecked lastError value: ${this.lastError}`, caller);
881 this.lastError = null;
886 * Captures the most recent stack frame which belongs to the extension.
888 * @returns {SavedFrame?}
891 return ChromeUtils.getCallerLocation(this.principal);
895 * Wraps the given promise so it can be safely returned to extension
896 * code in this context.
898 * If `callback` is provided, however, it is used as a completion
899 * function for the promise, and no promise is returned. In this case,
900 * the callback is called when the promise resolves or rejects. In the
901 * latter case, `lastError` is set to the rejection value, and the
902 * callback function must check `browser.runtime.lastError` or
903 * `extension.runtime.lastError` in order to prevent it being reported
906 * @param {Promise} promise The promise with which to wrap the
907 * callback. May resolve to a `SpreadArgs` instance, in which case
908 * each element will be used as a separate argument.
910 * Unless the promise object belongs to the cloneScope global, its
911 * resolution value is cloned into cloneScope prior to calling the
912 * `callback` function or resolving the wrapped promise.
914 * @param {Function} [callback] The callback function to wrap
916 * @returns {Promise|undefined} If callback is null, a promise object
917 * belonging to the target scope. Otherwise, undefined.
919 wrapPromise(promise, callback = null) {
920 let caller = this.getCaller();
921 let applySafe = this.applySafe.bind(this);
922 if (Cu.getGlobalForObject(promise) === this.cloneScope) {
923 applySafe = this.applySafeWithoutClone.bind(this);
930 Cu.reportError(`Promise resolved after context unloaded\n`, caller);
931 } else if (!this.active) {
933 `Promise resolved while context is inactive\n`,
936 } else if (args instanceof NoCloneSpreadArgs) {
937 this.applySafeWithoutClone(callback, args.unwrappedValues, caller);
938 } else if (args instanceof SpreadArgs) {
939 applySafe(callback, args, caller);
941 applySafe(callback, [args], caller);
945 this.withLastError(error, caller, () => {
948 `Promise rejected after context unloaded\n`,
951 } else if (!this.active) {
953 `Promise rejected while context is inactive\n`,
957 this.applySafeWithoutClone(callback, [], caller);
963 return new this.Promise((resolve, reject) => {
968 `Promise resolved after context unloaded\n`,
971 } else if (!this.active) {
973 `Promise resolved while context is inactive\n`,
976 } else if (value instanceof NoCloneSpreadArgs) {
977 let values = value.unwrappedValues;
978 this.applySafeWithoutClone(
980 values.length == 1 ? [values[0]] : [values],
983 } else if (value instanceof SpreadArgs) {
984 applySafe(resolve, value.length == 1 ? value : [value], caller);
986 applySafe(resolve, [value], caller);
992 `Promise rejected after context unloaded: ${
993 value && value.message
997 } else if (!this.active) {
999 `Promise rejected while context is inactive: ${
1000 value && value.message
1005 this.applySafeWithoutClone(
1007 [this.normalizeError(value, caller)],
1018 this.unloaded = true;
1020 for (let obj of this.onClose) {
1023 this.onClose.clear();
1027 * A simple proxy for unload(), for use with callOnClose().
1035 * An object that runs the implementation of a schema API. Instantiations of
1036 * this interfaces are used by Schemas.jsm.
1040 class SchemaAPIInterface {
1042 * Calls this as a function that returns its return value.
1045 * @param {Array} args The parameters for the function.
1046 * @returns {*} The return value of the invoked function.
1048 callFunction(args) {
1049 throw new Error("Not implemented");
1053 * Calls this as a function and ignores its return value.
1056 * @param {Array} args The parameters for the function.
1058 callFunctionNoReturn(args) {
1059 throw new Error("Not implemented");
1063 * Calls this as a function that completes asynchronously.
1066 * @param {Array} args The parameters for the function.
1067 * @param {callback} [callback] The callback to be called when the function
1069 * @param {boolean} [requireUserInput=false] If true, the function should
1070 * fail if the browser is not currently handling user input.
1071 * @returns {Promise|undefined} Must be void if `callback` is set, and a
1072 * promise otherwise. The promise is resolved when the function completes.
1074 callAsyncFunction(args, callback, requireUserInput = false) {
1075 throw new Error("Not implemented");
1079 * Retrieves the value of this as a property.
1082 * @returns {*} The value of the property.
1085 throw new Error("Not implemented");
1089 * Assigns the value to this as property.
1092 * @param {string} value The new value of the property.
1094 setProperty(value) {
1095 throw new Error("Not implemented");
1099 * Registers a `listener` to this as an event.
1102 * @param {Function} listener The callback to be called when the event fires.
1103 * @param {Array} args Extra parameters for EventManager.addListener.
1104 * @see EventManager.addListener
1106 addListener(listener, args) {
1107 throw new Error("Not implemented");
1111 * Checks whether `listener` is listening to this as an event.
1114 * @param {Function} listener The event listener.
1115 * @returns {boolean} Whether `listener` is registered with this as an event.
1116 * @see EventManager.hasListener
1118 hasListener(listener) {
1119 throw new Error("Not implemented");
1123 * Unregisters `listener` from this as an event.
1126 * @param {Function} listener The event listener.
1127 * @see EventManager.removeListener
1129 removeListener(listener) {
1130 throw new Error("Not implemented");
1134 * Revokes the implementation object, and prevents any further method
1135 * calls from having external effects.
1140 throw new Error("Not implemented");
1145 * An object that runs a locally implemented API.
1147 class LocalAPIImplementation extends SchemaAPIInterface {
1149 * Constructs an implementation of the `name` method or property of `pathObj`.
1151 * @param {object} pathObj The object containing the member with name `name`.
1152 * @param {string} name The name of the implemented member.
1153 * @param {BaseContext} context The context in which the schema is injected.
1155 constructor(pathObj, name, context) {
1157 this.pathObj = pathObj;
1159 this.context = context;
1163 if (this.pathObj[this.name][lazy.Schemas.REVOKE]) {
1164 this.pathObj[this.name][lazy.Schemas.REVOKE]();
1167 this.pathObj = null;
1169 this.context = null;
1172 callFunction(args) {
1174 return this.pathObj[this.name](...args);
1176 throw this.context.normalizeError(e);
1180 callFunctionNoReturn(args) {
1182 this.pathObj[this.name](...args);
1184 throw this.context.normalizeError(e);
1188 callAsyncFunction(args, callback, requireUserInput) {
1191 if (requireUserInput) {
1192 if (!this.context.contentWindow.windowUtils.isHandlingUserInput) {
1193 throw new ExtensionError(
1194 `${this.name} may only be called from a user input handler`
1198 promise = this.pathObj[this.name](...args) || Promise.resolve();
1200 promise = Promise.reject(e);
1202 return this.context.wrapPromise(promise, callback);
1206 return this.pathObj[this.name];
1209 setProperty(value) {
1210 this.pathObj[this.name] = value;
1213 addListener(listener, args) {
1215 this.pathObj[this.name].addListener.call(null, listener, ...args);
1217 throw this.context.normalizeError(e);
1221 hasListener(listener) {
1222 return this.pathObj[this.name].hasListener.call(null, listener);
1225 removeListener(listener) {
1226 this.pathObj[this.name].removeListener.call(null, listener);
1230 // Recursively copy properties from source to dest.
1231 function deepCopy(dest, source) {
1232 for (let prop in source) {
1233 let desc = Object.getOwnPropertyDescriptor(source, prop);
1234 if (typeof desc.value == "object") {
1235 if (!(prop in dest)) {
1238 deepCopy(dest[prop], source[prop]);
1240 Object.defineProperty(dest, prop, desc);
1245 function getChild(map, key) {
1246 let child = map.children.get(key);
1250 children: new Map(),
1253 map.children.set(key, child);
1258 function getPath(map, path) {
1259 for (let key of path) {
1260 map = getChild(map, key);
1265 function mergePaths(dest, source) {
1266 for (let name of source.modules) {
1267 dest.modules.add(name);
1270 for (let [name, child] of source.children.entries()) {
1271 mergePaths(getChild(dest, name), child);
1276 * Manages loading and accessing a set of APIs for a specific extension
1279 * @param {BaseContext} context
1280 * The context to manage APIs for.
1281 * @param {SchemaAPIManager} apiManager
1282 * The API manager holding the APIs to manage.
1283 * @param {object} root
1284 * The root object into which APIs will be injected.
1287 constructor(context, apiManager, root) {
1288 this.context = context;
1289 this.scopeName = context.envType;
1290 this.apiManager = apiManager;
1293 this.apiPaths = new Map();
1295 this.apis = new Map();
1299 * Synchronously loads and initializes an ExtensionAPI instance.
1301 * @param {string} name
1302 * The name of the API to load.
1305 if (this.apis.has(name)) {
1309 let { extension } = this.context;
1311 let api = this.apiManager.getAPI(name, extension, this.scopeName);
1316 this.apis.set(name, api);
1318 deepCopy(this.root, api.getAPI(this.context));
1322 * Asynchronously loads and initializes an ExtensionAPI instance.
1324 * @param {string} name
1325 * The name of the API to load.
1327 async asyncLoadAPI(name) {
1328 if (this.apis.has(name)) {
1332 let { extension } = this.context;
1333 if (!lazy.Schemas.checkPermissions(name, extension)) {
1337 let api = await this.apiManager.asyncGetAPI(
1342 // Check again, because async;
1343 if (this.apis.has(name)) {
1347 this.apis.set(name, api);
1349 deepCopy(this.root, api.getAPI(this.context));
1353 * Finds the API at the given path from the root object, and
1354 * synchronously loads the API that implements it if it has not
1355 * already been loaded.
1357 * @param {string} path
1358 * The "."-separated path to find.
1362 if (this.apiPaths.has(path)) {
1363 return this.apiPaths.get(path);
1366 let obj = this.root;
1367 let modules = this.apiManager.modulePaths;
1369 let parts = path.split(".");
1370 for (let [i, key] of parts.entries()) {
1374 modules = getChild(modules, key);
1376 for (let name of modules.modules) {
1377 if (!this.apis.has(name)) {
1382 if (!(key in obj) && i < parts.length - 1) {
1388 this.apiPaths.set(path, obj);
1393 * Finds the API at the given path from the root object, and
1394 * asynchronously loads the API that implements it if it has not
1395 * already been loaded.
1397 * @param {string} path
1398 * The "."-separated path to find.
1399 * @returns {Promise<*>}
1401 async asyncFindAPIPath(path) {
1402 if (this.apiPaths.has(path)) {
1403 return this.apiPaths.get(path);
1406 let obj = this.root;
1407 let modules = this.apiManager.modulePaths;
1409 let parts = path.split(".");
1410 for (let [i, key] of parts.entries()) {
1414 modules = getChild(modules, key);
1416 for (let name of modules.modules) {
1417 if (!this.apis.has(name)) {
1418 await this.asyncLoadAPI(name);
1422 if (!(key in obj) && i < parts.length - 1) {
1426 if (typeof obj[key] === "function") {
1427 obj = obj[key].bind(obj);
1433 this.apiPaths.set(path, obj);
1442 * @property {string} url
1443 * The URL of the script which contains the module's
1444 * implementation. This script must define a global property
1445 * matching the modules name, which must be a class constructor
1446 * which inherits from {@link ExtensionAPI}.
1448 * @property {string} schema
1449 * The URL of the JSON schema which describes the module's API.
1451 * @property {Array<string>} scopes
1452 * The list of scope names into which the API may be loaded.
1454 * @property {Array<string>} manifest
1455 * The list of top-level manifest properties which will trigger
1456 * the module to be loaded, and its `onManifestEntry` method to be
1459 * @property {Array<string>} events
1460 * The list events which will trigger the module to be loaded, and
1461 * its appropriate event handler method to be called. Currently
1462 * only accepts "startup".
1464 * @property {Array<string>} permissions
1465 * An optional list of permissions, any of which must be present
1466 * in order for the module to load.
1468 * @property {Array<Array<string>>} paths
1469 * A list of paths from the root API object which, when accessed,
1470 * will cause the API module to be instantiated and injected.
1474 * This object loads the ext-*.js scripts that define the extension API.
1476 * This class instance is shared with the scripts that it loads, so that the
1477 * ext-*.js scripts and the instantiator can communicate with each other.
1479 class SchemaAPIManager extends EventEmitter {
1481 * @param {string} processType
1482 * "main" - The main, one and only chrome browser process.
1483 * "addon" - An addon process.
1484 * "content" - A content process.
1485 * "devtools" - A devtools process.
1486 * @param {import("Schemas.sys.mjs").SchemaRoot} [schema]
1488 constructor(processType, schema) {
1490 this.processType = processType;
1493 this.schema = schema;
1496 this.modules = new Map();
1497 this.modulePaths = { children: new Map(), modules: new Set() };
1498 this.manifestKeys = new Map();
1499 this.eventModules = new DefaultMap(() => new Set());
1500 this.settingsModules = new Set();
1502 this._modulesJSONLoaded = false;
1504 this.schemaURLs = new Map();
1506 this.apis = new DefaultWeakMap(() => new Map());
1508 this._scriptScopes = [];
1511 onStartup(extension) {
1513 for (let apiName of this.eventModules.get("startup")) {
1515 extension.apiManager.asyncGetAPI(apiName, extension).then(api => {
1523 return Promise.all(promises);
1526 async loadModuleJSON(urls) {
1527 let promises = urls.map(url => fetch(url).then(resp => resp.json()));
1529 return this.initModuleJSON(await Promise.all(promises));
1532 initModuleJSON(blobs) {
1533 for (let json of blobs) {
1534 this.registerModules(json);
1537 this._modulesJSONLoaded = true;
1539 return new StructuredCloneHolder("SchemaAPIManager/initModuleJSON", null, {
1540 modules: this.modules,
1541 modulePaths: this.modulePaths,
1542 manifestKeys: this.manifestKeys,
1543 eventModules: this.eventModules,
1544 settingsModules: this.settingsModules,
1545 schemaURLs: this.schemaURLs,
1549 initModuleData(moduleData) {
1550 if (!this._modulesJSONLoaded) {
1551 let data = moduleData.deserialize({}, true);
1553 this.modules = data.modules;
1554 this.modulePaths = data.modulePaths;
1555 this.manifestKeys = data.manifestKeys;
1556 this.eventModules = new DefaultMap(() => new Set(), data.eventModules);
1557 this.settingsModules = new Set(data.settingsModules);
1558 this.schemaURLs = data.schemaURLs;
1561 this._modulesJSONLoaded = true;
1565 * Registers a set of ExtensionAPI modules to be lazily loaded and
1566 * managed by this manager.
1568 * @param {object} obj
1569 * An object containing property for eacy API module to be
1570 * registered. Each value should be an object implementing the
1571 * APIModule interface.
1573 registerModules(obj) {
1574 for (let [name, details] of Object.entries(obj)) {
1575 details.namespaceName = name;
1577 if (this.modules.has(name)) {
1578 throw new Error(`Module '${name}' already registered`);
1580 this.modules.set(name, details);
1582 if (details.schema) {
1585 (details.scopes.includes("content_parent") ||
1586 details.scopes.includes("content_child"));
1587 this.schemaURLs.set(details.schema, { content });
1590 for (let event of details.events || []) {
1591 this.eventModules.get(event).add(name);
1594 if (details.settings) {
1595 this.settingsModules.add(name);
1598 for (let key of details.manifest || []) {
1599 if (this.manifestKeys.has(key)) {
1601 `Manifest key '${key}' already registered by '${this.manifestKeys.get(
1607 this.manifestKeys.set(key, name);
1610 for (let path of details.paths || []) {
1611 getPath(this.modulePaths, path).modules.add(name);
1617 * Emits an `onManifestEntry` event for the top-level manifest entry
1618 * on all relevant {@link ExtensionAPI} instances for the given
1621 * The API modules will be synchronously loaded if they have not been
1624 * @param {Extension} extension
1625 * The extension for which to emit the events.
1626 * @param {string} entry
1627 * The name of the top-level manifest entry.
1631 emitManifestEntry(extension, entry) {
1632 let apiName = this.manifestKeys.get(entry);
1634 let api = extension.apiManager.getAPI(apiName, extension);
1635 return api.onManifestEntry(entry);
1639 * Emits an `onManifestEntry` event for the top-level manifest entry
1640 * on all relevant {@link ExtensionAPI} instances for the given
1643 * The API modules will be asynchronously loaded if they have not been
1646 * @param {Extension} extension
1647 * The extension for which to emit the events.
1648 * @param {string} entry
1649 * The name of the top-level manifest entry.
1651 * @returns {Promise<*>}
1653 async asyncEmitManifestEntry(extension, entry) {
1654 let apiName = this.manifestKeys.get(entry);
1656 let api = await extension.apiManager.asyncGetAPI(apiName, extension);
1657 return api.onManifestEntry(entry);
1662 * Returns the {@link ExtensionAPI} instance for the given API module,
1663 * for the given extension, in the given scope, synchronously loading
1664 * and instantiating it if necessary.
1666 * @param {string} name
1667 * The name of the API module to load.
1668 * @param {Extension} extension
1669 * The extension for which to load the API.
1670 * @param {string} [scope = null]
1671 * The scope type for which to retrieve the API, or null if not
1672 * being retrieved for a particular scope.
1674 * @returns {ExtensionAPI?}
1676 getAPI(name, extension, scope = null) {
1677 if (!this._checkGetAPI(name, extension, scope)) {
1681 let apis = this.apis.get(extension);
1682 if (apis.has(name)) {
1683 return apis.get(name);
1686 let module = this.loadModule(name);
1688 let api = new module(extension);
1689 apis.set(name, api);
1693 * Returns the {@link ExtensionAPI} instance for the given API module,
1694 * for the given extension, in the given scope, asynchronously loading
1695 * and instantiating it if necessary.
1697 * @param {string} name
1698 * The name of the API module to load.
1699 * @param {Extension} extension
1700 * The extension for which to load the API.
1701 * @param {string} [scope = null]
1702 * The scope type for which to retrieve the API, or null if not
1703 * being retrieved for a particular scope.
1705 * @returns {Promise<ExtensionAPI>?}
1707 async asyncGetAPI(name, extension, scope = null) {
1708 if (!this._checkGetAPI(name, extension, scope)) {
1712 let apis = this.apis.get(extension);
1713 if (apis.has(name)) {
1714 return apis.get(name);
1717 let module = await this.asyncLoadModule(name);
1719 // Check again, because async.
1720 if (apis.has(name)) {
1721 return apis.get(name);
1724 let api = new module(extension);
1725 apis.set(name, api);
1730 * Synchronously loads an API module, if not already loaded, and
1731 * returns its ExtensionAPI constructor.
1733 * @param {string} name
1734 * The name of the module to load.
1735 * @returns {typeof ExtensionAPI}
1738 let module = this.modules.get(name);
1739 if (module.loaded) {
1740 return this.global[name];
1743 this._checkLoadModule(module, name);
1747 Services.scriptloader.loadSubScript(module.url, this.global);
1749 module.loaded = true;
1751 return this.global[name];
1754 * aSynchronously loads an API module, if not already loaded, and
1755 * returns its ExtensionAPI constructor.
1757 * @param {string} name
1758 * The name of the module to load.
1760 * @returns {Promise<typeof ExtensionAPI>}
1762 asyncLoadModule(name) {
1763 let module = this.modules.get(name);
1764 if (module.loaded) {
1765 return Promise.resolve(this.global[name]);
1767 if (module.asyncLoaded) {
1768 return module.asyncLoaded;
1771 this._checkLoadModule(module, name);
1773 module.asyncLoaded = ChromeUtils.compileScript(module.url).then(script => {
1775 script.executeInGlobal(this.global);
1777 module.loaded = true;
1779 return this.global[name];
1782 return module.asyncLoaded;
1785 asyncLoadSettingsModules() {
1787 Array.from(this.settingsModules).map(apiName =>
1788 this.asyncLoadModule(apiName)
1794 return this.modules.get(name);
1798 * Checks whether the given API module may be loaded for the given
1799 * extension, in the given scope.
1801 * @param {string} name
1802 * The name of the API module to check.
1803 * @param {Extension} extension
1804 * The extension for which to check the API.
1805 * @param {string} [scope = null]
1806 * The scope type for which to check the API, or null if not
1807 * being checked for a particular scope.
1809 * @returns {boolean}
1810 * Whether the module may be loaded.
1812 _checkGetAPI(name, extension, scope = null) {
1813 let module = this.getModule(name);
1815 // A module may not exist for a particular manifest version, but
1816 // we allow keys in the manifest. An example is pageAction.
1821 module.permissions &&
1822 !module.permissions.some(perm => extension.hasPermission(perm))
1831 if (!module.scopes.includes(scope)) {
1835 if (!lazy.Schemas.checkPermissions(module.namespaceName, extension)) {
1842 _checkLoadModule(module, name) {
1844 throw new Error(`Module '${name}' does not exist`);
1846 if (module.asyncLoaded) {
1847 throw new Error(`Module '${name}' currently being lazily loaded`);
1849 if (this.global && this.global[name]) {
1851 `Module '${name}' conflicts with existing global property`
1857 * Create a global object that is used as the shared global for all ext-*.js
1858 * scripts that are loaded via `loadScript`.
1860 * @returns {object} A sandbox that is used as the global by `loadScript`.
1862 _createExtGlobal() {
1863 let global = Cu.Sandbox(
1864 Services.scriptSecurityManager.getSystemPrincipal(),
1867 wantGlobalProperties: ["ChromeUtils"],
1868 sandboxName: `Namespace of ext-*.js scripts for ${this.processType} (from: resource://gre/modules/ExtensionCommon.jsm)`,
1872 Object.assign(global, {
1880 ExtensionAPIPersistent,
1888 StructuredCloneHolder,
1895 ChromeUtils.defineLazyGetter(global, "console", getConsole);
1896 // eslint-disable-next-line mozilla/lazy-getter-object-name
1897 ChromeUtils.defineESModuleGetters(global, {
1898 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
1906 this.global = this._createExtGlobal();
1911 * Load an ext-*.js script. The script runs in its own scope, if it wishes to
1912 * share state with another script it can assign to the `global` variable. If
1913 * it wishes to communicate with this API manager, use `extensions`.
1915 * @param {string} scriptUrl The URL of the ext-*.js script.
1917 loadScript(scriptUrl) {
1918 // Create the object in the context of the sandbox so that the script runs
1919 // in the sandbox's context instead of here.
1920 let scope = Cu.createObjectIn(this.global);
1922 Services.scriptloader.loadSubScript(scriptUrl, scope);
1924 // Save the scope to avoid it being garbage collected.
1925 this._scriptScopes.push(scope);
1929 class LazyAPIManager extends SchemaAPIManager {
1930 constructor(processType, moduleData, schemaURLs) {
1933 /** @type {Promise | boolean} */
1934 this.initialized = false;
1936 this.initModuleData(moduleData);
1938 this.schemaURLs = schemaURLs;
1944 defineLazyGetter(LazyAPIManager.prototype, "schema", function () {
1945 let root = new lazy.SchemaRoot(lazy.Schemas.rootSchema, this.schemaURLs);
1946 root.parseSchemas();
1950 class MultiAPIManager extends SchemaAPIManager {
1951 constructor(processType, children) {
1954 this.initialized = false;
1956 this.children = children;
1960 if (!this.initialized) {
1961 this.initialized = true;
1963 for (let child of this.children) {
1964 if (child.lazyInit) {
1965 let res = child.lazyInit();
1966 if (res && typeof res.then === "function") {
1971 mergePaths(this.modulePaths, child.modulePaths);
1976 onStartup(extension) {
1977 return Promise.all(this.children.map(child => child.onStartup(extension)));
1981 for (let child of this.children) {
1982 if (child.modules.has(name)) {
1983 return child.modules.get(name);
1989 for (let child of this.children) {
1990 if (child.modules.has(name)) {
1991 return child.loadModule(name);
1996 asyncLoadModule(name) {
1997 for (let child of this.children) {
1998 if (child.modules.has(name)) {
1999 return child.asyncLoadModule(name);
2005 defineLazyGetter(MultiAPIManager.prototype, "schema", function () {
2006 let bases = this.children.map(child => child.schema);
2008 // All API manager schema roots should derive from the global schema root,
2009 // so it doesn't need its own entry.
2010 if (bases[bases.length - 1] === lazy.Schemas) {
2014 if (bases.length === 1) {
2017 return new lazy.SchemaRoot(bases, new Map());
2020 export function LocaleData(data) {
2021 this.defaultLocale = data.defaultLocale;
2022 this.selectedLocale = data.selectedLocale;
2023 this.locales = data.locales || new Map();
2024 this.warnedMissingKeys = new Set();
2026 // Map(locale-name -> Map(message-key -> localized-string))
2028 // Contains a key for each loaded locale, each of which is a
2029 // Map of message keys to their localized strings.
2030 this.messages = data.messages || new Map();
2032 if (data.builtinMessages) {
2033 this.messages.set(this.BUILTIN, data.builtinMessages);
2037 LocaleData.prototype = {
2038 // Representation of the object to send to content processes. This
2039 // should include anything the content process might need.
2042 defaultLocale: this.defaultLocale,
2043 selectedLocale: this.selectedLocale,
2044 messages: this.messages,
2045 locales: this.locales,
2049 BUILTIN: "@@BUILTIN_MESSAGES",
2052 return this.messages.has(locale);
2055 // https://developer.chrome.com/extensions/i18n
2056 localizeMessage(message, substitutions = [], options = {}) {
2057 let defaultOptions = {
2062 let locales = this.availableLocales;
2063 if (options.locale) {
2065 [this.BUILTIN, options.locale, this.defaultLocale].filter(locale =>
2066 this.messages.has(locale)
2071 options = Object.assign(defaultOptions, options);
2073 // Message names are case-insensitive, so normalize them to lower-case.
2074 message = message.toLowerCase();
2075 for (let locale of locales) {
2076 let messages = this.messages.get(locale);
2077 if (messages.has(message)) {
2078 let str = messages.get(message);
2080 if (!str.includes("$")) {
2084 if (!Array.isArray(substitutions)) {
2085 substitutions = [substitutions];
2088 let replacer = (matched, index, dollarSigns) => {
2090 // This is not quite Chrome-compatible. Chrome consumes any number
2091 // of digits following the $, but only accepts 9 substitutions. We
2092 // accept any number of substitutions.
2093 index = parseInt(index, 10) - 1;
2094 return index in substitutions ? substitutions[index] : "";
2096 // For any series of contiguous `$`s, the first is dropped, and
2097 // the rest remain in the output string.
2100 return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
2104 // Check for certain pre-defined messages.
2105 if (message == "@@ui_locale") {
2106 return this.uiLocale;
2107 } else if (message.startsWith("@@bidi_")) {
2108 let rtl = Services.locale.isAppLocaleRTL;
2110 if (message == "@@bidi_dir") {
2111 return rtl ? "rtl" : "ltr";
2112 } else if (message == "@@bidi_reversed_dir") {
2113 return rtl ? "ltr" : "rtl";
2114 } else if (message == "@@bidi_start_edge") {
2115 return rtl ? "right" : "left";
2116 } else if (message == "@@bidi_end_edge") {
2117 return rtl ? "left" : "right";
2121 if (!this.warnedMissingKeys.has(message)) {
2122 let error = `Unknown localization message ${message}`;
2123 if (options.cloneScope) {
2124 error = new options.cloneScope.Error(error);
2126 Cu.reportError(error);
2127 this.warnedMissingKeys.add(message);
2129 return options.defaultValue;
2132 // Localize a string, replacing all |__MSG_(.*)__| tokens with the
2133 // matching string from the current locale, as determined by
2134 // |this.selectedLocale|.
2136 // This may not be called before calling either |initLocale| or
2137 // |initAllLocales|.
2138 localize(str, locale = this.selectedLocale) {
2143 return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
2144 return this.localizeMessage(message, [], {
2146 defaultValue: matched,
2151 // Validates the contents of a locale JSON file, normalizes the
2152 // messages into a Map of message key -> localized string pairs.
2153 addLocale(locale, messages, extension) {
2154 let result = new Map();
2156 let isPlainObject = obj =>
2158 typeof obj === "object" &&
2159 ChromeUtils.getClassName(obj) === "Object";
2161 // Chrome does not document the semantics of its localization
2162 // system very well. It handles replacements by pre-processing
2163 // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
2164 // replacements. Later, it processes the resulting string for
2165 // |$[0-9]| replacements.
2167 // Again, it does not document this, but it accepts any number
2168 // of sequential |$|s, and replaces them with that number minus
2169 // 1. It also accepts |$| followed by any number of sequential
2170 // digits, but refuses to process a localized string which
2171 // provides more than 9 substitutions.
2172 if (!isPlainObject(messages)) {
2173 extension.packagingError(`Invalid locale data for ${locale}`);
2177 for (let key of Object.keys(messages)) {
2178 let msg = messages[key];
2180 if (!isPlainObject(msg) || typeof msg.message != "string") {
2181 extension.packagingError(
2182 `Invalid locale message data for ${locale}, message ${JSON.stringify(
2189 // Substitutions are case-insensitive, so normalize all of their names
2191 let placeholders = new Map();
2192 if ("placeholders" in msg && isPlainObject(msg.placeholders)) {
2193 for (let key of Object.keys(msg.placeholders)) {
2194 placeholders.set(key.toLowerCase(), msg.placeholders[key]);
2198 let replacer = (match, name) => {
2199 let replacement = placeholders.get(name.toLowerCase());
2200 if (isPlainObject(replacement) && "content" in replacement) {
2201 return replacement.content;
2206 let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
2208 // Message names are also case-insensitive, so normalize them to lower-case.
2209 result.set(key.toLowerCase(), value);
2212 this.messages.set(locale, result);
2216 get acceptLanguages() {
2217 let result = Services.prefs.getComplexValue(
2218 "intl.accept_languages",
2219 Ci.nsIPrefLocalizedString
2221 return result.split(/\s*,\s*/g);
2225 return Services.locale.appLocaleAsBCP47;
2228 get availableLocales() {
2229 const locales = [this.BUILTIN, this.selectedLocale, this.defaultLocale];
2230 const value = new Set(locales.filter(locale => this.messages.has(locale)));
2231 return redefineGetter(this, "availableLocales", value);
2236 * This is a generic class for managing event listeners.
2239 * new EventManager({
2241 * name: "api.subAPI",
2242 * register: fire => {
2243 * let listener = (...) => {
2244 * // Fire any listeners registered with addListener.
2245 * fire.async(arg1, arg2);
2247 * // Register the listener.
2248 * SomehowRegisterListener(listener);
2250 * // Return a way to unregister the listener.
2251 * SomehowUnregisterListener(listener);
2256 * The result is an object with addListener, removeListener, and
2257 * hasListener methods. `context` is an add-on scope (either an
2258 * ExtensionContext in the chrome process or ExtensionContext in a
2261 class EventManager {
2263 * A persistent event must provide module and name. Additionally the
2264 * module must implement primeListeners in the ExtensionAPI class.
2266 * A startup blocking event must also add the startupBlocking flag in
2267 * ext-toolkit.json or ext-browser.json.
2269 * Listeners synchronously added from a background extension context
2270 * will be persisted, for a persistent background script only the
2271 * "startup blocking" events will be persisted.
2273 * EventManager instances created in a child process can't persist any listener.
2275 * @param {object} params
2276 * Parameters that control this EventManager.
2277 * @param {BaseContext} params.context
2278 * An object representing the extension instance using this event.
2279 * @param {string} params.module
2280 * The API module name, required for persistent events.
2281 * @param {string} params.event
2282 * The API event name, required for persistent events.
2283 * @param {ExtensionAPI} params.extensionApi
2284 * The API intance. If the API uses the ExtensionAPIPersistent class, some simplification is
2285 * possible by passing the api (self or this) and the internal register function will be used.
2286 * @param {string} [params.name]
2287 * A name used only for debugging. If not provided, name is built from module and event.
2288 * @param {functon} params.register
2289 * A function called whenever a new listener is added.
2290 * @param {boolean} [params.inputHandling=false]
2291 * If true, the "handling user input" flag is set while handlers
2292 * for this event are executing.
2294 constructor(params) {
2302 inputHandling = false,
2303 resetIdleOnEvent = true,
2305 this.context = context;
2306 this.module = module;
2309 this.register = register;
2310 this.inputHandling = inputHandling;
2311 this.resetIdleOnEvent = resetIdleOnEvent;
2313 const isBackgroundParent =
2314 this.context.envType === "addon_parent" &&
2315 this.context.isBackgroundContext;
2317 // TODO(Bug 1844041): ideally we should restrict resetIdleOnEvent to
2318 // EventManager instances that belongs to the event page, but along
2319 // with that we should consider if calling sendMessage from an event
2320 // page should also reset idle timer, and so in the shorter term
2321 // here we are allowing listeners from other extension pages to
2322 // also reset the idle timer.
2323 const isAddonContext = ["addon_parent", "addon_child"].includes(
2324 this.context.envType
2327 // Avoid resetIdleOnEvent overhead by only consider it when applicable.
2328 if (!isAddonContext || context.extension.persistentBackground) {
2329 this.resetIdleOnEvent = false;
2333 this.name = `${module}.${event}`;
2336 if (!this.register && extensionApi instanceof ExtensionAPIPersistent) {
2337 this.register = (fire, ...params) => {
2338 return extensionApi.registerEventListener(
2339 { context, event, fire },
2344 if (!this.register) {
2346 `EventManager requires register method for ${this.name}.`
2350 this.canPersistEvents = module && event && isBackgroundParent;
2352 if (this.canPersistEvents) {
2353 let { extension } = context;
2354 if (extension.persistentBackground) {
2355 // Persistent backgrounds will only persist startup blocking APIs.
2356 let api_module = extension.apiManager.getModule(this.module);
2357 if (!api_module?.startupBlocking) {
2358 this.canPersistEvents = false;
2361 // Event pages will persist all APIs that implement primeListener.
2362 // The api is already loaded so this does not have performance effect.
2363 let api = extension.apiManager.getAPI(
2369 // If the api doesn't implement primeListener we do not persist the events.
2370 if (!api?.primeListener) {
2371 this.canPersistEvents = false;
2376 this.unregister = new Map();
2377 this.remove = new Map();
2381 * Information about listeners to persistent events is associated with
2382 * the extension to which they belong. Any extension thas has such
2383 * listeners has a property called `persistentListeners` that is a
2386 * - the first 2 keys are the module name (e.g., webRequest)
2387 * and the name of the event within the module (e.g., onBeforeRequest).
2389 * - the third level of the map is used to track multiple listeners for
2390 * the same event, these listeners are distinguished by the extra arguments
2391 * passed to addListener()
2393 * - for quick lookups, the key to the third Map is the result of calling
2394 * uneval() on the array of extra arguments.
2396 * - the value stored in the Map or persistent listeners we keep in memory
2397 * is a plain object with:
2398 * - a property called `params` that is the original (ie, not uneval()ed)
2399 * extra arguments to addListener()
2400 * - and a property called `listeners` that is an array of plain object
2401 * each representing a listener to be primed and a `primeId` autoincremented
2402 * integer that represents each of the primed listeners that belongs to the
2403 * group listeners with the same set of extra params.
2404 * - a `nextPrimeId` property keeps track of the numeric primeId that should
2405 * be assigned to new persistent listeners added for the same event and
2406 * same set of extra params.
2408 * For a primed listener (i.e., the stub listener created during browser startup
2409 * before the extension background page is started, and after an event page is
2410 * suspended on idle), the object will be later populated (by the callers of
2411 * EventManager.primeListeners) with an additional `primed` property that serves
2412 * as a placeholder listener, collecting all events that got emitted while the
2413 * background page was not yet started, and eventually replaced by a callback
2414 * registered from the extension code, once the background page scripts have been
2415 * executed (or dropped if the background page scripts do not register the same
2416 * listener anymore).
2418 * @param {Extension} extension
2419 * @returns {boolean} True if the extension had any persistent listeners.
2421 static _initPersistentListeners(extension) {
2422 if (extension.persistentListeners) {
2423 return !!extension.persistentListeners.size;
2426 let listeners = new DefaultMap(() => new DefaultMap(() => new Map()));
2427 extension.persistentListeners = listeners;
2429 let persistentListeners = extension.startupData?.persistentListeners;
2430 if (!persistentListeners) {
2435 for (let [module, savedModuleEntry] of Object.entries(
2438 for (let [event, savedEventEntry] of Object.entries(savedModuleEntry)) {
2439 for (let paramList of savedEventEntry) {
2440 /* Before Bug 1795801 (Firefox < 113) each entry was related to a listener
2441 * registered with a different set of extra params (and so only one listener
2442 * could be persisted for the same set of extra params)
2444 * After Bug 1795801 (Firefox >= 113) each entry still represents a listener
2445 * registered for that event, but multiple listeners registered with the same
2446 * set of extra params will be captured as multiple entries in the
2449 * NOTE: persisted listeners are stored in the startupData part of the Addon DB
2450 * and are expected to be preserved across Firefox and Addons upgrades and downgrades
2451 * (unlike the WebExtensions startupCache data which is cleared when Firefox or the
2452 * addon is updated) and so we are taking special care about forward and backward
2453 * compatibility of the persistentListeners on-disk format:
2455 * - forward compatibility: when this new version of this startupData loading logic
2456 * is loading the old persistentListeners on-disk format:
2457 * - on the first run only one listener will be primed for each of the extra params
2458 * recorded in the startupData (same as in older Firefox versions)
2459 * and Bug 1795801 will still be hit, but once the background
2460 * context is started once the startupData will be updated to
2461 * include each of the listeners (indipendently if the set of
2462 * extra params is the same as another listener already been
2464 * - after the first run, all listeners will be primed separately, even if the extra
2465 * params are the same as other listeners already primed, and so
2466 * each of the listener will receive the pending events collected
2467 * by their related primed listener and Bug 1795801 not to be hit anymore.
2469 * - backward compatibility: when the old version of this startupData loading logic
2470 * (https://searchfox.org/mozilla-central/rev/cd2121e7d8/toolkit/components/extensions/ExtensionCommon.jsm#2360-2371)
2471 * is loading the new persistentListeners on-disk format, the last
2472 * entry with the same set of extra params will be eventually overwritting the
2473 * entry for another primed listener with the same extra params, Bug 1795801 will still
2474 * be hit, but no actual change in behavior is expected.
2476 let key = uneval(paramList);
2477 const eventEntry = listeners.get(module).get(event);
2479 if (eventEntry.has(key)) {
2480 const keyEntry = eventEntry.get(key);
2481 let primeId = keyEntry.nextPrimeId;
2482 keyEntry.listeners.push({ primeId });
2483 keyEntry.nextPrimeId++;
2485 eventEntry.set(key, {
2488 listeners: [{ primeId: 0 }],
2498 // Extract just the information needed at startup for all persistent
2499 // listeners, and arrange for it to be saved. This should be called
2500 // whenever the set of persistent listeners for an extension changes.
2501 static _writePersistentListeners(extension) {
2502 let startupListeners = {};
2503 for (let [module, moduleEntry] of extension.persistentListeners) {
2504 startupListeners[module] = {};
2505 for (let [event, eventEntry] of moduleEntry) {
2506 // Turn the per-event entries from the format they are being kept
2510 // { params: paramList1, listeners: [listener1, listener2, ...] },
2511 // { params: paramList2, listeners: [listener3, listener3, ...] },
2515 // into the format used for storing them on disk (in the startupData),
2516 // which is an array of the params for each listener (with the param list
2517 // included as many times as many listeners are persisted for the same
2520 // [paramList1, paramList1, ..., paramList2, paramList2, ...]
2522 // This format will also work as expected on older Firefox versions where
2523 // only one listener was being persisted for each set of params.
2524 startupListeners[module][event] = Array.from(
2526 ).flatMap(keyEntry => keyEntry.listeners.map(() => keyEntry.params));
2530 extension.startupData.persistentListeners = startupListeners;
2531 extension.saveStartupData();
2534 // Set up "primed" event listeners for any saved event listeners
2535 // in an extension's startup data.
2536 // This function is only called during browser startup, it stores details
2537 // about all primed listeners in the extension's persistentListeners Map.
2538 static primeListeners(extension, isInStartup = false) {
2539 if (!EventManager._initPersistentListeners(extension)) {
2543 for (let [module, moduleEntry] of extension.persistentListeners) {
2544 // If we're in startup, we only want to continue attempting to prime a
2545 // subset of events that should be startup blocking.
2547 let api_module = extension.apiManager.getModule(module);
2548 if (!api_module.startupBlocking) {
2553 let api = extension.apiManager.getAPI(module, extension, "addon_parent");
2555 // If an extension is upgraded and a permission, such as webRequest, is
2556 // removed, we will have been called but the API is no longer available.
2557 if (!api?.primeListener) {
2558 // The runtime module no longer implements primed listeners, drop them.
2559 extension.persistentListeners.delete(module);
2560 EventManager._writePersistentListeners(extension);
2563 for (let [event, eventEntry] of moduleEntry) {
2564 for (let [key, { params, listeners }] of eventEntry) {
2565 for (let listener of listeners) {
2566 // Reset the `listener.added` flag by setting it to `false` while
2567 // re-priming the listeners because the event page has suspended
2568 // and the previous converted listener is no longer listening.
2569 const listenerWasAdded = listener.added;
2570 listener.added = false;
2571 listener.params = params;
2572 let primed = { pendingEvents: [] };
2574 let fireEvent = (...args) =>
2575 new Promise((resolve, reject) => {
2576 if (!listener.primed) {
2579 `primed listener ${module}.${event} not re-registered`
2584 primed.pendingEvents.push({ args, resolve, reject });
2585 extension.emit("background-script-event");
2589 wakeup: () => extension.wakeupBackground(),
2592 // fire.async for ProxyContextParent is already not cloning.
2597 let handler = api.primeListener(
2604 listener.primed = primed;
2605 Object.assign(primed, handler);
2609 `Error priming listener ${module}.${event}: ${e} :: ${e.stack}`
2611 // Force this listener to be cleared.
2612 listener.error = true;
2615 // If an attempt to prime a listener failed, ensure it is cleared now.
2616 // If a module is a startup blocking module, not all listeners may
2617 // get primed during early startup. For that reason, we don't clear
2618 // persisted listeners during early startup. At the end of background
2619 // execution any listeners that were not renewed will be cleared.
2621 // TODO(Bug 1797474): consider priming runtime.onStartup and
2622 // avoid to special handling it here.
2627 (`${module}.${event}` === "runtime.onStartup" &&
2628 listenerWasAdded) ||
2632 EventManager.clearPersistentListener(
2647 * This is called as a result of background script startup-finished and shutdown.
2649 * After startup, it removes any remaining primed listeners. These exist if the
2650 * listener was not renewed during startup. In this case the persisted listener
2651 * data is also removed.
2653 * During shutdown, care should be taken to set clearPersistent to false.
2654 * persisted listener data should NOT be cleared during shutdown.
2656 * @param {Extension} extension
2657 * @param {boolean} clearPersistent whether the persisted listener data should be cleared.
2659 static clearPrimedListeners(extension, clearPersistent = true) {
2660 if (!extension.persistentListeners) {
2664 for (let [module, moduleEntry] of extension.persistentListeners) {
2665 for (let [event, eventEntry] of moduleEntry) {
2666 for (let [key, { listeners }] of eventEntry) {
2667 for (let listener of listeners) {
2668 let { primed, added, primeId } = listener;
2669 // When a primed listener is added or renewed during initial
2670 // background execution we set an added flag. If it was primed
2671 // when added, primed is set to null.
2677 // When a primed listener was not renewed, primed will still be truthy.
2678 // These need to be cleared on shutdown (important for event pages), but
2679 // we only clear the persisted listener data after the startup of a background.
2680 // Release any pending events and unregister the primed handler.
2681 listener.primed = null;
2683 for (let evt of primed.pendingEvents) {
2684 evt.reject(new Error("listener not re-registered"));
2686 primed.unregister();
2689 // Clear any persisted events that were not renewed, should typically
2690 // only be done at the end of the background page load.
2691 if (clearPersistent) {
2692 EventManager.clearPersistentListener(
2706 // Record the fact that there is a listener for the given event in
2707 // the given extension. `args` is an Array containing any extra
2708 // arguments that were passed to addListener().
2709 static savePersistentListener(extension, module, event, args = []) {
2710 EventManager._initPersistentListeners(extension);
2711 let key = uneval(args);
2712 const eventEntry = extension.persistentListeners.get(module).get(event);
2715 if (!eventEntry.has(key)) {
2716 // when writing, only args are written, other properties are dropped
2718 eventEntry.set(key, {
2720 listeners: [{ added: true, primeId }],
2724 const keyEntry = eventEntry.get(key);
2725 primeId = keyEntry.nextPrimeId;
2726 keyEntry.listeners.push({ added: true, primeId });
2727 keyEntry.nextPrimeId = primeId + 1;
2730 EventManager._writePersistentListeners(extension);
2731 return [module, event, key, primeId];
2734 // Remove the record for the given event listener from the extension's
2735 // startup data. `key` must be a string, the result of calling uneval()
2736 // on the array of extra arguments originally passed to addListener().
2737 static clearPersistentListener(
2744 let eventEntry = extension.persistentListeners.get(module).get(event);
2746 let keyEntry = eventEntry.get(key);
2748 if (primeId != undefined && keyEntry) {
2749 keyEntry.listeners = keyEntry.listeners.filter(
2750 listener => listener.primeId !== primeId
2754 if (primeId == undefined || keyEntry?.listeners.length === 0) {
2755 eventEntry.delete(key);
2756 if (eventEntry.size == 0) {
2757 let moduleEntry = extension.persistentListeners.get(module);
2758 moduleEntry.delete(event);
2759 if (moduleEntry.size == 0) {
2760 extension.persistentListeners.delete(module);
2765 EventManager._writePersistentListeners(extension);
2768 addListener(callback, ...args) {
2769 if (this.unregister.has(callback)) {
2772 this.context.logActivity("api_call", `${this.name}.addListener`, { args });
2774 let shouldFire = () => {
2775 if (this.context.unloaded) {
2776 dump(`${this.name} event fired after context unloaded.\n`);
2777 } else if (!this.context.active) {
2778 dump(`${this.name} event fired while context is inactive.\n`);
2779 } else if (this.unregister.has(callback)) {
2785 let { extension } = this.context;
2786 const resetIdle = () => {
2787 if (this.resetIdleOnEvent) {
2788 extension?.emit("background-script-reset-idle", {
2790 eventName: this.name,
2796 // Bug 1754866 fire.sync doesn't match documentation.
2797 sync: (...args) => {
2800 let result = this.context.applySafe(callback, args);
2801 this.context.logActivity("api_event", this.name, { args, result });
2805 async: (...args) => {
2806 return Promise.resolve().then(() => {
2809 let result = this.context.applySafe(callback, args);
2810 this.context.logActivity("api_event", this.name, { args, result });
2816 if (!shouldFire()) {
2817 throw new Error("Called raw() on unloaded/inactive context");
2820 let result = Reflect.apply(callback, null, args);
2821 this.context.logActivity("api_event", this.name, { args, result });
2824 asyncWithoutClone: (...args) => {
2825 return Promise.resolve().then(() => {
2828 let result = this.context.applySafeWithoutClone(callback, args);
2829 this.context.logActivity("api_event", this.name, { args, result });
2836 let { module, event } = this;
2838 let unregister = null;
2839 let recordStartupData = false;
2841 // If this is a persistent event, check for a listener that was already
2842 // created during startup. If there is one, use it and don't create a
2844 if (this.canPersistEvents) {
2845 // Once a background is started, listenerPromises is set to null. At
2846 // that point, we stop recording startup data.
2847 recordStartupData = !!this.context.listenerPromises;
2849 let key = uneval(args);
2850 EventManager._initPersistentListeners(extension);
2851 let keyEntry = extension.persistentListeners
2856 // Get the first persistent listener which matches the module, event and extra arguments
2857 // and not added back by the extension yet, the persistent listener found may be either
2858 // primed or not (in particular API Events that belongs to APIs that should not be blocking
2859 // startup may have persistent listeners that are not primed during the first execution
2860 // of the background context happening as part of the applications startup, whereas they
2861 // will be primed when the background context will be suspended on the idle timeout).
2862 let listener = keyEntry?.listeners.find(listener => !listener.added);
2864 // During startup only a subset of persisted listeners are primed. As
2865 // well, each API determines whether to prime a specific listener.
2866 let { primed } = listener;
2868 listener.primed = null;
2870 primed.convert(fire, this.context);
2871 unregister = primed.unregister;
2873 for (let evt of primed.pendingEvents) {
2874 evt.resolve(fire.async(...evt.args));
2877 listener.added = true;
2879 recordStartupData = false;
2880 this.remove.set(callback, () => {
2881 EventManager.clearPersistentListener(
2893 unregister = this.register(fire, ...args);
2896 this.unregister.set(callback, unregister);
2897 this.context.callOnClose(this);
2899 // If this is a new listener for a persistent event, record
2900 // the details for subsequent startups.
2901 if (recordStartupData) {
2902 const [, , , /* _module */ /* _event */ /* _key */ primeId] =
2903 EventManager.savePersistentListener(extension, module, event, args);
2904 this.remove.set(callback, () => {
2905 EventManager.clearPersistentListener(
2916 removeListener(callback, clearPersistentListener = true) {
2917 if (!this.unregister.has(callback)) {
2920 this.context.logActivity("api_call", `${this.name}.removeListener`, {
2924 let unregister = this.unregister.get(callback);
2925 this.unregister.delete(callback);
2932 if (clearPersistentListener && this.remove.has(callback)) {
2933 let cleanup = this.remove.get(callback);
2934 this.remove.delete(callback);
2938 if (this.unregister.size == 0) {
2939 this.context.forgetOnClose(this);
2943 hasListener(callback) {
2944 return this.unregister.has(callback);
2948 for (let callback of this.unregister.keys()) {
2949 this.removeListener(callback, false);
2959 addListener: (...args) => this.addListener(...args),
2960 removeListener: (...args) => this.removeListener(...args),
2961 hasListener: (...args) => this.hasListener(...args),
2962 setUserInput: this.inputHandling,
2963 [lazy.Schemas.REVOKE]: () => this.revoke(),
2968 // Simple API for event listeners where events never fire.
2969 function ignoreEvent(context, name) {
2971 addListener: function (callback) {
2972 let id = context.extension.id;
2973 let frame = Components.stack.caller;
2974 let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
2975 let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
2984 Ci.nsIScriptError.warningFlag,
2985 "content javascript"
2987 Services.console.logMessage(scriptError);
2989 removeListener: function (callback) {},
2990 hasListener: function (callback) {},
2994 const stylesheetMap = new DefaultMap(url => {
2995 let uri = Services.io.newURI(url);
2996 return lazy.styleSheetService.preloadSheet(
2998 lazy.styleSheetService.AGENT_SHEET
3003 * Updates the in-memory representation of extension host permissions, i.e.
3004 * policy.allowedOrigins.
3006 * @param {WebExtensionPolicy} policy
3007 * A policy. All MatchPattern instances in policy.allowedOrigins are
3008 * expected to have been constructed with ignorePath: true.
3009 * @param {string[]} origins
3010 * A list of already-normalized origins, equivalent to using the
3011 * MatchPattern constructor with ignorePath: true.
3012 * @param {boolean} isAdd
3013 * Whether to add instead of removing the host permissions.
3015 function updateAllowedOrigins(policy, origins, isAdd) {
3016 if (!origins.length) {
3017 // Nothing to modify.
3020 let patternMap = new Map();
3021 for (let pattern of policy.allowedOrigins.patterns) {
3022 patternMap.set(pattern.pattern, pattern);
3025 for (let origin of origins) {
3026 patternMap.delete(origin);
3029 // In the parent process, policy.extension.restrictSchemes is available.
3030 // In the content process, we need to check the mozillaAddons permission,
3031 // which is only available if approved by the parent.
3032 const restrictSchemes =
3033 policy.extension?.restrictSchemes ??
3034 policy.hasPermission("mozillaAddons");
3035 for (let origin of origins) {
3036 if (patternMap.has(origin)) {
3041 new MatchPattern(origin, { restrictSchemes, ignorePath: true })
3045 // patternMap contains only MatchPattern instances, so we don't need to set
3046 // the options parameter (with restrictSchemes, etc.) since that is only used
3047 // if the input is a string.
3048 policy.allowedOrigins = new MatchPatternSet(Array.from(patternMap.values()));
3051 export var ExtensionCommon = {
3056 ExtensionAPIPersistent,
3058 LocalAPIImplementation,
3073 runSafeSyncWithoutClone,
3075 updateAllowedOrigins,
3076 withHandlingUserInput,