diff --git a/toolkit/components/extensions/.eslintrc.js b/toolkit/components/extensions/.eslintrc.js index f62ff67ff06967..777ca9bf56d1ac 100644 --- a/toolkit/components/extensions/.eslintrc.js +++ b/toolkit/components/extensions/.eslintrc.js @@ -56,8 +56,22 @@ module.exports = { }, ], - // No using variables before defined - "no-use-before-define": "error", + // No using things before they're defined. + "no-use-before-define": [ + "error", + { + allowNamedExports: true, + classes: true, + // The next two being false allows idiomatic patterns which are more + // type-inference friendly. Functions are hoisted, so this is safe. + functions: false, + // This flag is only meaningful for `var` declarations. + // When false, it still disallows use-before-define in the same scope. + // Since we only allow `var` at the global scope, this is no worse than + // how we currently declare an uninitialized `let` at the top of file. + variables: false, + }, + ], // Disallow using variables outside the blocks they are defined (especially // since only let and const are used, see "no-var"). diff --git a/toolkit/components/extensions/ConduitsParent.sys.mjs b/toolkit/components/extensions/ConduitsParent.sys.mjs index e0debfd1876a04..3342315323823c 100644 --- a/toolkit/components/extensions/ConduitsParent.sys.mjs +++ b/toolkit/components/extensions/ConduitsParent.sys.mjs @@ -452,13 +452,13 @@ export class ConduitsParent extends JSWindowActorParent { return Hub.recvConduitOpened(arg, this); } - sender = Hub.remotes.get(sender); - if (!sender || sender.actor !== this) { + let remote = Hub.remotes.get(sender); + if (!remote || remote.actor !== this) { throw new Error(`Unknown sender or wrong actor for recv${name}`); } if (name === "ConduitClosed") { - return Hub.recvConduitClosed(sender); + return Hub.recvConduitClosed(remote); } let conduit = Hub.byMethod.get(name); @@ -466,7 +466,7 @@ export class ConduitsParent extends JSWindowActorParent { throw new Error(`Parent conduit for recv${name} not found`); } - return conduit._recv(name, arg, { actor: this, query, sender }); + return conduit._recv(name, arg, { actor: this, query, sender: remote }); } /** diff --git a/toolkit/components/extensions/Extension.sys.mjs b/toolkit/components/extensions/Extension.sys.mjs index 164c0bfc461d11..7aafa867be080c 100644 --- a/toolkit/components/extensions/Extension.sys.mjs +++ b/toolkit/components/extensions/Extension.sys.mjs @@ -172,7 +172,7 @@ export { Management }; const { getUniqueId, promiseTimeout } = ExtensionUtils; -const { EventEmitter, updateAllowedOrigins } = ExtensionCommon; +const { EventEmitter, redefineGetter, updateAllowedOrigins } = ExtensionCommon; ChromeUtils.defineLazyGetter( lazy, @@ -869,6 +869,19 @@ const manifestTypes = new Map([ * `loadManifest` has been called, and completed. */ export class ExtensionData { + /** + * Note: These fields are only available and meant to be used on Extension + * instances, declared here because methods from this class reference them. + */ + /** @type {object} TODO: move to the Extension class, bug 1871094. */ + addonData; + /** @type {nsIURI} */ + baseURI; + /** @type {nsIPrincipal} */ + principal; + /** @type {boolean} */ + temporarilyInstalled; + constructor(rootURI, isPrivileged = false) { this.rootURI = rootURI; this.resourceURL = rootURI.spec; @@ -2676,7 +2689,7 @@ class BootstrapScope { // APP_STARTED. In some situations, such as background and // persisted listeners, we also need to know that the addon // was updated. - this.updateReason = this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]; + this.updateReason = BootstrapScope.BOOTSTRAP_REASON_MAP[reason]; // Retain any previously granted permissions that may have migrated // into the optional list. if (data.oldPermissions) { @@ -2703,7 +2716,7 @@ class BootstrapScope { // eslint-disable-next-line no-use-before-define this.extension = new Extension( data, - this.BOOTSTRAP_REASON_TO_STRING_MAP[reason], + BootstrapScope.BOOTSTRAP_REASON_MAP[reason], this.updateReason ); return this.extension.startup(); @@ -2711,31 +2724,27 @@ class BootstrapScope { async shutdown(data, reason) { let result = await this.extension.shutdown( - this.BOOTSTRAP_REASON_TO_STRING_MAP[reason] + BootstrapScope.BOOTSTRAP_REASON_MAP[reason] ); this.extension = null; return result; } -} -ChromeUtils.defineLazyGetter( - BootstrapScope.prototype, - "BOOTSTRAP_REASON_TO_STRING_MAP", - () => { - const { BOOTSTRAP_REASONS } = lazy.AddonManagerPrivate; - - return Object.freeze({ - [BOOTSTRAP_REASONS.APP_STARTUP]: "APP_STARTUP", - [BOOTSTRAP_REASONS.APP_SHUTDOWN]: "APP_SHUTDOWN", - [BOOTSTRAP_REASONS.ADDON_ENABLE]: "ADDON_ENABLE", - [BOOTSTRAP_REASONS.ADDON_DISABLE]: "ADDON_DISABLE", - [BOOTSTRAP_REASONS.ADDON_INSTALL]: "ADDON_INSTALL", - [BOOTSTRAP_REASONS.ADDON_UNINSTALL]: "ADDON_UNINSTALL", - [BOOTSTRAP_REASONS.ADDON_UPGRADE]: "ADDON_UPGRADE", - [BOOTSTRAP_REASONS.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE", + static get BOOTSTRAP_REASON_MAP() { + const BR = lazy.AddonManagerPrivate.BOOTSTRAP_REASONS; + const value = Object.freeze({ + [BR.APP_STARTUP]: "APP_STARTUP", + [BR.APP_SHUTDOWN]: "APP_SHUTDOWN", + [BR.ADDON_ENABLE]: "ADDON_ENABLE", + [BR.ADDON_DISABLE]: "ADDON_DISABLE", + [BR.ADDON_INSTALL]: "ADDON_INSTALL", + [BR.ADDON_UNINSTALL]: "ADDON_UNINSTALL", + [BR.ADDON_UPGRADE]: "ADDON_UPGRADE", + [BR.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE", }); + return redefineGetter(this, "BOOTSTRAP_REASON_TO_STRING_MAP", value); } -); +} class DictionaryBootstrapScope extends BootstrapScope { install(data, reason) {} @@ -2744,11 +2753,11 @@ class DictionaryBootstrapScope extends BootstrapScope { startup(data, reason) { // eslint-disable-next-line no-use-before-define this.dictionary = new Dictionary(data); - return this.dictionary.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + return this.dictionary.startup(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); } - shutdown(data, reason) { - this.dictionary.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + async shutdown(data, reason) { + this.dictionary.shutdown(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); this.dictionary = null; } } @@ -2756,16 +2765,16 @@ class DictionaryBootstrapScope extends BootstrapScope { class LangpackBootstrapScope extends BootstrapScope { install(data, reason) {} uninstall(data, reason) {} - update(data, reason) {} + async update(data, reason) {} startup(data, reason) { // eslint-disable-next-line no-use-before-define this.langpack = new Langpack(data); - return this.langpack.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + return this.langpack.startup(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); } - shutdown(data, reason) { - this.langpack.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + async shutdown(data, reason) { + this.langpack.shutdown(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); this.langpack = null; } } @@ -2779,12 +2788,12 @@ class SitePermissionBootstrapScope extends BootstrapScope { // eslint-disable-next-line no-use-before-define this.sitepermission = new SitePermission(data); return this.sitepermission.startup( - this.BOOTSTRAP_REASON_TO_STRING_MAP[reason] + BootstrapScope.BOOTSTRAP_REASON_MAP[reason] ); } - shutdown(data, reason) { - this.sitepermission.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); + async shutdown(data, reason) { + this.sitepermission.shutdown(BootstrapScope.BOOTSTRAP_REASON_MAP[reason]); this.sitepermission = null; } } @@ -2800,6 +2809,9 @@ let pendingExtensions = new Map(); * @augments ExtensionData */ export class Extension extends ExtensionData { + /** @type {Map>} */ + persistentListeners; + constructor(addonData, startupReason, updateReason) { super(addonData.resourceURI, addonData.isPrivileged); @@ -2832,6 +2844,7 @@ export class Extension extends ExtensionData { this.startupData = addonData.startupData || {}; this.startupReason = startupReason; this.updateReason = updateReason; + this.temporarilyInstalled = !!addonData.temporarilyInstalled; if ( updateReason || @@ -3077,10 +3090,6 @@ export class Extension extends ExtensionData { return [this.id, this.version, Services.locale.appLocaleAsBCP47]; } - get temporarilyInstalled() { - return !!this.addonData.temporarilyInstalled; - } - saveStartupData() { if (this.dontSaveStartupData) { return; diff --git a/toolkit/components/extensions/ExtensionActions.sys.mjs b/toolkit/components/extensions/ExtensionActions.sys.mjs index 33a5d10072190b..8e8cf3abd28544 100644 --- a/toolkit/components/extensions/ExtensionActions.sys.mjs +++ b/toolkit/components/extensions/ExtensionActions.sys.mjs @@ -43,6 +43,7 @@ class PanelActionBase { enabled: true, title: options.default_title || extension.name, popup: options.default_popup || "", + icon: null, }; this.globals = Object.create(this.defaults); diff --git a/toolkit/components/extensions/ExtensionChild.sys.mjs b/toolkit/components/extensions/ExtensionChild.sys.mjs index 91bc92bed60f2a..a06ac8024f11f9 100644 --- a/toolkit/components/extensions/ExtensionChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionChild.sys.mjs @@ -39,7 +39,7 @@ import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; const { DefaultMap, ExtensionError, LimitedSet, getUniqueId } = ExtensionUtils; const { - defineLazyGetter, + redefineGetter, EventEmitter, EventManager, LocalAPIImplementation, @@ -299,12 +299,13 @@ class Port { } throw new this.context.Error("Attempt to postMessage on disconnected port"); } -} -defineLazyGetter(Port.prototype, "api", function () { - let api = this.getAPI(); - return Cu.cloneInto(api, this.context.cloneScope, { cloneFunctions: true }); -}); + get api() { + const scope = this.context.cloneScope; + const value = Cu.cloneInto(this.getAPI(), scope, { cloneFunctions: true }); + return redefineGetter(this, "api", value); + } +} /** * Each extension context gets its own Messenger object. It handles the @@ -522,7 +523,7 @@ class BrowserExtensionContent extends EventEmitter { emit(event, ...args) { Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, { event, args }); - super.emit(event, ...args); + return super.emit(event, ...args); } // TODO(Bug 1768471): consider folding this back into emit if we will change it to @@ -914,10 +915,10 @@ class ChildAPIManager { * hasListener methods. See SchemaAPIInterface for documentation. */ getParentEvent(path) { - path = path.split("."); + let parts = path.split("."); - let name = path.pop(); - let namespace = path.join("."); + let name = parts.pop(); + let namespace = parts.join("."); let impl = new ProxyAPIImplementation(namespace, name, this, true); return { diff --git a/toolkit/components/extensions/ExtensionCommon.sys.mjs b/toolkit/components/extensions/ExtensionCommon.sys.mjs index 517fedf0e09c09..e4cd09c0c884ff 100644 --- a/toolkit/components/extensions/ExtensionCommon.sys.mjs +++ b/toolkit/components/extensions/ExtensionCommon.sys.mjs @@ -54,8 +54,6 @@ function getConsole() { }); } -export var ExtensionCommon; - // Run a function and report exceptions. function runSafeSyncWithoutClone(f, ...args) { try { @@ -122,6 +120,8 @@ function withHandlingUserInput(window, callable) { * 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 @@ -131,30 +131,39 @@ function withHandlingUserInput(window, callable) { * value. */ function defineLazyGetter(object, prop, getter) { - let redefine = (obj, value) => { - Object.defineProperty(obj, prop, { - enumerable: true, - configurable: true, - writable: true, - value, - }); - return value; - }; - Object.defineProperty(object, prop, { enumerable: true, configurable: true, - get() { - return redefine(this, getter.call(this)); + return redefineGetter(this, prop, getter.call(this), true); }, - set(value) { - redefine(this, 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; @@ -292,11 +301,11 @@ class EventEmitter { * The listener to call when events are emitted. */ once(event, listener) { - let wrapper = (...args) => { + let wrapper = (event, ...args) => { this.off(event, wrapper); this[ONCE_MAP].delete(listener); - return listener(...args); + return listener(event, ...args); }; this[ONCE_MAP].set(listener, wrapper); @@ -360,11 +369,28 @@ class ExtensionAPI extends EventEmitter { destroy() {} - onManifestEntry(entry) {} + /** @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} manifest + */ + static onUpdate(id, manifest) {} } /** @@ -374,6 +400,9 @@ class ExtensionAPI extends EventEmitter { * this.apiNamespace = class extends ExtensionAPIPersistent {}; */ class ExtensionAPIPersistent extends ExtensionAPI { + /** @type {Record} */ + PERSISTENT_EVENTS; + /** * Check for event entry. * @@ -440,6 +469,11 @@ class ExtensionAPIPersistent extends ExtensionAPI { * @abstract */ class BaseContext { + /** @type {boolean} */ + isTopContext; + /** @type {string} */ + viewType; + constructor(envType, extension) { this.envType = envType; this.onClose = new Set(); @@ -1895,6 +1929,7 @@ class LazyAPIManager extends SchemaAPIManager { constructor(processType, moduleData, schemaURLs) { super(processType); + /** @type {Promise | boolean} */ this.initialized = false; this.initModuleData(moduleData); @@ -1979,7 +2014,7 @@ defineLazyGetter(MultiAPIManager.prototype, "schema", function () { return new lazy.SchemaRoot(bases, new Map()); }); -function LocaleData(data) { +export function LocaleData(data) { this.defaultLocale = data.defaultLocale; this.selectedLocale = data.selectedLocale; this.locales = data.locales || new Map(); @@ -2186,15 +2221,13 @@ LocaleData.prototype = { get uiLocale() { return Services.locale.appLocaleAsBCP47; }, -}; -defineLazyGetter(LocaleData.prototype, "availableLocales", function () { - return new Set( - [this.BUILTIN, this.selectedLocale, this.defaultLocale].filter(locale => - this.messages.has(locale) - ) - ); -}); + 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. @@ -3012,7 +3045,7 @@ function updateAllowedOrigins(policy, origins, isAdd) { policy.allowedOrigins = new MatchPatternSet(Array.from(patternMap.values())); } -ExtensionCommon = { +export var ExtensionCommon = { BaseContext, CanOfAPIs, EventManager, @@ -3028,6 +3061,7 @@ ExtensionCommon = { checkLoadURI, checkLoadURL, defineLazyGetter, + redefineGetter, getConsole, ignoreEvent, instanceOf, diff --git a/toolkit/components/extensions/ExtensionContent.sys.mjs b/toolkit/components/extensions/ExtensionContent.sys.mjs index 9b67de4360a222..131d555bf0adf1 100644 --- a/toolkit/components/extensions/ExtensionContent.sys.mjs +++ b/toolkit/components/extensions/ExtensionContent.sys.mjs @@ -59,6 +59,7 @@ const { CanOfAPIs, SchemaAPIManager, defineLazyGetter, + redefineGetter, runSafeSyncWithoutClone, } = ExtensionCommon; @@ -142,7 +143,7 @@ class CacheMap extends DefaultMap { super.get(url).timer.cancel(); } - super.delete(url); + return super.delete(url); } clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) { @@ -160,16 +161,17 @@ class CacheMap extends DefaultMap { class ScriptCache extends CacheMap { constructor(options, extension) { - super(SCRIPT_EXPIRY_TIMEOUT_MS, null, extension); - this.options = options; - } - - defaultConstructor(url) { - let promise = ChromeUtils.compileScript(url, this.options); - promise.then(script => { - promise.script = script; - }); - return promise; + super( + SCRIPT_EXPIRY_TIMEOUT_MS, + url => { + let promise = ChromeUtils.compileScript(url, options); + promise.then(script => { + promise.script = script; + }); + return promise; + }, + extension + ); } } @@ -207,7 +209,7 @@ class BaseCSSCache extends CacheMap { } } - super.delete(key); + return super.delete(key); } } @@ -907,13 +909,6 @@ class ContentScriptContextChild extends BaseContext { this.url = contentWindow.location.href; - defineLazyGetter(this, "chromeObj", () => { - let chromeObj = Cu.createObjectIn(this.sandbox); - - this.childManager.inject(chromeObj); - return chromeObj; - }); - lazy.Schemas.exportLazyGetter( this.sandbox, "browser", @@ -993,31 +988,28 @@ class ContentScriptContextChild extends BaseContext { this.sandbox = null; } -} - -defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function () { - return new Messenger(this); -}); -defineLazyGetter( - ContentScriptContextChild.prototype, - "childManager", - function () { + get childManager() { apiManager.lazyInit(); - - let localApis = {}; - let can = new CanOfAPIs(this, apiManager, localApis); - + let can = new CanOfAPIs(this, apiManager, {}); let childManager = new ChildAPIManager(this, this.messageManager, can, { envType: "content_parent", url: this.url, }); - this.callOnClose(childManager); + return redefineGetter(this, "childManager", childManager); + } - return childManager; + get chromeObj() { + let chromeObj = Cu.createObjectIn(this.sandbox); + this.childManager.inject(chromeObj); + return redefineGetter(this, "chromeObj", chromeObj); } -); + + get messenger() { + return redefineGetter(this, "messenger", new Messenger(this)); + } +} // Responsible for creating ExtensionContexts and injecting content // scripts into them when new documents are created. diff --git a/toolkit/components/extensions/ExtensionDNR.sys.mjs b/toolkit/components/extensions/ExtensionDNR.sys.mjs index b6d1f68f4659a9..2757333b3b419b 100644 --- a/toolkit/components/extensions/ExtensionDNR.sys.mjs +++ b/toolkit/components/extensions/ExtensionDNR.sys.mjs @@ -145,7 +145,7 @@ class RuleCondition { } } -class Rule { +export class Rule { constructor(rule) { this.id = rule.id; this.priority = rule.priority; @@ -633,6 +633,24 @@ class ModifyHeadersBase { this.channel = channel; } + /** + * @param {MatchedRule} matchedRule + * @returns {object[]} + */ + headerActionsFor(matchedRule) { + throw new Error("Not implemented."); + } + + /** + * @param {MatchedRule} matchedrule + * @param {string} name + * @param {string} value + * @param {boolean} merge + */ + setHeaderImpl(matchedrule, name, value, merge) { + throw new Error("Not implemented."); + } + /** @param {MatchedRule[]} matchedRules */ applyModifyHeaders(matchedRules) { for (const matchedRule of matchedRules) { @@ -1190,7 +1208,7 @@ class RuleValidator { } } -class RuleQuotaCounter { +export class RuleQuotaCounter { constructor(isStaticRulesets) { this.isStaticRulesets = isStaticRulesets; this.ruleLimitName = isStaticRulesets diff --git a/toolkit/components/extensions/ExtensionDNRStore.sys.mjs b/toolkit/components/extensions/ExtensionDNRStore.sys.mjs index 2e8777cc4281eb..0dd6fad75a6cb3 100644 --- a/toolkit/components/extensions/ExtensionDNRStore.sys.mjs +++ b/toolkit/components/extensions/ExtensionDNRStore.sys.mjs @@ -148,7 +148,7 @@ class StoreData { if (!(extension instanceof lazy.Extension)) { throw new Error("Missing mandatory extension parameter"); } - this.schemaVersion = schemaVersion || this.constructor.VERSION; + this.schemaVersion = schemaVersion || StoreData.VERSION; this.extVersion = extVersion ?? extension.version; this.#extUUID = extension.uuid; // Used to skip storing the data in the startupCache or storing the lastUpdateTag in @@ -220,8 +220,8 @@ class StoreData { } #updateRulesets({ - staticRulesets, - dynamicRuleset, + staticRulesets = null, + dynamicRuleset = null, lastUpdateTag = Services.uuid.generateUUID().toString(), } = {}) { if (staticRulesets) { @@ -667,11 +667,7 @@ class RulesetsStore { * @returns {StoreData} */ #getDefaults(extension) { - return new StoreData(extension, { - extUUID: extension.uuid, - extVersion: extension.version, - temporarilyInstalled: extension.temporarilyInstalled, - }); + return new StoreData(extension, { extVersion: extension.version }); } /** diff --git a/toolkit/components/extensions/ExtensionPageChild.sys.mjs b/toolkit/components/extensions/ExtensionPageChild.sys.mjs index 7c051c511e9252..d84459f1edb9a4 100644 --- a/toolkit/components/extensions/ExtensionPageChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionPageChild.sys.mjs @@ -29,13 +29,11 @@ import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; const { getInnerWindowID, promiseEvent } = ExtensionUtils; -const { BaseContext, CanOfAPIs, SchemaAPIManager, defineLazyGetter } = +const { BaseContext, CanOfAPIs, SchemaAPIManager, redefineGetter } = ExtensionCommon; const { ChildAPIManager, Messenger } = ExtensionChild; -export var ExtensionPageChild; - const initializeBackgroundPage = context => { // Override the `alert()` method inside background windows; // we alias it to console.log(). @@ -185,7 +183,7 @@ export function getContextChildManagerGetter( }; } -class ExtensionBaseContextChild extends BaseContext { +export class ExtensionBaseContextChild extends BaseContext { /** * This ExtensionBaseContextChild represents an addon execution environment * that is running in an addon or devtools child process. @@ -220,12 +218,6 @@ class ExtensionBaseContextChild extends BaseContext { }); } - defineLazyGetter(this, "browserObj", () => { - let browserObj = Cu.createObjectIn(contentWindow); - this.childManager.inject(browserObj); - return browserObj; - }); - lazy.Schemas.exportLazyGetter(contentWindow, "browser", () => { return this.browserObj; }); @@ -245,6 +237,12 @@ class ExtensionBaseContextChild extends BaseContext { }); } + get browserObj() { + const browserObj = Cu.createObjectIn(this.contentWindow); + this.childManager.inject(browserObj); + return redefineGetter(this, "browserObj", browserObj); + } + logActivity(type, name, data) { ExtensionActivityLogChild.log(this, type, name, data); } @@ -283,11 +281,16 @@ class ExtensionBaseContextChild extends BaseContext { super.unload(); } -} -defineLazyGetter(ExtensionBaseContextChild.prototype, "messenger", function () { - return new Messenger(this); -}); + get messenger() { + return redefineGetter(this, "messenger", new Messenger(this)); + } + + /** @type {ReturnType>} */ + get childManager() { + throw new Error("childManager getter must be overridden"); + } +} class ExtensionPageContextChild extends ExtensionBaseContextChild { /** @@ -322,15 +325,16 @@ class ExtensionPageContextChild extends ExtensionBaseContextChild { super.unload(); this.extension.views.delete(this); } -} -defineLazyGetter( - ExtensionPageContextChild.prototype, - "childManager", - getContextChildManagerGetter({ envType: "addon_parent" }) -); + get childManager() { + const childManager = getContextChildManagerGetter({ + envType: "addon_parent", + }).call(this); + return redefineGetter(this, "childManager", childManager); + } +} -class DevToolsContextChild extends ExtensionBaseContextChild { +export class DevToolsContextChild extends ExtensionBaseContextChild { /** * This DevToolsContextChild represents a devtools-related addon execution * environment that has access to the devtools API namespace and to the same subset @@ -361,15 +365,16 @@ class DevToolsContextChild extends ExtensionBaseContextChild { super.unload(); this.extension.devtoolsViews.delete(this); } -} -defineLazyGetter( - DevToolsContextChild.prototype, - "childManager", - getContextChildManagerGetter({ envType: "devtools_parent" }) -); + get childManager() { + const childManager = getContextChildManagerGetter({ + envType: "devtools_parent", + }).call(this); + return redefineGetter(this, "childManager", childManager); + } +} -ExtensionPageChild = { +export var ExtensionPageChild = { initialized: false, // Map diff --git a/toolkit/components/extensions/ExtensionParent.sys.mjs b/toolkit/components/extensions/ExtensionParent.sys.mjs index 1028ac62124159..da90a62954da75 100644 --- a/toolkit/components/extensions/ExtensionParent.sys.mjs +++ b/toolkit/components/extensions/ExtensionParent.sys.mjs @@ -46,7 +46,7 @@ const DUMMY_PAGE_URI = Services.io.newURI( "chrome://extensions/content/dummy.xhtml" ); -var { BaseContext, CanOfAPIs, SchemaAPIManager, SpreadArgs, defineLazyGetter } = +var { BaseContext, CanOfAPIs, SchemaAPIManager, SpreadArgs, redefineGetter } = ExtensionCommon; var { @@ -72,7 +72,6 @@ schemaURLs.add("chrome://extensions/content/schemas/experiments.json"); let GlobalManager; let ParentAPIManager; -let StartupCache; function verifyActorForContext(actor, context) { if (JSWindowActorParent.isInstance(actor)) { @@ -664,26 +663,25 @@ class ProxyContextParent extends BaseContext { super.unload(); apiManager.emit("proxy-context-unload", this); } -} -defineLazyGetter(ProxyContextParent.prototype, "apiCan", function () { - let obj = {}; - let can = new CanOfAPIs(this, this.extension.apiManager, obj); - return can; -}); + get apiCan() { + const apiCan = new CanOfAPIs(this, this.extension.apiManager, {}); + return redefineGetter(this, "apiCan", apiCan); + } -defineLazyGetter(ProxyContextParent.prototype, "apiObj", function () { - return this.apiCan.root; -}); + get apiObj() { + return redefineGetter(this, "apiObj", this.apiCan.root); + } -defineLazyGetter(ProxyContextParent.prototype, "sandbox", function () { - // NOTE: the required Blob and URL globals are used in the ext-registerContentScript.js - // API module to convert JS and CSS data into blob URLs. - return Cu.Sandbox(this.principal, { - sandboxName: this.uri.spec, - wantGlobalProperties: ["Blob", "URL"], - }); -}); + get sandbox() { + // Note: Blob and URL globals are used in ext-contentScripts.js. + const sandbox = Cu.Sandbox(this.principal, { + sandboxName: this.uri.spec, + wantGlobalProperties: ["Blob", "URL"], + }); + return redefineGetter(this, "sandbox", sandbox); + } +} /** * The parent side of proxied API context for extension content script @@ -728,11 +726,6 @@ class ExtensionPageContextParent extends ProxyContextParent { } } - onBrowserChange(browser) { - super.onBrowserChange(browser); - this.xulBrowser = browser; - } - unload() { super.unload(); this.extension.views.delete(this); @@ -1404,7 +1397,7 @@ class HiddenXULWindow { } } - let awaitFrameLoader = Promise.resolve(); + let awaitFrameLoader; if (browser.getAttribute("remote") === "true") { awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated"); @@ -2055,20 +2048,76 @@ let IconDetails = { }, }; +class CacheStore { + constructor(storeName) { + this.storeName = storeName; + } + + async getStore(path = null) { + let data = await StartupCache.dataPromise; + + let store = data.get(this.storeName); + if (!store) { + store = new Map(); + data.set(this.storeName, store); + } + + let key = path; + if (Array.isArray(path)) { + for (let elem of path.slice(0, -1)) { + let next = store.get(elem); + if (!next) { + next = new Map(); + store.set(elem, next); + } + store = next; + } + key = path[path.length - 1]; + } + + return [store, key]; + } + + async get(path, createFunc) { + let [store, key] = await this.getStore(path); + + let result = store.get(key); + + if (result === undefined) { + result = await createFunc(path); + store.set(key, result); + StartupCache.save(); + } + + return result; + } + + async set(path, value) { + let [store, key] = await this.getStore(path); + + store.set(key, value); + StartupCache.save(); + } + + async getAll() { + let [store] = await this.getStore(); + + return new Map(store); + } + + async delete(path) { + let [store, key] = await this.getStore(path); + + if (store.delete(key)) { + StartupCache.save(); + } + } +} + // A cache to support faster initialization of extensions at browser startup. // All cached data is removed when the browser is updated. // Extension-specific data is removed when the add-on is updated. -StartupCache = { - STORE_NAMES: Object.freeze([ - "general", - "locales", - "manifests", - "other", - "permissions", - "schemas", - "menus", - ]), - +var StartupCache = { _ensureDirectoryPromise: null, _saveTask: null, @@ -2179,80 +2228,18 @@ StartupCache = { delete(extension, path) { return this.general.delete([extension.id, extension.version, ...path]); }, + + general: new CacheStore("general"), + locales: new CacheStore("locales"), + manifests: new CacheStore("manifests"), + other: new CacheStore("other"), + permissions: new CacheStore("permissions"), + schemas: new CacheStore("schemas"), + menus: new CacheStore("menus"), }; Services.obs.addObserver(StartupCache, "startupcache-invalidate"); -class CacheStore { - constructor(storeName) { - this.storeName = storeName; - } - - async getStore(path = null) { - let data = await StartupCache.dataPromise; - - let store = data.get(this.storeName); - if (!store) { - store = new Map(); - data.set(this.storeName, store); - } - - let key = path; - if (Array.isArray(path)) { - for (let elem of path.slice(0, -1)) { - let next = store.get(elem); - if (!next) { - next = new Map(); - store.set(elem, next); - } - store = next; - } - key = path[path.length - 1]; - } - - return [store, key]; - } - - async get(path, createFunc) { - let [store, key] = await this.getStore(path); - - let result = store.get(key); - - if (result === undefined) { - result = await createFunc(path); - store.set(key, result); - StartupCache.save(); - } - - return result; - } - - async set(path, value) { - let [store, key] = await this.getStore(path); - - store.set(key, value); - StartupCache.save(); - } - - async getAll() { - let [store] = await this.getStore(); - - return new Map(store); - } - - async delete(path) { - let [store, key] = await this.getStore(path); - - if (store.delete(key)) { - StartupCache.save(); - } - } -} - -for (let name of StartupCache.STORE_NAMES) { - StartupCache[name] = new CacheStore(name); -} - export var ExtensionParent = { GlobalManager, HiddenExtensionPage, diff --git a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs index 799cf516b8c32b..7b30fa2cdfa734 100644 --- a/toolkit/components/extensions/ExtensionShortcuts.sys.mjs +++ b/toolkit/components/extensions/ExtensionShortcuts.sys.mjs @@ -108,7 +108,7 @@ export class ExtensionShortcutKeyMap extends DefaultMap { // Class internals. constructor() { - super(); + super(() => new Set()); // Overridden in some unit test to make it easier to cover some // platform specific behaviors (in particular the platform specific. @@ -116,10 +116,6 @@ export class ExtensionShortcutKeyMap extends DefaultMap { this._os = lazy.ExtensionParent.PlatformInfo.os; } - defaultConstructor() { - return new Set(); - } - getPlatformShortcutString(shortcutString) { if (this._os == "mac") { // when running on macos, make sure to also track in the shortcutKeyMap @@ -150,7 +146,7 @@ export class ExtensionShortcutKeyMap extends DefaultMap { delete(shortcutString) { const platformShortcut = this.getPlatformShortcutString(shortcutString); - super.delete(platformShortcut); + return super.delete(platformShortcut); } } @@ -397,7 +393,7 @@ export class ExtensionShortcuts { this.keysetsMap.get(window).remove(); } let sidebarKey; - commands.forEach((command, name) => { + for (let [name, command] of commands) { if (command.shortcut) { let parts = command.shortcut.split("+"); @@ -419,7 +415,7 @@ export class ExtensionShortcuts { sidebarKey = keyElement; } } - }); + } doc.documentElement.appendChild(keyset); if (sidebarKey) { window.SidebarUI.updateShortcut({ keyId: sidebarKey.id }); diff --git a/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs b/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs index 72a14ef9b223eb..3f338aae9076f4 100644 --- a/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorageIDB.sys.mjs @@ -2,7 +2,6 @@ * 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/. */ -export let ExtensionStorageIDB; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { IndexedDB } from "resource://gre/modules/IndexedDB.sys.mjs"; @@ -238,10 +237,7 @@ class ExtensionStorageLocalIDB extends IndexedDB { // Explicitly create a transaction, so that we can explicitly abort it // as soon as one of the put requests fails. const transaction = this.transaction(IDB_DATA_STORENAME, "readwrite"); - const objectStore = transaction.objectStore( - IDB_DATA_STORENAME, - "readwrite" - ); + const objectStore = transaction.objectStore(IDB_DATA_STORENAME); const transactionCompleted = transaction.promiseComplete(); if (!serialize) { @@ -585,7 +581,7 @@ async function migrateJSONFileData(extension, storagePrincipal) { * This ExtensionStorage class implements a backend for the storage.local API which * uses IndexedDB to store the data. */ -ExtensionStorageIDB = { +export var ExtensionStorageIDB = { BACKEND_ENABLED_PREF, IDB_MIGRATED_PREF_BRANCH, IDB_MIGRATE_RESULT_HISTOGRAM, diff --git a/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs b/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs index 95200a835e3e33..d10b140f7ef17e 100644 --- a/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs +++ b/toolkit/components/extensions/ExtensionStorageSyncKinto.sys.mjs @@ -373,6 +373,7 @@ async function storageSyncInit() { } return storageSyncInit.promise; } +storageSyncInit.promise = undefined; // Kinto record IDs have two conditions: // diff --git a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs index 9cdc2ec5111b52..06137b9a23fa2f 100644 --- a/toolkit/components/extensions/ExtensionTelemetry.sys.mjs +++ b/toolkit/components/extensions/ExtensionTelemetry.sys.mjs @@ -330,7 +330,7 @@ export var ExtensionTelemetry = new Proxy(metricsCache, { // telemetry histogram counterpart, we would need to change this check // accordingly. if (!(prop in HISTOGRAMS_IDS)) { - throw new Error(`Unknown metric ${prop}`); + throw new Error(`Unknown metric ${String(prop)}`); } // Lazily create and cache the metric result object. diff --git a/toolkit/components/extensions/ExtensionTestCommon.sys.mjs b/toolkit/components/extensions/ExtensionTestCommon.sys.mjs index 760eba7749f59e..94cb801cc5edf5 100644 --- a/toolkit/components/extensions/ExtensionTestCommon.sys.mjs +++ b/toolkit/components/extensions/ExtensionTestCommon.sys.mjs @@ -36,8 +36,6 @@ const { flushJarCache } = ExtensionUtils; const { instanceOf } = ExtensionCommon; -export var ExtensionTestCommon; - /** * A skeleton Extension-like object, used for testing, which installs an * add-on via the add-on manager when startup() is called, and @@ -226,7 +224,7 @@ const ExtensionTestAssertions = { const { persistentListeners } = extension; if ( - !persistentListeners?.size > 0 || + !persistentListeners?.size || !persistentListeners.get(apiNs)?.has(apiEvent) ) { return []; @@ -283,7 +281,7 @@ const ExtensionTestAssertions = { }, }; -ExtensionTestCommon = class ExtensionTestCommon { +export var ExtensionTestCommon = class ExtensionTestCommon { static get testAssertions() { return ExtensionTestAssertions; } diff --git a/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs index 77089492f5d825..f93f6968e93cde 100644 --- a/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs +++ b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs @@ -21,7 +21,7 @@ import { } from "resource://gre/modules/ExtensionPageChild.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; -const { BaseContext, defineLazyGetter } = ExtensionCommon; +const { BaseContext, redefineGetter } = ExtensionCommon; const { ChildAPIManager, @@ -154,14 +154,12 @@ class WorkerPort extends Port { api.portId = this.portId; return api; } -} -defineLazyGetter(WorkerPort.prototype, "api", function () { - // No need to clone the API object for the worker, because it runs - // on a different JSRuntime and doesn't have direct access to this - // object. - return this.getAPI(); -}); + get api() { + // No need to clone this for the worker, it's on a separate JSRuntime. + return redefineGetter(this, "api", this.getAPI()); + } +} /** * A Messenger subclass specialized for the background service worker. @@ -697,20 +695,19 @@ class WorkerContextChild extends BaseContext { super.unload(); } -} -defineLazyGetter(WorkerContextChild.prototype, "messenger", function () { - return new WorkerMessenger(this); -}); - -defineLazyGetter( - WorkerContextChild.prototype, - "childManager", - getContextChildManagerGetter( - { envType: "addon_parent" }, - WebIDLChildAPIManager - ) -); + get childManager() { + const childManager = getContextChildManagerGetter( + { envType: "addon_parent" }, + WebIDLChildAPIManager + ).call(this); + return redefineGetter(this, "childManager", childManager); + } + + get messenger() { + return redefineGetter(this, "messenger", new WorkerMessenger(this)); + } +} export var ExtensionWorkerChild = { /** @type {Map} */ diff --git a/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs b/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs index 1ef9dcb56902a8..08df6b3c24ffd8 100644 --- a/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs +++ b/toolkit/components/extensions/ExtensionXPCShellUtils.sys.mjs @@ -32,6 +32,13 @@ let BASE_MANIFEST = Object.freeze({ }); class ExtensionWrapper { + /** @type {AddonWrapper} */ + addon; + /** @type {Promise} */ + addonPromise; + /** @type {nsIFile[]} */ + cleanupFiles; + constructor(testScope, extension = null) { this.testScope = testScope; @@ -447,20 +454,13 @@ class AOMExtensionWrapper extends ExtensionWrapper { return super.unload(); } - async upgrade(data) { - this.startupPromise = new Promise(resolve => { - this.resolveStartup = resolve; - }); - this.state = "restarting"; - - await this._flushCache(); - - let xpiFile = lazy.ExtensionTestCommon.generateXPI(data); - - this.cleanupFiles.push(xpiFile); - - return this._install(xpiFile); - } + /** + * Override for subclasses which don't set an ID in the constructor. + * + * @param {nsIURI} uri + * @param {string} id + */ + maybeSetID(uri, id) {} } class InstallableWrapper extends AOMExtensionWrapper { @@ -574,6 +574,21 @@ class InstallableWrapper extends AOMExtensionWrapper { return this._install(this.file); } + + async upgrade(data) { + this.startupPromise = new Promise(resolve => { + this.resolveStartup = resolve; + }); + this.state = "restarting"; + + await this._flushCache(); + + let xpiFile = lazy.ExtensionTestCommon.generateXPI(data); + + this.cleanupFiles.push(xpiFile); + + return this._install(xpiFile); + } } class ExternallyInstalledWrapper extends AOMExtensionWrapper { @@ -587,8 +602,6 @@ class ExternallyInstalledWrapper extends AOMExtensionWrapper { this.state = "restarting"; } - - maybeSetID(uri, id) {} } export var ExtensionTestUtils = { @@ -769,7 +782,7 @@ export var ExtensionTestUtils = { * * @returns {XPCShellContentUtils.ContentPage} */ - loadContentPage(url, options, ...args) { - return XPCShellContentUtils.loadContentPage(url, options, ...args); + loadContentPage(url, options) { + return XPCShellContentUtils.loadContentPage(url, options); }, }; diff --git a/toolkit/components/extensions/NativeMessaging.sys.mjs b/toolkit/components/extensions/NativeMessaging.sys.mjs index a0d75360054357..dcd8fe78078925 100644 --- a/toolkit/components/extensions/NativeMessaging.sys.mjs +++ b/toolkit/components/extensions/NativeMessaging.sys.mjs @@ -40,7 +40,15 @@ const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes"; const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes"; -export var NativeApp = class extends EventEmitter { +XPCOMUtils.defineLazyPreferenceGetter(lazy, "maxRead", PREF_MAX_READ, MAX_READ); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "maxWrite", + PREF_MAX_WRITE, + MAX_WRITE +); + +export class NativeApp extends EventEmitter { /** * @param {BaseContext} context The context that initiated the native app. * @param {string} application The identifier of the native app. @@ -153,7 +161,7 @@ export var NativeApp = class extends EventEmitter { static encodeMessage(context, message) { message = context.jsonStringify(message); let buffer = new TextEncoder().encode(message).buffer; - if (buffer.byteLength > NativeApp.maxWrite) { + if (buffer.byteLength > lazy.maxWrite) { throw new context.Error("Write too big"); } return buffer; @@ -174,9 +182,9 @@ export var NativeApp = class extends EventEmitter { this.readPromise = this.proc.stdout .readUint32() .then(len => { - if (len > NativeApp.maxRead) { + if (len > lazy.maxRead) { throw new ExtensionError( - `Native application tried to send a message of ${len} bytes, which exceeds the limit of ${NativeApp.maxRead} bytes.` + `Native application tried to send a message of ${len} bytes, which exceeds the limit of ${lazy.maxRead} bytes.` ); } return this.proc.stdout.readJSON(len); @@ -265,7 +273,7 @@ export var NativeApp = class extends EventEmitter { let buffer = msg; - if (buffer.byteLength > NativeApp.maxWrite) { + if (buffer.byteLength > lazy.maxWrite) { throw new ExtensionError("Write too big"); } @@ -380,17 +388,4 @@ export var NativeApp = class extends EventEmitter { return result; } -}; - -XPCOMUtils.defineLazyPreferenceGetter( - NativeApp, - "maxRead", - PREF_MAX_READ, - MAX_READ -); -XPCOMUtils.defineLazyPreferenceGetter( - NativeApp, - "maxWrite", - PREF_MAX_WRITE, - MAX_WRITE -); +} diff --git a/toolkit/components/extensions/Schemas.sys.mjs b/toolkit/components/extensions/Schemas.sys.mjs index 8eb16a6d20dcd7..ff7f926705e5d9 100644 --- a/toolkit/components/extensions/Schemas.sys.mjs +++ b/toolkit/components/extensions/Schemas.sys.mjs @@ -39,8 +39,6 @@ XPCOMUtils.defineLazyPreferenceGetter( false ); -export let Schemas; - const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content"; const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged"; @@ -378,9 +376,12 @@ class Context { localize(value, context) { return value; }, + ...params.preprocessors, }; + this.postprocessors = POSTPROCESSORS; - this.isChromeCompat = false; + this.isChromeCompat = params.isChromeCompat ?? false; + this.manifestVersion = params.manifestVersion; this.currentChoices = new Set(); this.choicePathIndex = 0; @@ -390,17 +391,6 @@ class Context { this[method] = params[method].bind(params); } } - - let props = ["isChromeCompat", "manifestVersion", "preprocessors"]; - for (let prop of props) { - if (prop in params) { - if (prop in this && typeof this[prop] == "object") { - Object.assign(this[prop], params[prop]); - } else { - this[prop] = params[prop]; - } - } - } } get choicePath() { @@ -1211,7 +1201,7 @@ const FORMATS = { // Our pattern just checks the format, we could still have invalid // values (e.g., month=99 or month=02 and day=31). Let the Date // constructor do the dirty work of validating. - if (isNaN(new Date(string))) { + if (isNaN(Date.parse(string))) { throw new Error(`Invalid date string ${string}`); } return string; @@ -2554,6 +2544,8 @@ class ValueProperty extends Entry { // Represents a "property" defined in a schema namespace that is not a // constant. class TypeProperty extends Entry { + unsupported = false; + constructor(schema, path, name, type, writable, permissions) { super(schema); this.path = path; @@ -2692,6 +2684,8 @@ class SubModuleProperty extends Entry { // care of validating parameter lists (i.e., handling of optional // parameters and parameter type checking). class CallEntry extends Entry { + hasAsyncCallback = false; + constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) { super(schema); this.path = path; @@ -3118,9 +3112,11 @@ class Namespace extends Map { this._lazySchemas.unshift(...this.superNamespace._lazySchemas); } - for (let type of Object.keys(LOADERS)) { - this[type] = new DefaultMap(() => []); - } + // Keep in sync with LOADERS above. + this.types = new DefaultMap(() => []); + this.properties = new DefaultMap(() => []); + this.functions = new DefaultMap(() => []); + this.events = new DefaultMap(() => []); for (let schema of this._lazySchemas) { for (let type of schema.types || []) { @@ -3652,7 +3648,7 @@ export class SchemaRoot extends Namespace { } } -Schemas = { +export var Schemas = { initialized: false, REVOKE: Symbol("@@revoke"),