Bug 1805526 - Refactor extension.startup() permissions setup, r=robwu
[gecko.git] / toolkit / components / extensions / Extension.jsm
blob36762978ad3e01204dbb53b372f3f9dfb896498b
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/. */
6 "use strict";
8 var EXPORTED_SYMBOLS = [
9   "Dictionary",
10   "Extension",
11   "ExtensionData",
12   "Langpack",
13   "Management",
14   "SitePermission",
15   "ExtensionAddonObserver",
16   "PRIVILEGED_PERMS",
19 /* exported Extension, ExtensionData */
22  * This file is the main entry point for extensions. When an extension
23  * loads, its bootstrap.js file creates a Extension instance
24  * and calls .startup() on it. It calls .shutdown() when the extension
25  * unloads. Extension manages any extension-specific state in
26  * the chrome process.
27  *
28  * TODO(rpl): we are current restricting the extensions to a single process
29  * (set as the current default value of the "dom.ipc.processCount.extension"
30  * preference), if we switch to use more than one extension process, we have to
31  * be sure that all the browser's frameLoader are associated to the same process,
32  * e.g. by enabling the `maychangeremoteness` attribute, and/or setting
33  * `initialBrowsingContextGroupId` attribute to the correct value.
34  *
35  * At that point we are going to keep track of the existing browsers associated to
36  * a webextension to ensure that they are all running in the same process (and we
37  * are also going to do the same with the browser element provided to the
38  * addon debugging Remote Debugging actor, e.g. because the addon has been
39  * reloaded by the user, we have to  ensure that the new extension pages are going
40  * to run in the same process of the existing addon debugging browser element).
41  */
43 const { XPCOMUtils } = ChromeUtils.importESModule(
44   "resource://gre/modules/XPCOMUtils.sys.mjs"
46 const { AppConstants } = ChromeUtils.importESModule(
47   "resource://gre/modules/AppConstants.sys.mjs"
50 const lazy = {};
52 ChromeUtils.defineESModuleGetters(lazy, {
53   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
54   E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
55   ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
56   ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs",
57   Log: "resource://gre/modules/Log.sys.mjs",
58   SITEPERMS_ADDON_TYPE:
59     "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
60 });
62 XPCOMUtils.defineLazyModuleGetters(lazy, {
63   AddonManager: "resource://gre/modules/AddonManager.jsm",
64   AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
65   AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
66   ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
67   ExtensionPreferencesManager:
68     "resource://gre/modules/ExtensionPreferencesManager.jsm",
69   ExtensionProcessScript: "resource://gre/modules/ExtensionProcessScript.jsm",
70   ExtensionScriptingStore: "resource://gre/modules/ExtensionScriptingStore.jsm",
71   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
72   ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
73   ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm",
74   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
75   NetUtil: "resource://gre/modules/NetUtil.jsm",
76   PluralForm: "resource://gre/modules/PluralForm.jsm",
77   Schemas: "resource://gre/modules/Schemas.jsm",
78   ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
80   // These are used for manipulating jar entry paths, which always use Unix
81   // separators.
82   basename: "resource://gre/modules/osfile/ospath_unix.jsm",
83   dirname: "resource://gre/modules/osfile/ospath_unix.jsm",
84 });
86 XPCOMUtils.defineLazyGetter(lazy, "resourceProtocol", () =>
87   Services.io
88     .getProtocolHandler("resource")
89     .QueryInterface(Ci.nsIResProtocolHandler)
92 const { ExtensionCommon } = ChromeUtils.import(
93   "resource://gre/modules/ExtensionCommon.jsm"
95 const { ExtensionParent } = ChromeUtils.import(
96   "resource://gre/modules/ExtensionParent.jsm"
98 const { ExtensionUtils } = ChromeUtils.import(
99   "resource://gre/modules/ExtensionUtils.jsm"
102 XPCOMUtils.defineLazyServiceGetters(lazy, {
103   aomStartup: [
104     "@mozilla.org/addons/addon-manager-startup;1",
105     "amIAddonManagerStartup",
106   ],
107   spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
110 XPCOMUtils.defineLazyPreferenceGetter(
111   lazy,
112   "processCount",
113   "dom.ipc.processCount.extension"
116 // Temporary pref to be turned on when ready.
117 XPCOMUtils.defineLazyPreferenceGetter(
118   lazy,
119   "userContextIsolation",
120   "extensions.userContextIsolation.enabled",
121   false
124 XPCOMUtils.defineLazyPreferenceGetter(
125   lazy,
126   "userContextIsolationDefaultRestricted",
127   "extensions.userContextIsolation.defaults.restricted",
128   "[]"
131 // This pref modifies behavior for MV2.  MV3 is enabled regardless.
132 XPCOMUtils.defineLazyPreferenceGetter(
133   lazy,
134   "eventPagesEnabled",
135   "extensions.eventPages.enabled"
138 var {
139   GlobalManager,
140   ParentAPIManager,
141   StartupCache,
142   apiManager: Management,
143 } = ExtensionParent;
145 const { getUniqueId, promiseTimeout } = ExtensionUtils;
147 const { EventEmitter, updateAllowedOrigins } = ExtensionCommon;
149 XPCOMUtils.defineLazyGetter(
150   lazy,
151   "LocaleData",
152   () => ExtensionCommon.LocaleData
155 XPCOMUtils.defineLazyGetter(lazy, "NO_PROMPT_PERMISSIONS", async () => {
156   // Wait until all extension API schemas have been loaded and parsed.
157   await Management.lazyInit();
158   return new Set(
159     lazy.Schemas.getPermissionNames([
160       "PermissionNoPrompt",
161       "OptionalPermissionNoPrompt",
162       "PermissionPrivileged",
163     ])
164   );
167 // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
168 XPCOMUtils.defineLazyGetter(lazy, "SCHEMA_SITE_PERMISSIONS", async () => {
169   // Wait until all extension API schemas have been loaded and parsed.
170   await Management.lazyInit();
171   return lazy.Schemas.getPermissionNames(["SitePermission"]);
174 const { sharedData } = Services.ppmm;
176 const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed";
177 const SVG_CONTEXT_PROPERTIES_PERMISSION =
178   "internal:svgContextPropertiesAllowed";
180 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
181 // storage used by the browser.storage.local API is not directly accessible from the extension code,
182 // it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.sys.mjs).
183 const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;
185 // The maximum time to wait for extension child shutdown blockers to complete.
186 const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
188 // Permissions that are only available to privileged extensions.
189 const PRIVILEGED_PERMS = new Set([
190   "activityLog",
191   "mozillaAddons",
192   "networkStatus",
193   "normandyAddonStudy",
194   "telemetry",
195   "urlbar",
198 const PRIVILEGED_PERMS_ANDROID_ONLY = new Set([
199   "geckoViewAddons",
200   "nativeMessagingFromContent",
201   "nativeMessaging",
204 const PRIVILEGED_PERMS_DESKTOP_ONLY = new Set(["normandyAddonStudy", "urlbar"]);
206 if (AppConstants.platform == "android") {
207   for (const perm of PRIVILEGED_PERMS_ANDROID_ONLY) {
208     PRIVILEGED_PERMS.add(perm);
209   }
212 if (
213   AppConstants.MOZ_APP_NAME != "firefox" ||
214   AppConstants.platform == "android"
215 ) {
216   for (const perm of PRIVILEGED_PERMS_DESKTOP_ONLY) {
217     PRIVILEGED_PERMS.delete(perm);
218   }
221 const PREF_DNR_ENABLED = "extensions.dnr.enabled";
223 // Message included in warnings and errors related to privileged permissions and
224 // privileged manifest properties. Provides a link to the firefox-source-docs.mozilla.org
225 // section related to developing and sign Privileged Add-ons.
226 const PRIVILEGED_ADDONS_DEVDOCS_MESSAGE =
227   "See https://mzl.la/3NS9KJd for more details about how to develop a privileged add-on.";
229 const INSTALL_AND_UPDATE_STARTUP_REASONS = new Set([
230   "ADDON_INSTALL",
231   "ADDON_UPGRADE",
232   "ADDON_DOWNGRADE",
235 const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
236 const PERMISSION_KEY_DELIMITER = "^";
238 // Returns true if the extension is owned by Mozilla (is either privileged,
239 // using one of the @mozilla.com/@mozilla.org protected addon id suffixes).
241 // This method throws if the extension's startupReason is not one of the expected
242 // ones (either ADDON_INSTALL, ADDON_UPGRADE or ADDON_DOWNGRADE).
244 // NOTE: This methos is internally referring to "addonData.recommendationState" to
245 // identify a Mozilla line extension. That property is part of the addonData only when
246 // the extension is installed or updated, and so we enforce the expected
247 // startup reason values to prevent it from silently returning different results
248 // if called with an unexpected startupReason.
249 function isMozillaExtension(extension) {
250   const { addonData, id, isPrivileged, startupReason } = extension;
252   if (!INSTALL_AND_UPDATE_STARTUP_REASONS.has(startupReason)) {
253     throw new Error(
254       `isMozillaExtension called with unexpected startupReason: ${startupReason}`
255     );
256   }
258   if (isPrivileged) {
259     return true;
260   }
262   if (id.endsWith("@mozilla.com") || id.endsWith("@mozilla.org")) {
263     return true;
264   }
266   // This check is a subset of what is being checked in AddonWrapper's
267   // recommendationStates (states expire dates for line extensions are
268   // not consideredcimportant in determining that the extension is
269   // provided by mozilla, and so they are omitted here on purpose).
270   const isMozillaLineExtension = addonData.recommendationState?.states?.includes(
271     "line"
272   );
273   const isSigned =
274     addonData.signedState > lazy.AddonManager.SIGNEDSTATE_MISSING;
276   return isSigned && isMozillaLineExtension;
279 function isDNRPermissionAllowed(perm) {
280   // DNR is under development and therefore disabled by default for now.
281   if (!Services.prefs.getBoolPref(PREF_DNR_ENABLED, false)) {
282     return false;
283   }
285   return true;
289  * Classify an individual permission from a webextension manifest
290  * as a host/origin permission, an api permission, or a regular permission.
292  * @param {string} perm  The permission string to classify
293  * @param {boolean} restrictSchemes
294  * @param {boolean} isPrivileged whether or not the webextension is privileged
296  * @returns {object}
297  *          An object with exactly one of the following properties:
298  *          "origin" to indicate this is a host/origin permission.
299  *          "api" to indicate this is an api permission
300  *                (as used for webextensions experiments).
301  *          "permission" to indicate this is a regular permission.
302  *          "invalid" to indicate that the given permission cannot be used.
303  */
304 function classifyPermission(perm, restrictSchemes, isPrivileged) {
305   let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
306   if (!match) {
307     try {
308       let { pattern } = new MatchPattern(perm, {
309         restrictSchemes,
310         ignorePath: true,
311       });
312       return { origin: pattern };
313     } catch (e) {
314       return { invalid: perm };
315     }
316   } else if (match[1] == "experiments" && match[2]) {
317     return { api: match[2] };
318   } else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) {
319     return { invalid: perm, privileged: true };
320   } else if (
321     perm.startsWith("declarativeNetRequest") &&
322     !isDNRPermissionAllowed(perm)
323   ) {
324     return { invalid: perm };
325   }
326   return { permission: perm };
329 const LOGGER_ID_BASE = "addons.webextension.";
330 const UUID_MAP_PREF = "extensions.webextensions.uuids";
331 const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
332 const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
334 const COMMENT_REGEXP = new RegExp(
335   String.raw`
336     ^
337     (
338       (?:
339         [^"\n] |
340         " (?:[^"\\\n] | \\.)* "
341       )*?
342     )
344     //.*
345   `.replace(/\s+/g, ""),
346   "gm"
349 // All moz-extension URIs use a machine-specific UUID rather than the
350 // extension's own ID in the host component. This makes it more
351 // difficult for web pages to detect whether a user has a given add-on
352 // installed (by trying to load a moz-extension URI referring to a
353 // web_accessible_resource from the extension). UUIDMap.get()
354 // returns the UUID for a given add-on ID.
355 var UUIDMap = {
356   _read() {
357     let pref = Services.prefs.getStringPref(UUID_MAP_PREF, "{}");
358     try {
359       return JSON.parse(pref);
360     } catch (e) {
361       Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
362       return {};
363     }
364   },
366   _write(map) {
367     Services.prefs.setStringPref(UUID_MAP_PREF, JSON.stringify(map));
368   },
370   get(id, create = true) {
371     let map = this._read();
373     if (id in map) {
374       return map[id];
375     }
377     let uuid = null;
378     if (create) {
379       uuid = Services.uuid.generateUUID().number;
380       uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
382       map[id] = uuid;
383       this._write(map);
384     }
385     return uuid;
386   },
388   remove(id) {
389     let map = this._read();
390     delete map[id];
391     this._write(map);
392   },
395 function clearCacheForExtensionPrincipal(principal, clearAll = false) {
396   if (!principal.schemeIs("moz-extension")) {
397     return Promise.reject(new Error("Unexpected non extension principal"));
398   }
400   // TODO(Bug 1750053): replace the two specific flags with a "clear all caches one"
401   // (along with covering the other kind of cached data with tests).
402   const clearDataFlags = clearAll
403     ? Ci.nsIClearDataService.CLEAR_ALL_CACHES
404     : Ci.nsIClearDataService.CLEAR_IMAGE_CACHE |
405       Ci.nsIClearDataService.CLEAR_CSS_CACHE;
407   return new Promise(resolve =>
408     Services.clearData.deleteDataFromPrincipal(
409       principal,
410       false,
411       clearDataFlags,
412       () => resolve()
413     )
414   );
418  * Observer AddonManager events and translate them into extension events,
419  * as well as handle any last cleanup after uninstalling an extension.
420  */
421 var ExtensionAddonObserver = {
422   initialized: false,
424   init() {
425     if (!this.initialized) {
426       lazy.AddonManager.addAddonListener(this);
427       this.initialized = true;
428     }
429   },
431   // AddonTestUtils will call this as necessary.
432   uninit() {
433     if (this.initialized) {
434       lazy.AddonManager.removeAddonListener(this);
435       this.initialized = false;
436     }
437   },
439   onEnabling(addon) {
440     if (addon.type !== "extension") {
441       return;
442     }
443     Management._callHandlers([addon.id], "enabling", "onEnabling");
444   },
446   onDisabled(addon) {
447     if (addon.type !== "extension") {
448       return;
449     }
450     if (Services.appinfo.inSafeMode) {
451       // Ensure ExtensionPreferencesManager updates its data and
452       // modules can run any disable logic they need to.  We only
453       // handle safeMode here because there is a bunch of additional
454       // logic that happens in Extension.shutdown when running in
455       // normal mode.
456       Management._callHandlers([addon.id], "disable", "onDisable");
457     }
458   },
460   onUninstalling(addon) {
461     let extension = GlobalManager.extensionMap.get(addon.id);
462     if (extension) {
463       // Let any other interested listeners respond
464       // (e.g., display the uninstall URL)
465       Management.emit("uninstalling", extension);
466     }
467   },
469   onUninstalled(addon) {
470     // Cleanup anything that is used by non-extension addon types
471     // since only extensions have uuid's.
472     lazy.ExtensionPermissions.removeAll(addon.id);
474     let uuid = UUIDMap.get(addon.id, false);
475     if (!uuid) {
476       return;
477     }
479     let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
480     let principal = Services.scriptSecurityManager.createContentPrincipal(
481       baseURI,
482       {}
483     );
485     // Clear all cached resources (e.g. CSS and images);
486     lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
487       `Clear cache for ${addon.id}`,
488       clearCacheForExtensionPrincipal(principal, /* clearAll */ true)
489     );
491     // Clear all the registered service workers for the extension
492     // principal (the one that may have been registered through the
493     // manifest.json file and the ones that may have been registered
494     // from an extension page through the service worker API).
495     //
496     // Any stored data would be cleared below (if the pref
497     // "extensions.webextensions.keepStorageOnUninstall has not been
498     // explicitly set to true, which is usually only done in
499     // tests and by some extensions developers for testing purpose).
500     //
501     // TODO: ServiceWorkerCleanUp may go away once Bug 1183245
502     // is fixed, and so this may actually go away, replaced by
503     // marking the registration as disabled or to be removed on
504     // shutdown (where we do know if the extension is shutting
505     // down because is being uninstalled) and then cleared from
506     // the persisted serviceworker registration on the next
507     // startup.
508     lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
509       `Clear ServiceWorkers for ${addon.id}`,
510       lazy.ServiceWorkerCleanUp.removeFromPrincipal(principal)
511     );
513     // Clear the persisted dynamic content scripts created with the scripting
514     // API (if any).
515     lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
516       `Clear scripting store for ${addon.id}`,
517       lazy.ExtensionScriptingStore.clearOnUninstall(addon.id)
518     );
520     // Clear the DNR API's rules data persisted on disk (if any).
521     lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
522       `Clear declarativeNetRequest store for ${addon.id}`,
523       lazy.ExtensionDNRStore.clearOnUninstall(uuid)
524     );
526     if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) {
527       // Clear browser.storage.local backends.
528       lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
529         `Clear Extension Storage ${addon.id} (File Backend)`,
530         lazy.ExtensionStorage.clear(addon.id, { shouldNotifyListeners: false })
531       );
533       // Clear any IndexedDB and Cache API storage created by the extension.
534       // If LSNG is enabled, this also clears localStorage.
535       Services.qms.clearStoragesForPrincipal(principal);
537       // Clear any storage.local data stored in the IDBBackend.
538       let storagePrincipal = Services.scriptSecurityManager.createContentPrincipal(
539         baseURI,
540         {
541           userContextId: WEBEXT_STORAGE_USER_CONTEXT_ID,
542         }
543       );
544       Services.qms.clearStoragesForPrincipal(storagePrincipal);
546       lazy.ExtensionStorageIDB.clearMigratedExtensionPref(addon.id);
548       // If LSNG is not enabled, we need to clear localStorage explicitly using
549       // the old API.
550       if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
551         // Clear localStorage created by the extension
552         let storage = Services.domStorageManager.getStorage(
553           null,
554           principal,
555           principal
556         );
557         if (storage) {
558           storage.clear();
559         }
560       }
562       // Remove any permissions related to the unlimitedStorage permission
563       // if we are also removing all the data stored by the extension.
564       Services.perms.removeFromPrincipal(
565         principal,
566         "WebExtensions-unlimitedStorage"
567       );
568       Services.perms.removeFromPrincipal(principal, "persistent-storage");
569     }
571     // Clear any protocol handler permissions granted to this add-on.
572     let permissions = Services.perms.getAllWithTypePrefix(
573       PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER
574     );
575     for (let perm of permissions) {
576       if (perm.principal.equalsURI(baseURI)) {
577         Services.perms.removePermission(perm);
578       }
579     }
581     if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) {
582       // Clear the entry in the UUID map
583       UUIDMap.remove(addon.id);
584     }
585   },
588 ExtensionAddonObserver.init();
590 const manifestTypes = new Map([
591   ["theme", "manifest.ThemeManifest"],
592   ["locale", "manifest.WebExtensionLangpackManifest"],
593   ["dictionary", "manifest.WebExtensionDictionaryManifest"],
594   ["extension", "manifest.WebExtensionManifest"],
595   // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
596   ["sitepermission-deprecated", "manifest.WebExtensionSitePermissionsManifest"],
600  * Represents the data contained in an extension, contained either
601  * in a directory or a zip file, which may or may not be installed.
602  * This class implements the functionality of the Extension class,
603  * primarily related to manifest parsing and localization, which is
604  * useful prior to extension installation or initialization.
606  * No functionality of this class is guaranteed to work before
607  * `loadManifest` has been called, and completed.
608  */
609 class ExtensionData {
610   constructor(rootURI, isPrivileged = false) {
611     this.rootURI = rootURI;
612     this.resourceURL = rootURI.spec;
613     this.isPrivileged = isPrivileged;
615     this.manifest = null;
616     this.type = null;
617     this.id = null;
618     this.uuid = null;
619     this.localeData = null;
620     this.fluentL10n = null;
621     this._promiseLocales = null;
623     this.apiNames = new Set();
624     this.dependencies = new Set();
625     this.permissions = new Set();
627     this.startupData = null;
629     this.errors = [];
630     this.warnings = [];
631     this.eventPagesEnabled = lazy.eventPagesEnabled;
632   }
634   /**
635    * A factory function that allows the construction of ExtensionData, with
636    * the isPrivileged flag computed asynchronously.
637    *
638    * @param {object} options
639    * @param {nsIURI} options.rootURI
640    *  The URI pointing to the extension root.
641    * @param {function(type, id)} options.checkPrivileged
642    *  An (async) function that takes the addon type and addon ID and returns
643    *  whether the given add-on is privileged.
644    * @param {boolean} options.temporarilyInstalled
645    *  whether the given add-on is installed as temporary.
646    * @returns {ExtensionData}
647    */
648   static async constructAsync({
649     rootURI,
650     checkPrivileged,
651     temporarilyInstalled,
652   }) {
653     let extension = new ExtensionData(rootURI);
654     // checkPrivileged depends on the extension type and id.
655     await extension.initializeAddonTypeAndID();
656     let { type, id } = extension;
657     extension.isPrivileged = await checkPrivileged(type, id);
658     extension.temporarilyInstalled = temporarilyInstalled;
659     return extension;
660   }
662   static getIsPrivileged({ signedState, builtIn, temporarilyInstalled }) {
663     return (
664       signedState === lazy.AddonManager.SIGNEDSTATE_PRIVILEGED ||
665       signedState === lazy.AddonManager.SIGNEDSTATE_SYSTEM ||
666       builtIn ||
667       (lazy.AddonSettings.EXPERIMENTS_ENABLED && temporarilyInstalled)
668     );
669   }
671   get builtinMessages() {
672     return null;
673   }
675   get logger() {
676     let id = this.id || "<unknown>";
677     return lazy.Log.repository.getLogger(LOGGER_ID_BASE + id);
678   }
680   /**
681    * Report an error about the extension's manifest file.
682    *
683    * @param {string} message The error message
684    */
685   manifestError(message) {
686     this.packagingError(`Reading manifest: ${message}`);
687   }
689   /**
690    * Report a warning about the extension's manifest file.
691    *
692    * @param {string} message The warning message
693    */
694   manifestWarning(message) {
695     this.packagingWarning(`Reading manifest: ${message}`);
696   }
698   // Report an error about the extension's general packaging.
699   packagingError(message) {
700     this.errors.push(message);
701     this.logError(message);
702   }
704   packagingWarning(message) {
705     this.warnings.push(message);
706     this.logWarning(message);
707   }
709   logWarning(message) {
710     this._logMessage(message, "warn");
711   }
713   logError(message) {
714     this._logMessage(message, "error");
715   }
717   _logMessage(message, severity) {
718     this.logger[severity](`Loading extension '${this.id}': ${message}`);
719   }
721   ensureNoErrors() {
722     if (this.errors.length) {
723       // startup() repeatedly checks whether there are errors after parsing the
724       // extension/manifest before proceeding with starting up.
725       throw new Error(this.errors.join("\n"));
726     }
727   }
729   /**
730    * Returns the moz-extension: URL for the given path within this
731    * extension.
732    *
733    * Must not be called unless either the `id` or `uuid` property has
734    * already been set.
735    *
736    * @param {string} path The path portion of the URL.
737    * @returns {string}
738    */
739   getURL(path = "") {
740     if (!(this.id || this.uuid)) {
741       throw new Error(
742         "getURL may not be called before an `id` or `uuid` has been set"
743       );
744     }
745     if (!this.uuid) {
746       this.uuid = UUIDMap.get(this.id);
747     }
748     return `moz-extension://${this.uuid}/${path}`;
749   }
751   /**
752    * Discovers the file names within a directory or JAR file.
753    *
754    * @param {Ci.nsIFileURL|Ci.nsIJARURI} path
755    *   The path to the directory or jar file to look at.
756    * @param {boolean} [directoriesOnly]
757    *   If true, this will return only the directories present within the directory.
758    * @returns {string[]}
759    *   An array of names of files/directories (only the name, not the path).
760    */
761   async _readDirectory(path, directoriesOnly = false) {
762     if (this.rootURI instanceof Ci.nsIFileURL) {
763       let uri = Services.io.newURI("./" + path, null, this.rootURI);
764       let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
766       let results = [];
767       try {
768         let children = await IOUtils.getChildren(fullPath);
769         for (let child of children) {
770           if (
771             !directoriesOnly ||
772             (await IOUtils.stat(child)).type == "directory"
773           ) {
774             results.push(PathUtils.filename(child));
775           }
776         }
777       } catch (ex) {
778         // Fall-through, return what we have.
779       }
780       return results;
781     }
783     let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
785     // Append the sub-directory path to the base JAR URI and normalize the
786     // result.
787     let entry = `${uri.JAREntry}/${path}/`
788       .replace(/\/\/+/g, "/")
789       .replace(/^\//, "");
790     uri = Services.io.newURI(`jar:${uri.JARFile.spec}!/${entry}`);
792     let results = [];
793     for (let name of lazy.aomStartup.enumerateJARSubtree(uri)) {
794       if (!name.startsWith(entry)) {
795         throw new Error("Unexpected ZipReader entry");
796       }
798       // The enumerator returns the full path of all entries.
799       // Trim off the leading path, and filter out entries from
800       // subdirectories.
801       name = name.slice(entry.length);
802       if (
803         name &&
804         !/\/./.test(name) &&
805         (!directoriesOnly || name.endsWith("/"))
806       ) {
807         results.push(name.replace("/", ""));
808       }
809     }
811     return results;
812   }
814   readJSON(path) {
815     return new Promise((resolve, reject) => {
816       let uri = this.rootURI.resolve(`./${path}`);
818       lazy.NetUtil.asyncFetch(
819         { uri, loadUsingSystemPrincipal: true },
820         (inputStream, status) => {
821           if (!Components.isSuccessCode(status)) {
822             // Convert status code to a string
823             let e = Components.Exception("", status);
824             reject(new Error(`Error while loading '${uri}' (${e.name})`));
825             return;
826           }
827           try {
828             let text = lazy.NetUtil.readInputStreamToString(
829               inputStream,
830               inputStream.available(),
831               { charset: "utf-8" }
832             );
834             text = text.replace(COMMENT_REGEXP, "$1");
836             resolve(JSON.parse(text));
837           } catch (e) {
838             reject(e);
839           }
840         }
841       );
842     });
843   }
845   get restrictSchemes() {
846     return !(this.isPrivileged && this.hasPermission("mozillaAddons"));
847   }
849   /**
850    * Given an array of host and permissions, generate a structured permissions object
851    * that contains seperate host origins and permissions arrays.
852    *
853    * @param {Array} permissionsArray
854    * @param {Array} [hostPermissions]
855    * @returns {object} permissions object
856    */
857   permissionsObject(permissionsArray = [], hostPermissions = []) {
858     let permissions = new Set();
859     let origins = new Set();
860     let { restrictSchemes, isPrivileged } = this;
862     for (let perm of permissionsArray.concat(hostPermissions)) {
863       let type = classifyPermission(perm, restrictSchemes, isPrivileged);
864       if (type.origin) {
865         origins.add(perm);
866       } else if (type.permission) {
867         permissions.add(perm);
868       }
869     }
871     return {
872       permissions,
873       origins,
874     };
875   }
877   /**
878    * Returns an object representing any capabilities that the extension
879    * has access to based on fixed properties in the manifest.  The result
880    * includes the contents of the "permissions" property as well as other
881    * capabilities that are derived from manifest fields that users should
882    * be informed of (e.g., origins where content scripts are injected).
883    */
884   get manifestPermissions() {
885     if (this.type !== "extension") {
886       return null;
887     }
889     let { permissions } = this.permissionsObject(this.manifest.permissions);
891     if (
892       this.manifest.devtools_page &&
893       !this.manifest.optional_permissions.includes("devtools")
894     ) {
895       permissions.add("devtools");
896     }
898     return {
899       permissions: Array.from(permissions),
900       origins: this.originControls ? [] : this.getManifestOrigins(),
901     };
902   }
904   /**
905    * @returns {string[]} all origins that are referenced in manifest via
906    * permissions, host_permissions, or content_scripts keys.
907    */
908   getManifestOrigins() {
909     if (this.type !== "extension") {
910       return null;
911     }
913     let { origins } = this.permissionsObject(
914       this.manifest.permissions,
915       this.manifest.host_permissions
916     );
918     for (let entry of this.manifest.content_scripts || []) {
919       for (let origin of entry.matches) {
920         origins.add(origin);
921       }
922     }
924     return Array.from(origins);
925   }
927   /**
928    * Returns optional permissions from the manifest, including host permissions
929    * if originControls is true.
930    */
931   get manifestOptionalPermissions() {
932     if (this.type !== "extension") {
933       return null;
934     }
936     let { permissions, origins } = this.permissionsObject(
937       this.manifest.optional_permissions
938     );
939     if (this.originControls) {
940       for (let origin of this.getManifestOrigins()) {
941         origins.add(origin);
942       }
943     }
945     return {
946       permissions: Array.from(permissions),
947       origins: Array.from(origins),
948     };
949   }
951   /**
952    * Returns an object representing all capabilities this extension has
953    * access to, including fixed ones from the manifest as well as dynamically
954    * granted permissions.
955    */
956   get activePermissions() {
957     if (this.type !== "extension") {
958       return null;
959     }
961     let result = {
962       origins: this.allowedOrigins.patterns
963         .map(matcher => matcher.pattern)
964         // moz-extension://id/* is always added to allowedOrigins, but it
965         // is not a valid host permission in the API. So, remove it.
966         .filter(pattern => !pattern.startsWith("moz-extension:")),
967       apis: [...this.apiNames],
968     };
970     const EXP_PATTERN = /^experiments\.\w+/;
971     result.permissions = [...this.permissions].filter(
972       p => !result.origins.includes(p) && !EXP_PATTERN.test(p)
973     );
974     return result;
975   }
977   // Returns whether the front end should prompt for this permission
978   static async shouldPromptFor(permission) {
979     return !(await lazy.NO_PROMPT_PERMISSIONS).has(permission);
980   }
982   // Compute the difference between two sets of permissions, suitable
983   // for presenting to the user.
984   static comparePermissions(oldPermissions, newPermissions) {
985     let oldMatcher = new MatchPatternSet(oldPermissions.origins, {
986       restrictSchemes: false,
987     });
988     return {
989       // formatPermissionStrings ignores any scheme, so only look at the domain.
990       origins: newPermissions.origins.filter(
991         perm =>
992           !oldMatcher.subsumesDomain(
993             new MatchPattern(perm, { restrictSchemes: false })
994           )
995       ),
996       permissions: newPermissions.permissions.filter(
997         perm => !oldPermissions.permissions.includes(perm)
998       ),
999     };
1000   }
1002   // Return those permissions in oldPermissions that also exist in newPermissions.
1003   static intersectPermissions(oldPermissions, newPermissions) {
1004     let matcher = new MatchPatternSet(newPermissions.origins, {
1005       restrictSchemes: false,
1006     });
1008     return {
1009       origins: oldPermissions.origins.filter(perm =>
1010         matcher.subsumesDomain(
1011           new MatchPattern(perm, { restrictSchemes: false })
1012         )
1013       ),
1014       permissions: oldPermissions.permissions.filter(perm =>
1015         newPermissions.permissions.includes(perm)
1016       ),
1017     };
1018   }
1020   /**
1021    * When updating the addon, find and migrate permissions that have moved from required
1022    * to optional.  This also handles any updates required for permission removal.
1023    *
1024    * @param {string} id The id of the addon being updated
1025    * @param {object} oldPermissions
1026    * @param {object} oldOptionalPermissions
1027    * @param {object} newPermissions
1028    * @param {object} newOptionalPermissions
1029    */
1030   static async migratePermissions(
1031     id,
1032     oldPermissions,
1033     oldOptionalPermissions,
1034     newPermissions,
1035     newOptionalPermissions
1036   ) {
1037     let migrated = ExtensionData.intersectPermissions(
1038       oldPermissions,
1039       newOptionalPermissions
1040     );
1041     // If a permission is optional in this version and was mandatory in the previous
1042     // version, it was already accepted by the user at install time so add it to the
1043     // list of granted optional permissions now.
1044     await lazy.ExtensionPermissions.add(id, migrated);
1046     // Now we need to update ExtensionPreferencesManager, removing any settings
1047     // for old permissions that no longer exist.
1048     let permSet = new Set(
1049       newPermissions.permissions.concat(newOptionalPermissions.permissions)
1050     );
1051     let oldPerms = oldPermissions.permissions.concat(
1052       oldOptionalPermissions.permissions
1053     );
1055     let removed = oldPerms.filter(x => !permSet.has(x));
1056     // Force the removal here to ensure the settings are removed prior
1057     // to startup.  This will remove both required or optional permissions,
1058     // whereas the call from within ExtensionPermissions would only result
1059     // in a removal for optional permissions that were removed.
1060     await lazy.ExtensionPreferencesManager.removeSettingsForPermissions(
1061       id,
1062       removed
1063     );
1065     // Remove any optional permissions that have been removed from the manifest.
1066     await lazy.ExtensionPermissions.remove(id, {
1067       permissions: removed,
1068       origins: [],
1069     });
1070   }
1072   canUseAPIExperiment() {
1073     return (
1074       this.type == "extension" &&
1075       (this.isPrivileged ||
1076         // TODO(Bug 1771341): Allowing the "experiment_apis" property when only
1077         // AddonSettings.EXPERIMENTS_ENABLED is true is currently needed to allow,
1078         // while running under automation, the test harness extensions (like mochikit
1079         // and specialpowers) to use that privileged manifest property.
1080         lazy.AddonSettings.EXPERIMENTS_ENABLED)
1081     );
1082   }
1084   canUseThemeExperiment() {
1085     return (
1086       ["extension", "theme"].includes(this.type) &&
1087       (this.isPrivileged ||
1088         // "theme_experiment" MDN docs are currently explicitly mentioning this is expected
1089         // to be allowed also for non-signed extensions installed non-temporarily on builds
1090         // where the signature checks can be disabled).
1091         //
1092         // NOTE: be careful to don't regress "theme_experiment" (see Bug 1773076) while changing
1093         // AddonSettings.EXPERIMENTS_ENABLED (e.g. as part of fixing Bug 1771341).
1094         lazy.AddonSettings.EXPERIMENTS_ENABLED)
1095     );
1096   }
1098   get manifestVersion() {
1099     return this.manifest.manifest_version;
1100   }
1102   get persistentBackground() {
1103     let { manifest } = this;
1104     if (
1105       !manifest.background ||
1106       manifest.background.service_worker ||
1107       this.manifestVersion > 2
1108     ) {
1109       return false;
1110     }
1111     // V2 addons can only use event pages if the pref is also flipped and
1112     // persistent is explicilty set to false.
1113     return !this.eventPagesEnabled || manifest.background.persistent;
1114   }
1116   /**
1117    * backgroundState can be starting, running, suspending or stopped.
1118    * It is undefined if the extension has no background page.
1119    * See ext-backgroundPage.js for more details.
1120    *
1121    * @param {string} state starting, running, suspending or stopped
1122    */
1123   set backgroundState(state) {
1124     this._backgroundState = state;
1125   }
1127   get backgroundState() {
1128     return this._backgroundState;
1129   }
1131   async getExtensionVersionWithoutValidation() {
1132     return (await this.readJSON("manifest.json")).version;
1133   }
1135   /**
1136    * Load a locale and return a localized manifest.  The extension must
1137    * be initialized, and manifest parsed prior to calling.
1138    *
1139    * @param {string} locale to load, if necessary.
1140    * @returns {object} normalized manifest.
1141    */
1142   async getLocalizedManifest(locale) {
1143     if (!this.type || !this.localeData) {
1144       throw new Error("The extension has not been initialized.");
1145     }
1146     // Upon update or reinstall, the Extension.manifest may be read from
1147     // StartupCache.manifest, however rawManifest is *not*.  We need the
1148     // raw manifest in order to get a localized manifest.
1149     if (!this.rawManifest) {
1150       this.rawManifest = await this.readJSON("manifest.json");
1151     }
1153     if (!this.localeData.has(locale)) {
1154       // Locales are not avialable until some additional
1155       // initialization is done.  We could just call initAllLocales,
1156       // but that is heavy handed, especially when we likely only
1157       // need one out of 20.
1158       let locales = await this.promiseLocales();
1159       if (locales.get(locale)) {
1160         await this.initLocale(locale);
1161       }
1162       if (!this.localeData.has(locale)) {
1163         throw new Error(`The extension does not contain the locale ${locale}`);
1164       }
1165     }
1166     let normalized = await this._getNormalizedManifest(locale);
1167     if (normalized.error) {
1168       throw new Error(normalized.error);
1169     }
1170     return normalized.value;
1171   }
1173   async _getNormalizedManifest(locale) {
1174     let manifestType = manifestTypes.get(this.type);
1176     let context = {
1177       url: this.baseURI && this.baseURI.spec,
1178       principal: this.principal,
1179       logError: error => {
1180         this.manifestWarning(error);
1181       },
1182       preprocessors: {},
1183       manifestVersion: this.manifestVersion,
1184     };
1186     if (this.fluentL10n || this.localeData) {
1187       context.preprocessors.localize = (value, context) =>
1188         this.localize(value, locale);
1189     }
1191     return lazy.Schemas.normalize(this.rawManifest, manifestType, context);
1192   }
1194   async initializeAddonTypeAndID() {
1195     if (this.type) {
1196       // Already initialized.
1197       return;
1198     }
1199     this.rawManifest = await this.readJSON("manifest.json");
1200     let manifest = this.rawManifest;
1202     if (manifest.theme) {
1203       this.type = "theme";
1204     } else if (manifest.langpack_id) {
1205       this.type = "locale";
1206     } else if (manifest.dictionaries) {
1207       this.type = "dictionary";
1208     } else if (manifest.site_permissions) {
1209       // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
1210       this.type = "sitepermission-deprecated";
1211     } else {
1212       this.type = "extension";
1213     }
1215     if (!this.id) {
1216       let bss =
1217         manifest.browser_specific_settings?.gecko ||
1218         manifest.applications?.gecko;
1219       let id = bss?.id;
1220       // This is a basic type check.
1221       // When parseManifest is called, the ID is validated more thoroughly
1222       // because the id is defined to be an ExtensionID type in
1223       // toolkit/components/extensions/schemas/manifest.json
1224       if (typeof id == "string") {
1225         this.id = id;
1226       }
1227     }
1228   }
1230   // eslint-disable-next-line complexity
1231   async parseManifest() {
1232     await Promise.all([this.initializeAddonTypeAndID(), Management.lazyInit()]);
1234     let manifest = this.rawManifest;
1235     this.manifest = manifest;
1237     if (manifest.default_locale) {
1238       await this.initLocale();
1239     }
1241     if (manifest.l10n_resources) {
1242       if (this.isPrivileged) {
1243         // TODO (Bug 1733466): For historical reasons fluent isn't being used to
1244         // localize manifest properties read from the add-on manager (e.g., author,
1245         // homepage, etc.), the changes introduced by Bug 1734987 does now ensure
1246         // that isPrivileged will be set while parsing the manifest and so this
1247         // can be now supported but requires some additional changes, being tracked
1248         // by Bug 1733466.
1249         if (this.constructor != ExtensionData) {
1250           this.fluentL10n = new Localization(manifest.l10n_resources, true);
1251         }
1252       } else if (this.temporarilyInstalled) {
1253         this.manifestError(
1254           `Using 'l10n_resources' requires a privileged add-on. ` +
1255             PRIVILEGED_ADDONS_DEVDOCS_MESSAGE
1256         );
1257       } else {
1258         // Warn but don't make this fatal.
1259         this.manifestWarning(
1260           "Ignoring l10n_resources in unprivileged extension"
1261         );
1262       }
1263     }
1265     let normalized = await this._getNormalizedManifest();
1266     if (normalized.error) {
1267       this.manifestError(normalized.error);
1268       return null;
1269     }
1271     manifest = normalized.value;
1273     // `browser_specific_settings` is the recommended key to use in the
1274     // manifest, and the only possible choice in MV3+. For MV2 extensions, we
1275     // still allow `applications`, though. Because `applications` used to be
1276     // the only key in the distant past, most internal code is written using
1277     // applications. That's why we end up re-assigning `browser_specific_settings`
1278     // to `applications` below.
1279     //
1280     // Also, when a MV3+ extension specifies `applications`, the key isn't
1281     // recognized and therefore filtered out from the normalized manifest as
1282     // part of the JSONSchema normalization.
1283     if (manifest.browser_specific_settings?.gecko) {
1284       if (manifest.applications) {
1285         this.manifestWarning(
1286           `"applications" property ignored and overridden by "browser_specific_settings"`
1287         );
1288       }
1289       manifest.applications = manifest.browser_specific_settings;
1290     }
1292     if (
1293       this.manifestVersion < 3 &&
1294       manifest.background &&
1295       !this.eventPagesEnabled &&
1296       !manifest.background.persistent
1297     ) {
1298       this.logWarning("Event pages are not currently supported.");
1299     }
1301     if (
1302       this.isPrivileged &&
1303       manifest.hidden &&
1304       (manifest.action || manifest.browser_action || manifest.page_action)
1305     ) {
1306       this.manifestError(
1307         "Cannot use browser and/or page actions in hidden add-ons"
1308       );
1309     }
1311     let apiNames = new Set();
1312     let dependencies = new Set();
1313     let originPermissions = new Set();
1314     let permissions = new Set();
1315     let webAccessibleResources = [];
1317     let schemaPromises = new Map();
1319     // Note: this.id and this.type were computed in initializeAddonTypeAndID.
1320     // The format of `this.id` was confirmed to be a valid extensionID by the
1321     // Schema validation as part of the _getNormalizedManifest() call.
1322     let result = {
1323       apiNames,
1324       dependencies,
1325       id: this.id,
1326       manifest,
1327       modules: null,
1328       // Whether to treat all origin permissions (including content scripts)
1329       // from the manifestas as optional, and enable users to control them.
1330       originControls: this.manifestVersion >= 3,
1331       originPermissions,
1332       permissions,
1333       schemaURLs: null,
1334       type: this.type,
1335       webAccessibleResources,
1336     };
1338     if (this.type === "extension") {
1339       let { isPrivileged } = this;
1340       let restrictSchemes = !(
1341         isPrivileged && manifest.permissions.includes("mozillaAddons")
1342       );
1344       // Privileged and temporary extensions can opt out of originControls.
1345       if (
1346         (isPrivileged || this.temporarilyInstalled) &&
1347         manifest.granted_host_permissions
1348       ) {
1349         result.originControls = false;
1350       }
1352       let host_permissions = manifest.host_permissions ?? [];
1354       for (let perm of manifest.permissions.concat(host_permissions)) {
1355         if (perm === "geckoProfiler" && !isPrivileged) {
1356           const acceptedExtensions = Services.prefs.getStringPref(
1357             "extensions.geckoProfiler.acceptedExtensionIds",
1358             ""
1359           );
1360           if (!acceptedExtensions.split(",").includes(this.id)) {
1361             this.manifestError(
1362               "Only specific extensions are allowed to access the geckoProfiler."
1363             );
1364             continue;
1365           }
1366         }
1368         let type = classifyPermission(perm, restrictSchemes, isPrivileged);
1369         if (type.origin) {
1370           perm = type.origin;
1371           if (!result.originControls) {
1372             originPermissions.add(perm);
1373           }
1374         } else if (type.api) {
1375           apiNames.add(type.api);
1376         } else if (type.invalid) {
1377           // If EXPERIMENTS_ENABLED is not enabled prevent the install
1378           // to ensure developer awareness.
1379           if (this.temporarilyInstalled && type.privileged) {
1380             this.manifestError(
1381               `Using the privileged permission '${perm}' requires a privileged add-on. ` +
1382                 PRIVILEGED_ADDONS_DEVDOCS_MESSAGE
1383             );
1384             continue;
1385           }
1386           this.manifestWarning(`Invalid extension permission: ${perm}`);
1387           continue;
1388         }
1390         // Unfortunately, we treat <all_urls> as an API permission as well.
1391         if (!type.origin || (perm === "<all_urls>" && !result.originControls)) {
1392           permissions.add(perm);
1393         }
1394       }
1396       if (this.id) {
1397         // An extension always gets permission to its own url.
1398         let matcher = new MatchPattern(this.getURL(), { ignorePath: true });
1399         originPermissions.add(matcher.pattern);
1401         // Apply optional permissions
1402         let perms = await lazy.ExtensionPermissions.get(this.id);
1403         for (let perm of perms.permissions) {
1404           permissions.add(perm);
1405         }
1406         for (let origin of perms.origins) {
1407           originPermissions.add(origin);
1408         }
1409       }
1411       for (let api of apiNames) {
1412         dependencies.add(`${api}@experiments.addons.mozilla.org`);
1413       }
1415       let moduleData = data => ({
1416         url: this.rootURI.resolve(data.script),
1417         events: data.events,
1418         paths: data.paths,
1419         scopes: data.scopes,
1420       });
1422       let computeModuleInit = (scope, modules) => {
1423         let manager = new ExtensionCommon.SchemaAPIManager(scope);
1424         return manager.initModuleJSON([modules]);
1425       };
1427       result.contentScripts = [];
1428       for (let options of manifest.content_scripts || []) {
1429         result.contentScripts.push({
1430           allFrames: options.all_frames,
1431           matchAboutBlank: options.match_about_blank,
1432           frameID: options.frame_id,
1433           runAt: options.run_at,
1435           matches: options.matches,
1436           excludeMatches: options.exclude_matches || [],
1437           includeGlobs: options.include_globs,
1438           excludeGlobs: options.exclude_globs,
1440           jsPaths: options.js || [],
1441           cssPaths: options.css || [],
1442         });
1443       }
1445       if (manifest.experiment_apis) {
1446         if (this.canUseAPIExperiment()) {
1447           let parentModules = {};
1448           let childModules = {};
1450           for (let [name, data] of Object.entries(manifest.experiment_apis)) {
1451             let schema = this.getURL(data.schema);
1453             if (!schemaPromises.has(schema)) {
1454               schemaPromises.set(
1455                 schema,
1456                 this.readJSON(data.schema).then(json =>
1457                   lazy.Schemas.processSchema(json)
1458                 )
1459               );
1460             }
1462             if (data.parent) {
1463               parentModules[name] = moduleData(data.parent);
1464             }
1466             if (data.child) {
1467               childModules[name] = moduleData(data.child);
1468             }
1469           }
1471           result.modules = {
1472             child: computeModuleInit("addon_child", childModules),
1473             parent: computeModuleInit("addon_parent", parentModules),
1474           };
1475         } else if (this.temporarilyInstalled) {
1476           // Hard error for un-privileged temporary installs using experimental apis.
1477           this.manifestError(
1478             `Using 'experiment_apis' requires a privileged add-on. ` +
1479               PRIVILEGED_ADDONS_DEVDOCS_MESSAGE
1480           );
1481         } else {
1482           this.manifestWarning(
1483             `Using experimental APIs requires a privileged add-on.`
1484           );
1485         }
1486       }
1488       // Normalize all patterns to contain a single leading /
1489       if (manifest.web_accessible_resources) {
1490         // Normalize into V3 objects
1491         let wac =
1492           this.manifestVersion >= 3
1493             ? manifest.web_accessible_resources
1494             : [{ resources: manifest.web_accessible_resources }];
1495         webAccessibleResources.push(
1496           ...wac.map(obj => {
1497             obj.resources = obj.resources.map(path =>
1498               path.replace(/^\/*/, "/")
1499             );
1500             return obj;
1501           })
1502         );
1503       }
1504     } else if (this.type == "locale") {
1505       // Langpack startup is performance critical, so we want to compute as much
1506       // as possible here to make startup not trigger async DB reads.
1507       // We'll store the four items below in the startupData.
1509       // 1. Compute the chrome resources to be registered for this langpack.
1510       const platform = AppConstants.platform;
1511       const chromeEntries = [];
1512       for (const [language, entry] of Object.entries(manifest.languages)) {
1513         for (const [alias, path] of Object.entries(
1514           entry.chrome_resources || {}
1515         )) {
1516           if (typeof path === "string") {
1517             chromeEntries.push(["locale", alias, language, path]);
1518           } else if (platform in path) {
1519             // If the path is not a string, it's an object with path per
1520             // platform where the keys are taken from AppConstants.platform
1521             chromeEntries.push(["locale", alias, language, path[platform]]);
1522           }
1523         }
1524       }
1526       // 2. Compute langpack ID.
1527       const productCodeName = AppConstants.MOZ_BUILD_APP.replace("/", "-");
1529       // The result path looks like this:
1530       //   Firefox - `langpack-pl-browser`
1531       //   Fennec - `langpack-pl-mobile-android`
1532       const langpackId = `langpack-${manifest.langpack_id}-${productCodeName}`;
1534       // 3. Compute L10nRegistry sources for this langpack.
1535       const l10nRegistrySources = {};
1537       // Check if there's a root directory `/localization` in the langpack.
1538       // If there is one, add it with the name `toolkit` as a FileSource.
1539       const entries = await this._readDirectory("localization");
1540       if (entries.length) {
1541         l10nRegistrySources.toolkit = "";
1542       }
1544       // Add any additional sources listed in the manifest
1545       if (manifest.sources) {
1546         for (const [sourceName, { base_path }] of Object.entries(
1547           manifest.sources
1548         )) {
1549           l10nRegistrySources[sourceName] = base_path;
1550         }
1551       }
1553       // 4. Save the list of languages handled by this langpack.
1554       const languages = Object.keys(manifest.languages);
1556       this.startupData = {
1557         chromeEntries,
1558         langpackId,
1559         l10nRegistrySources,
1560         languages,
1561       };
1562     } else if (this.type == "dictionary") {
1563       let dictionaries = {};
1564       for (let [lang, path] of Object.entries(manifest.dictionaries)) {
1565         path = path.replace(/^\/+/, "");
1567         let dir = lazy.dirname(path);
1568         if (dir === ".") {
1569           dir = "";
1570         }
1571         let leafName = lazy.basename(path);
1572         let affixPath = leafName.slice(0, -3) + "aff";
1574         let entries = await this._readDirectory(dir);
1575         if (!entries.includes(leafName)) {
1576           this.manifestError(
1577             `Invalid dictionary path specified for '${lang}': ${path}`
1578           );
1579         }
1580         if (!entries.includes(affixPath)) {
1581           this.manifestError(
1582             `Invalid dictionary path specified for '${lang}': Missing affix file: ${path}`
1583           );
1584         }
1586         dictionaries[lang] = path;
1587       }
1589       this.startupData = { dictionaries };
1590     }
1592     if (schemaPromises.size) {
1593       let schemas = new Map();
1594       for (let [url, promise] of schemaPromises) {
1595         schemas.set(url, await promise);
1596       }
1597       result.schemaURLs = schemas;
1598     }
1600     return result;
1601   }
1603   // Reads the extension's |manifest.json| file, and stores its
1604   // parsed contents in |this.manifest|.
1605   async loadManifest() {
1606     let [manifestData] = await Promise.all([
1607       this.parseManifest(),
1608       Management.lazyInit(),
1609     ]);
1611     if (!manifestData) {
1612       return;
1613     }
1615     // Do not override the add-on id that has been already assigned.
1616     if (!this.id) {
1617       this.id = manifestData.id;
1618     }
1620     this.manifest = manifestData.manifest;
1621     this.apiNames = manifestData.apiNames;
1622     this.contentScripts = manifestData.contentScripts;
1623     this.dependencies = manifestData.dependencies;
1624     this.permissions = manifestData.permissions;
1625     this.schemaURLs = manifestData.schemaURLs;
1626     this.type = manifestData.type;
1628     this.modules = manifestData.modules;
1630     this.apiManager = this.getAPIManager();
1631     await this.apiManager.lazyInit();
1633     this.webAccessibleResources = manifestData.webAccessibleResources;
1635     this.originControls = manifestData.originControls;
1636     this.allowedOrigins = new MatchPatternSet(manifestData.originPermissions, {
1637       restrictSchemes: this.restrictSchemes,
1638     });
1640     return this.manifest;
1641   }
1643   hasPermission(perm, includeOptional = false) {
1644     // If the permission is a "manifest property" permission, we check if the extension
1645     // does have the required property in its manifest.
1646     let manifest_ = "manifest:";
1647     if (perm.startsWith(manifest_)) {
1648       // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
1649       let value = this.manifest;
1650       for (let prop of perm.substr(manifest_.length).split(".")) {
1651         if (!value) {
1652           break;
1653         }
1654         value = value[prop];
1655       }
1657       return value != null;
1658     }
1660     if (this.permissions.has(perm)) {
1661       return true;
1662     }
1664     if (includeOptional && this.manifest.optional_permissions.includes(perm)) {
1665       return true;
1666     }
1668     return false;
1669   }
1671   getAPIManager() {
1672     let apiManagers = [Management];
1674     for (let id of this.dependencies) {
1675       let policy = WebExtensionPolicy.getByID(id);
1676       if (policy) {
1677         if (policy.extension.experimentAPIManager) {
1678           apiManagers.push(policy.extension.experimentAPIManager);
1679         } else if (AppConstants.DEBUG) {
1680           Cu.reportError(`Cannot find experimental API exported from ${id}`);
1681         }
1682       }
1683     }
1685     if (this.modules) {
1686       this.experimentAPIManager = new ExtensionCommon.LazyAPIManager(
1687         "main",
1688         this.modules.parent,
1689         this.schemaURLs
1690       );
1692       apiManagers.push(this.experimentAPIManager);
1693     }
1695     if (apiManagers.length == 1) {
1696       return apiManagers[0];
1697     }
1699     return new ExtensionCommon.MultiAPIManager("main", apiManagers.reverse());
1700   }
1702   localizeMessage(...args) {
1703     return this.localeData.localizeMessage(...args);
1704   }
1706   localize(str, locale) {
1707     // If the extension declares fluent resources in the manifest, try
1708     // first to localize with fluent.  Also use the original webextension
1709     // method (_locales/xx.json) so extensions can migrate bit by bit.
1710     // Note also that fluent keys typically use hyphense, so hyphens are
1711     // allowed in the __MSG_foo__ keys used by fluent, though they are
1712     // not allowed in the keys used for json translations.
1713     if (this.fluentL10n) {
1714       str = str.replace(/__MSG_([-A-Za-z0-9@_]+?)__/g, (matched, message) => {
1715         let translation = this.fluentL10n.formatValueSync(message);
1716         return translation !== undefined ? translation : matched;
1717       });
1718     }
1719     if (this.localeData) {
1720       str = this.localeData.localize(str, locale);
1721     }
1722     return str;
1723   }
1725   // If a "default_locale" is specified in that manifest, returns it
1726   // as a Gecko-compatible locale string. Otherwise, returns null.
1727   get defaultLocale() {
1728     if (this.manifest.default_locale != null) {
1729       return this.normalizeLocaleCode(this.manifest.default_locale);
1730     }
1732     return null;
1733   }
1735   // Returns true if an addon is builtin to Firefox or
1736   // distributed via Normandy into a system location.
1737   get isAppProvided() {
1738     return this.addonData.builtIn || this.addonData.isSystem;
1739   }
1741   get isHidden() {
1742     return (
1743       this.addonData.locationHidden ||
1744       (this.isPrivileged && this.manifest.hidden)
1745     );
1746   }
1748   // Normalizes a Chrome-compatible locale code to the appropriate
1749   // Gecko-compatible variant. Currently, this means simply
1750   // replacing underscores with hyphens.
1751   normalizeLocaleCode(locale) {
1752     return locale.replace(/_/g, "-");
1753   }
1755   // Reads the locale file for the given Gecko-compatible locale code, and
1756   // stores its parsed contents in |this.localeMessages.get(locale)|.
1757   async readLocaleFile(locale) {
1758     let locales = await this.promiseLocales();
1759     let dir = locales.get(locale) || locale;
1760     let file = `_locales/${dir}/messages.json`;
1762     try {
1763       let messages = await this.readJSON(file);
1764       return this.localeData.addLocale(locale, messages, this);
1765     } catch (e) {
1766       this.packagingError(`Loading locale file ${file}: ${e}`);
1767       return new Map();
1768     }
1769   }
1771   async _promiseLocaleMap() {
1772     let locales = new Map();
1774     let entries = await this._readDirectory("_locales", true);
1775     for (let name of entries) {
1776       let locale = this.normalizeLocaleCode(name);
1777       locales.set(locale, name);
1778     }
1780     return locales;
1781   }
1783   _setupLocaleData(locales) {
1784     if (this.localeData) {
1785       return this.localeData.locales;
1786     }
1788     this.localeData = new lazy.LocaleData({
1789       defaultLocale: this.defaultLocale,
1790       locales,
1791       builtinMessages: this.builtinMessages,
1792     });
1794     return locales;
1795   }
1797   // Reads the list of locales available in the extension, and returns a
1798   // Promise which resolves to a Map upon completion.
1799   // Each map key is a Gecko-compatible locale code, and each value is the
1800   // "_locales" subdirectory containing that locale:
1801   //
1802   // Map(gecko-locale-code -> locale-directory-name)
1803   promiseLocales() {
1804     if (!this._promiseLocales) {
1805       this._promiseLocales = (async () => {
1806         let locales = this._promiseLocaleMap();
1807         return this._setupLocaleData(locales);
1808       })();
1809     }
1811     return this._promiseLocales;
1812   }
1814   // Reads the locale messages for all locales, and returns a promise which
1815   // resolves to a Map of locale messages upon completion. Each key in the map
1816   // is a Gecko-compatible locale code, and each value is a locale data object
1817   // as returned by |readLocaleFile|.
1818   async initAllLocales() {
1819     let locales = await this.promiseLocales();
1821     await Promise.all(
1822       Array.from(locales.keys(), locale => this.readLocaleFile(locale))
1823     );
1825     let defaultLocale = this.defaultLocale;
1826     if (defaultLocale) {
1827       if (!locales.has(defaultLocale)) {
1828         this.manifestError(
1829           'Value for "default_locale" property must correspond to ' +
1830             'a directory in "_locales/". Not found: ' +
1831             JSON.stringify(`_locales/${this.manifest.default_locale}/`)
1832         );
1833       }
1834     } else if (locales.size) {
1835       this.manifestError(
1836         'The "default_locale" property is required when a ' +
1837           '"_locales/" directory is present.'
1838       );
1839     }
1841     return this.localeData.messages;
1842   }
1844   // Reads the locale file for the given Gecko-compatible locale code, or the
1845   // default locale if no locale code is given, and sets it as the currently
1846   // selected locale on success.
1847   //
1848   // Pre-loads the default locale for fallback message processing, regardless
1849   // of the locale specified.
1850   //
1851   // If no locales are unavailable, resolves to |null|.
1852   async initLocale(locale = this.defaultLocale) {
1853     if (locale == null) {
1854       return null;
1855     }
1857     let promises = [this.readLocaleFile(locale)];
1859     let { defaultLocale } = this;
1860     if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
1861       promises.push(this.readLocaleFile(defaultLocale));
1862     }
1864     let results = await Promise.all(promises);
1866     this.localeData.selectedLocale = locale;
1867     return results[0];
1868   }
1870   /**
1871    * @param {string} origin
1872    * @returns {boolean}       If this is one of the "all sites" permission.
1873    */
1874   static isAllSitesPermission(origin) {
1875     try {
1876       let info = ExtensionData.classifyOriginPermissions([origin], true);
1877       return !!info.allUrls;
1878     } catch (e) {
1879       // Passed string is not an origin permission.
1880       return false;
1881     }
1882   }
1884   /**
1885    * @typedef {object} HostPermissions
1886    * @param {string} allUrls   permission used to obtain all urls access
1887    * @param {Set} wildcards    set contains permissions with wildcards
1888    * @param {Set} sites        set contains explicit host permissions
1889    * @param {Map} wildcardsMap mapping origin wildcards to labels
1890    * @param {Map} sitesMap     mapping origin patterns to labels
1891    */
1893   /**
1894    * Classify host permissions
1895    *
1896    * @param {Array<string>} origins
1897    *                        permission origins
1898    * @param {boolean}       ignoreNonWebSchemes
1899    *                        return only these schemes: *, http, https, ws, wss
1900    *
1901    * @returns {HostPermissions}
1902    */
1903   static classifyOriginPermissions(origins = [], ignoreNonWebSchemes = false) {
1904     let allUrls = null,
1905       wildcards = new Set(),
1906       sites = new Set(),
1907       // TODO: use map.values() instead of these sets.  Note: account for two
1908       // match patterns producing the same permission string, see bug 1765828.
1909       wildcardsMap = new Map(),
1910       sitesMap = new Map();
1912     // https://searchfox.org/mozilla-central/rev/6f6cf28107/toolkit/components/extensions/MatchPattern.cpp#235
1913     const wildcardSchemes = ["*", "http", "https", "ws", "wss"];
1915     for (let permission of origins) {
1916       if (permission == "<all_urls>") {
1917         allUrls = permission;
1918         continue;
1919       }
1921       // Privileged extensions may request access to "about:"-URLs, such as
1922       // about:reader.
1923       let match = /^([a-z*]+):\/\/([^/]*)\/|^about:/.exec(permission);
1924       if (!match) {
1925         throw new Error(`Unparseable host permission ${permission}`);
1926       }
1928       // Note: the scheme is ignored in the permission warnings. If this ever
1929       // changes, update the comparePermissions method as needed.
1930       let [, scheme, host] = match;
1931       if (ignoreNonWebSchemes && !wildcardSchemes.includes(scheme)) {
1932         continue;
1933       }
1935       if (!host || host == "*") {
1936         if (!allUrls) {
1937           allUrls = permission;
1938         }
1939       } else if (host.startsWith("*.")) {
1940         wildcards.add(host.slice(2));
1941         // Using MatchPattern to normalize the pattern string.
1942         let pat = new MatchPattern(permission, { ignorePath: true });
1943         wildcardsMap.set(pat.pattern, `${scheme}://${host.slice(2)}`);
1944       } else {
1945         sites.add(host);
1946         let pat = new MatchPattern(permission, {
1947           ignorePath: true,
1948           // Safe because used just for normalization, not for granting access.
1949           restrictSchemes: false,
1950         });
1951         sitesMap.set(pat.pattern, `${scheme}://${host}`);
1952       }
1953     }
1954     return { allUrls, wildcards, sites, wildcardsMap, sitesMap };
1955   }
1957   /**
1958    * Formats all the strings for a permissions dialog/notification.
1959    *
1960    * @param {object} info Information about the permissions being requested.
1961    *
1962    * @param {Array<string>} info.permissions.origins
1963    *                        Origin permissions requested.
1964    * @param {Array<string>} info.permissions.permissions
1965    *                        Regular (non-origin) permissions requested.
1966    * @param {Array<string>} info.optionalPermissions.origins
1967    *                        Optional origin permissions listed in the manifest.
1968    * @param {Array<string>} info.optionalPermissions.permissions
1969    *                        Optional (non-origin) permissions listed in the manifest.
1970    * @param {boolean} info.unsigned
1971    *                  True if the prompt is for installing an unsigned addon.
1972    * @param {string} info.type
1973    *                 The type of prompt being shown.  May be one of "update",
1974    *                 "sideload", "optional", or omitted for a regular
1975    *                 install prompt.
1976    * @param {string} info.appName
1977    *                 The localized name of the application, to be substituted
1978    *                 in computed strings as needed.
1979    * @param {nsIStringBundle} bundle
1980    *                          The string bundle to use for l10n.
1981    * @param {object} options
1982    * @param {boolean} options.collapseOrigins
1983    *                  Wether to limit the number of displayed host permissions.
1984    *                  Default is false.
1985    * @param {boolean} options.buildOptionalOrigins
1986    *                  Wether to build optional origins Maps for permission
1987    *                  controls.  Defaults to false.
1988    * @param {Function} options.getKeyForPermission
1989    *                   An optional callback function that returns the locale key for a given
1990    *                   permission name (set by default to a callback returning the locale
1991    *                   key following the default convention `webextPerms.description.PERMNAME`).
1992    *                   Overriding the default mapping can become necessary, when a permission
1993    *                   description needs to be modified and a non-default locale key has to be
1994    *                   used. There is at least one non-default locale key used in Thunderbird.
1995    *
1996    * @returns {object} An object with properties containing localized strings
1997    *                   for various elements of a permission dialog. The "header"
1998    *                   property on this object is the notification header text
1999    *                   and it has the string "<>" as a placeholder for the
2000    *                   addon name.
2001    *
2002    *                   "object.msgs" is an array of localized strings describing required permissions
2003    *
2004    *                   "object.optionalPermissions" is a map of permission name to localized
2005    *                   strings describing the permission.
2006    *
2007    *                   "object.optionalOrigins" is a map of a host permission to localized strings
2008    *                   describing the host permission, where appropriate.  Currently only
2009    *                   all url style permissions are included.
2010    */
2011   static formatPermissionStrings(
2012     info,
2013     bundle,
2014     {
2015       collapseOrigins = false,
2016       buildOptionalOrigins = false,
2017       getKeyForPermission = perm => `webextPerms.description.${perm}`,
2018     } = {}
2019   ) {
2020     let result = {
2021       msgs: [],
2022       optionalPermissions: {},
2023       optionalOrigins: {},
2024     };
2026     const haveAccessKeys = AppConstants.platform !== "android";
2028     let headerKey;
2029     result.text = "";
2030     result.listIntro = "";
2031     result.acceptText = bundle.GetStringFromName("webextPerms.add.label");
2032     result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label");
2033     if (haveAccessKeys) {
2034       result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey");
2035       result.cancelKey = bundle.GetStringFromName(
2036         "webextPerms.cancel.accessKey"
2037       );
2038     }
2040     // Synthetic addon install can only grant access to a single permission so we can have
2041     // a less-generic message than addons with site permissions.
2042     // NOTE: this is used as part of the synthetic addon install flow implemented for the
2043     // SitePermissionAddonProvider.
2044     // (and so it should not be removed as part of Bug 1789718 changes, while this additional note should be).
2045     if (info.addon?.type === lazy.SITEPERMS_ADDON_TYPE) {
2046       // We simplify the origin to make it more user friendly. The origin is assured to be
2047       // available because the SitePermsAddon install is always expected to be triggered
2048       // from a website, making the siteOrigin always available through the installing principal.
2049       const host = new URL(info.siteOrigin).hostname;
2051       // messages are specific to the type of gated permission being installed
2052       result.header = bundle.formatStringFromName(
2053         `webextSitePerms.headerWithGatedPerms.${info.sitePermissions[0]}`,
2054         [host]
2055       );
2056       result.text = bundle.GetStringFromName(
2057         `webextSitePerms.descriptionGatedPerms`
2058       );
2060       return result;
2061     }
2063     // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
2064     if (info.sitePermissions) {
2065       // Generate a map of site_permission names to permission strings for site permissions.
2066       for (let permission of info.sitePermissions) {
2067         try {
2068           result.msgs.push(
2069             bundle.GetStringFromName(
2070               `webextSitePerms.description.${permission}`
2071             )
2072           );
2073         } catch (err) {
2074           Cu.reportError(
2075             `site_permission ${permission} missing readable text property`
2076           );
2077           // We must never have a DOM api permission that is hidden so in
2078           // the case of any error, we'll use the plain permission string.
2079           // test_ext_sitepermissions.js tests for no missing messages, this
2080           // is just an extra fallback.
2081           result.msgs.push(permission);
2082         }
2083       }
2085       // We simplify the origin to make it more user friendly.  The origin is
2086       // assured to be available via schema requirement.
2087       const host = new URL(info.siteOrigin).hostname;
2089       headerKey = info.unsigned
2090         ? "webextSitePerms.headerUnsignedWithPerms"
2091         : "webextSitePerms.headerWithPerms";
2092       result.header = bundle.formatStringFromName(headerKey, ["<>", host]);
2094       return result;
2095     }
2097     let perms = info.permissions || { origins: [], permissions: [] };
2098     let optional_permissions = info.optionalPermissions || {
2099       origins: [],
2100       permissions: [],
2101     };
2103     // First classify our host permissions
2104     let { allUrls, wildcards, sites } = ExtensionData.classifyOriginPermissions(
2105       perms.origins
2106     );
2108     // Format the host permissions.  If we have a wildcard for all urls,
2109     // a single string will suffice.  Otherwise, show domain wildcards
2110     // first, then individual host permissions.
2111     if (allUrls) {
2112       result.msgs.push(
2113         bundle.GetStringFromName("webextPerms.hostDescription.allUrls")
2114       );
2115     } else {
2116       // Formats a list of host permissions.  If we have 4 or fewer, display
2117       // them all, otherwise display the first 3 followed by an item that
2118       // says "...plus N others"
2119       let format = (list, itemKey, moreKey) => {
2120         function formatItems(items) {
2121           result.msgs.push(
2122             ...items.map(item => bundle.formatStringFromName(itemKey, [item]))
2123           );
2124         }
2125         if (list.length < 5 || !collapseOrigins) {
2126           formatItems(list);
2127         } else {
2128           formatItems(list.slice(0, 3));
2130           let remaining = list.length - 3;
2131           result.msgs.push(
2132             lazy.PluralForm.get(
2133               remaining,
2134               bundle.GetStringFromName(moreKey)
2135             ).replace("#1", remaining)
2136           );
2137         }
2138       };
2140       format(
2141         Array.from(wildcards),
2142         "webextPerms.hostDescription.wildcard",
2143         "webextPerms.hostDescription.tooManyWildcards"
2144       );
2145       format(
2146         Array.from(sites),
2147         "webextPerms.hostDescription.oneSite",
2148         "webextPerms.hostDescription.tooManySites"
2149       );
2150     }
2152     // Next, show the native messaging permission if it is present.
2153     const NATIVE_MSG_PERM = "nativeMessaging";
2154     if (perms.permissions.includes(NATIVE_MSG_PERM)) {
2155       result.msgs.push(
2156         bundle.formatStringFromName(getKeyForPermission(NATIVE_MSG_PERM), [
2157           info.appName,
2158         ])
2159       );
2160     }
2162     // Finally, show remaining permissions, in the same order as AMO.
2163     // The permissions are sorted alphabetically by the permission
2164     // string to match AMO.
2165     let permissionsCopy = perms.permissions.slice(0);
2166     for (let permission of permissionsCopy.sort()) {
2167       // Handled above
2168       if (permission == NATIVE_MSG_PERM) {
2169         continue;
2170       }
2171       try {
2172         result.msgs.push(
2173           bundle.GetStringFromName(getKeyForPermission(permission))
2174         );
2175       } catch (err) {
2176         // We deliberately do not include all permissions in the prompt.
2177         // So if we don't find one then just skip it.
2178       }
2179     }
2181     // Generate a map of permission names to permission strings for optional
2182     // permissions.  The key is necessary to handle toggling those permissions.
2183     for (let permission of optional_permissions.permissions) {
2184       if (permission == NATIVE_MSG_PERM) {
2185         result.optionalPermissions[
2186           permission
2187         ] = bundle.formatStringFromName(getKeyForPermission(permission), [
2188           info.appName,
2189         ]);
2190         continue;
2191       }
2192       try {
2193         result.optionalPermissions[permission] = bundle.GetStringFromName(
2194           getKeyForPermission(permission)
2195         );
2196       } catch (err) {
2197         // We deliberately do not have strings for all permissions.
2198         // So if we don't find one then just skip it.
2199       }
2200     }
2202     let optionalInfo = ExtensionData.classifyOriginPermissions(
2203       optional_permissions.origins,
2204       true
2205     );
2206     if (optionalInfo.allUrls) {
2207       result.optionalOrigins[optionalInfo.allUrls] = bundle.GetStringFromName(
2208         "webextPerms.hostDescription.allUrls"
2209       );
2210     }
2212     // Current UX controls are meant for developer testing with mv3.
2213     if (buildOptionalOrigins) {
2214       for (let [pattern, originLabel] of optionalInfo.wildcardsMap.entries()) {
2215         let key = "webextPerms.hostDescription.wildcard";
2216         let str = bundle.formatStringFromName(key, [originLabel]);
2217         result.optionalOrigins[pattern] = str;
2218       }
2219       for (let [pattern, originLabel] of optionalInfo.sitesMap.entries()) {
2220         let key = "webextPerms.hostDescription.oneSite";
2221         let str = bundle.formatStringFromName(key, [originLabel]);
2222         result.optionalOrigins[pattern] = str;
2223       }
2224     }
2226     if (info.type == "sideload") {
2227       headerKey = "webextPerms.sideloadHeader";
2228       let key = !result.msgs.length
2229         ? "webextPerms.sideloadTextNoPerms"
2230         : "webextPerms.sideloadText2";
2231       result.text = bundle.GetStringFromName(key);
2232       result.acceptText = bundle.GetStringFromName(
2233         "webextPerms.sideloadEnable.label"
2234       );
2235       result.cancelText = bundle.GetStringFromName(
2236         "webextPerms.sideloadCancel.label"
2237       );
2238       if (haveAccessKeys) {
2239         result.acceptKey = bundle.GetStringFromName(
2240           "webextPerms.sideloadEnable.accessKey"
2241         );
2242         result.cancelKey = bundle.GetStringFromName(
2243           "webextPerms.sideloadCancel.accessKey"
2244         );
2245       }
2246     } else if (info.type == "update") {
2247       headerKey = "webextPerms.updateText2";
2248       result.text = "";
2249       result.acceptText = bundle.GetStringFromName(
2250         "webextPerms.updateAccept.label"
2251       );
2252       if (haveAccessKeys) {
2253         result.acceptKey = bundle.GetStringFromName(
2254           "webextPerms.updateAccept.accessKey"
2255         );
2256       }
2257     } else if (info.type == "optional") {
2258       headerKey = "webextPerms.optionalPermsHeader";
2259       result.text = "";
2260       result.listIntro = bundle.GetStringFromName(
2261         "webextPerms.optionalPermsListIntro"
2262       );
2263       result.acceptText = bundle.GetStringFromName(
2264         "webextPerms.optionalPermsAllow.label"
2265       );
2266       result.cancelText = bundle.GetStringFromName(
2267         "webextPerms.optionalPermsDeny.label"
2268       );
2269       if (haveAccessKeys) {
2270         result.acceptKey = bundle.GetStringFromName(
2271           "webextPerms.optionalPermsAllow.accessKey"
2272         );
2273         result.cancelKey = bundle.GetStringFromName(
2274           "webextPerms.optionalPermsDeny.accessKey"
2275         );
2276       }
2277     } else {
2278       headerKey = "webextPerms.header";
2279       if (result.msgs.length) {
2280         headerKey = info.unsigned
2281           ? "webextPerms.headerUnsignedWithPerms"
2282           : "webextPerms.headerWithPerms";
2283       } else if (info.unsigned) {
2284         headerKey = "webextPerms.headerUnsigned";
2285       }
2286     }
2287     result.header = bundle.formatStringFromName(headerKey, ["<>"]);
2288     return result;
2289   }
2292 const PROXIED_EVENTS = new Set([
2293   "test-harness-message",
2294   "background-script-suspend",
2295   "background-script-suspend-canceled",
2296   "background-script-suspend-ignored",
2299 class BootstrapScope {
2300   install(data, reason) {}
2301   uninstall(data, reason) {
2302     lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
2303       `Uninstalling add-on: ${data.id}`,
2304       Management.emit("uninstall", { id: data.id }).then(() => {
2305         Management.emit("uninstall-complete", { id: data.id });
2306       })
2307     );
2308   }
2310   fetchState() {
2311     if (this.extension) {
2312       return { state: this.extension.state };
2313     }
2314     return null;
2315   }
2317   async update(data, reason) {
2318     // For updates that happen during startup, such as sideloads
2319     // and staged updates, the extension startupReason will be
2320     // APP_STARTED.  In some situations, such as background and
2321     // persisted listeners, we also need to know that the addon
2322     // was updated.
2323     this.updateReason = this.BOOTSTRAP_REASON_TO_STRING_MAP[reason];
2324     // Retain any previously granted permissions that may have migrated
2325     // into the optional list.
2326     if (data.oldPermissions) {
2327       // New permissions may be null, ensure we have an empty
2328       // permission set in that case.
2329       let emptyPermissions = { permissions: [], origins: [] };
2330       await ExtensionData.migratePermissions(
2331         data.id,
2332         data.oldPermissions,
2333         data.oldOptionalPermissions,
2334         data.userPermissions || emptyPermissions,
2335         data.optionalPermissions || emptyPermissions
2336       );
2337     }
2339     return Management.emit("update", {
2340       id: data.id,
2341       resourceURI: data.resourceURI,
2342       isPrivileged: data.isPrivileged,
2343     });
2344   }
2346   startup(data, reason) {
2347     // eslint-disable-next-line no-use-before-define
2348     this.extension = new Extension(
2349       data,
2350       this.BOOTSTRAP_REASON_TO_STRING_MAP[reason],
2351       this.updateReason
2352     );
2353     return this.extension.startup();
2354   }
2356   async shutdown(data, reason) {
2357     let result = await this.extension.shutdown(
2358       this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]
2359     );
2360     this.extension = null;
2361     return result;
2362   }
2365 XPCOMUtils.defineLazyGetter(
2366   BootstrapScope.prototype,
2367   "BOOTSTRAP_REASON_TO_STRING_MAP",
2368   () => {
2369     const { BOOTSTRAP_REASONS } = lazy.AddonManagerPrivate;
2371     return Object.freeze({
2372       [BOOTSTRAP_REASONS.APP_STARTUP]: "APP_STARTUP",
2373       [BOOTSTRAP_REASONS.APP_SHUTDOWN]: "APP_SHUTDOWN",
2374       [BOOTSTRAP_REASONS.ADDON_ENABLE]: "ADDON_ENABLE",
2375       [BOOTSTRAP_REASONS.ADDON_DISABLE]: "ADDON_DISABLE",
2376       [BOOTSTRAP_REASONS.ADDON_INSTALL]: "ADDON_INSTALL",
2377       [BOOTSTRAP_REASONS.ADDON_UNINSTALL]: "ADDON_UNINSTALL",
2378       [BOOTSTRAP_REASONS.ADDON_UPGRADE]: "ADDON_UPGRADE",
2379       [BOOTSTRAP_REASONS.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE",
2380     });
2381   }
2384 class DictionaryBootstrapScope extends BootstrapScope {
2385   install(data, reason) {}
2386   uninstall(data, reason) {}
2388   startup(data, reason) {
2389     // eslint-disable-next-line no-use-before-define
2390     this.dictionary = new Dictionary(data);
2391     return this.dictionary.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2392   }
2394   shutdown(data, reason) {
2395     this.dictionary.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2396     this.dictionary = null;
2397   }
2400 class LangpackBootstrapScope extends BootstrapScope {
2401   install(data, reason) {}
2402   uninstall(data, reason) {}
2403   update(data, reason) {}
2405   startup(data, reason) {
2406     // eslint-disable-next-line no-use-before-define
2407     this.langpack = new Langpack(data);
2408     return this.langpack.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2409   }
2411   shutdown(data, reason) {
2412     this.langpack.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2413     this.langpack = null;
2414   }
2417 // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
2418 class SitePermissionBootstrapScope extends BootstrapScope {
2419   install(data, reason) {}
2420   uninstall(data, reason) {}
2422   startup(data, reason) {
2423     // eslint-disable-next-line no-use-before-define
2424     this.sitepermission = new SitePermission(data);
2425     return this.sitepermission.startup(
2426       this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]
2427     );
2428   }
2430   shutdown(data, reason) {
2431     this.sitepermission.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2432     this.sitepermission = null;
2433   }
2436 let activeExtensionIDs = new Set();
2438 let pendingExtensions = new Map();
2441  * This class is the main representation of an active WebExtension
2442  * in the main process.
2444  * @augments ExtensionData
2445  */
2446 class Extension extends ExtensionData {
2447   constructor(addonData, startupReason, updateReason) {
2448     super(addonData.resourceURI, addonData.isPrivileged);
2450     this.startupStates = new Set();
2451     this.state = "Not started";
2452     this.userContextIsolation = lazy.userContextIsolation;
2454     this.sharedDataKeys = new Set();
2456     this.uuid = UUIDMap.get(addonData.id);
2457     this.instanceId = getUniqueId();
2459     this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
2460     Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
2462     if (addonData.cleanupFile) {
2463       Services.obs.addObserver(this, "xpcom-shutdown");
2464       this.cleanupFile = addonData.cleanupFile || null;
2465       delete addonData.cleanupFile;
2466     }
2468     if (addonData.TEST_NO_ADDON_MANAGER) {
2469       this.dontSaveStartupData = true;
2470     }
2471     if (addonData.TEST_NO_DELAYED_STARTUP) {
2472       this.testNoDelayedStartup = true;
2473     }
2475     this.addonData = addonData;
2476     this.startupData = addonData.startupData || {};
2477     this.startupReason = startupReason;
2478     this.updateReason = updateReason;
2480     if (
2481       updateReason ||
2482       ["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)
2483     ) {
2484       this.startupClearCachePromise = StartupCache.clearAddonData(addonData.id);
2485     }
2487     this.remote = !WebExtensionPolicy.isExtensionProcess;
2488     this.remoteType = this.remote ? lazy.E10SUtils.EXTENSION_REMOTE_TYPE : null;
2490     if (this.remote && lazy.processCount !== 1) {
2491       throw new Error(
2492         "Out-of-process WebExtensions are not supported with multiple child processes"
2493       );
2494     }
2496     // This is filled in the first time an extension child is created.
2497     this.parentMessageManager = null;
2499     this.id = addonData.id;
2500     this.version = addonData.version;
2501     this.baseURL = this.getURL("");
2502     this.baseURI = Services.io.newURI(this.baseURL).QueryInterface(Ci.nsIURL);
2503     this.principal = this.createPrincipal();
2505     this.views = new Set();
2506     this._backgroundPageFrameLoader = null;
2508     this.onStartup = null;
2510     this.hasShutdown = false;
2511     this.onShutdown = new Set();
2513     this.uninstallURL = null;
2515     this.allowedOrigins = null;
2516     this._optionalOrigins = null;
2517     this.webAccessibleResources = null;
2519     this.registeredContentScripts = new Map();
2521     this.emitter = new EventEmitter();
2523     if (this.startupData.lwtData && this.startupReason == "APP_STARTUP") {
2524       lazy.LightweightThemeManager.fallbackThemeData = this.startupData.lwtData;
2525     }
2527     /* eslint-disable mozilla/balanced-listeners */
2528     this.on("add-permissions", (ignoreEvent, permissions) => {
2529       for (let perm of permissions.permissions) {
2530         this.permissions.add(perm);
2531       }
2532       this.policy.permissions = Array.from(this.permissions);
2534       updateAllowedOrigins(this.policy, permissions.origins, /* isAdd */ true);
2535       this.allowedOrigins = this.policy.allowedOrigins;
2537       if (this.policy.active) {
2538         this.setSharedData("", this.serialize());
2539         Services.ppmm.sharedData.flush();
2540         this.broadcast("Extension:UpdatePermissions", {
2541           id: this.id,
2542           origins: permissions.origins,
2543           permissions: permissions.permissions,
2544           add: true,
2545         });
2546       }
2548       this.cachePermissions();
2549       this.updatePermissions();
2550     });
2552     this.on("remove-permissions", (ignoreEvent, permissions) => {
2553       for (let perm of permissions.permissions) {
2554         this.permissions.delete(perm);
2555       }
2556       this.policy.permissions = Array.from(this.permissions);
2558       updateAllowedOrigins(this.policy, permissions.origins, /* isAdd */ false);
2559       this.allowedOrigins = this.policy.allowedOrigins;
2561       if (this.policy.active) {
2562         this.setSharedData("", this.serialize());
2563         Services.ppmm.sharedData.flush();
2564         this.broadcast("Extension:UpdatePermissions", {
2565           id: this.id,
2566           origins: permissions.origins,
2567           permissions: permissions.permissions,
2568           add: false,
2569         });
2570       }
2572       this.cachePermissions();
2573       this.updatePermissions();
2574     });
2575     /* eslint-enable mozilla/balanced-listeners */
2576   }
2578   set state(startupState) {
2579     this.startupStates.clear();
2580     this.startupStates.add(startupState);
2581   }
2583   get state() {
2584     return `${Array.from(this.startupStates).join(", ")}`;
2585   }
2587   async addStartupStatePromise(name, fn) {
2588     this.startupStates.add(name);
2589     try {
2590       await fn();
2591     } finally {
2592       this.startupStates.delete(name);
2593     }
2594   }
2596   // Some helpful properties added elsewhere:
2598   static getBootstrapScope() {
2599     return new BootstrapScope();
2600   }
2602   get browsingContextGroupId() {
2603     return this.policy.browsingContextGroupId;
2604   }
2606   get groupFrameLoader() {
2607     let frameLoader = this._backgroundPageFrameLoader;
2608     for (let view of this.views) {
2609       if (view.viewType === "background" && view.xulBrowser) {
2610         return view.xulBrowser.frameLoader;
2611       }
2612       if (!frameLoader && view.xulBrowser) {
2613         frameLoader = view.xulBrowser.frameLoader;
2614       }
2615     }
2616     return frameLoader || ExtensionParent.DebugUtils.getFrameLoader(this.id);
2617   }
2619   get backgroundContext() {
2620     for (let view of this.views) {
2621       if (
2622         view.viewType === "background" ||
2623         view.viewType === "background_worker"
2624       ) {
2625         return view;
2626       }
2627     }
2628     return undefined;
2629   }
2631   on(hook, f) {
2632     return this.emitter.on(hook, f);
2633   }
2635   off(hook, f) {
2636     return this.emitter.off(hook, f);
2637   }
2639   once(hook, f) {
2640     return this.emitter.once(hook, f);
2641   }
2643   emit(event, ...args) {
2644     if (PROXIED_EVENTS.has(event)) {
2645       Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {
2646         event,
2647         args,
2648       });
2649     }
2651     return this.emitter.emit(event, ...args);
2652   }
2654   receiveMessage({ name, data }) {
2655     if (name === this.MESSAGE_EMIT_EVENT) {
2656       this.emitter.emit(data.event, ...data.args);
2657     }
2658   }
2660   testMessage(...args) {
2661     this.emit("test-harness-message", ...args);
2662   }
2664   createPrincipal(uri = this.baseURI, originAttributes = {}) {
2665     return Services.scriptSecurityManager.createContentPrincipal(
2666       uri,
2667       originAttributes
2668     );
2669   }
2671   // Checks that the given URL is a child of our baseURI.
2672   isExtensionURL(url) {
2673     let uri = Services.io.newURI(url);
2675     let common = this.baseURI.getCommonBaseSpec(uri);
2676     return common == this.baseURL;
2677   }
2679   checkLoadURI(uri, options = {}) {
2680     return ExtensionCommon.checkLoadURI(uri, this.principal, options);
2681   }
2683   // Note: use checkLoadURI instead of checkLoadURL if you already have a URI.
2684   checkLoadURL(url, options = {}) {
2685     // As an optimization, if the URL starts with the extension's base URL,
2686     // don't do any further checks. It's always allowed to load it.
2687     if (url.startsWith(this.baseURL)) {
2688       return true;
2689     }
2691     return ExtensionCommon.checkLoadURL(url, this.principal, options);
2692   }
2694   async promiseLocales(locale) {
2695     let locales = await StartupCache.locales.get(
2696       [this.id, "@@all_locales"],
2697       () => this._promiseLocaleMap()
2698     );
2700     return this._setupLocaleData(locales);
2701   }
2703   readLocaleFile(locale) {
2704     return StartupCache.locales
2705       .get([this.id, this.version, locale], () => super.readLocaleFile(locale))
2706       .then(result => {
2707         this.localeData.messages.set(locale, result);
2708       });
2709   }
2711   get manifestCacheKey() {
2712     return [this.id, this.version, Services.locale.appLocaleAsBCP47];
2713   }
2715   get temporarilyInstalled() {
2716     return !!this.addonData.temporarilyInstalled;
2717   }
2719   saveStartupData() {
2720     if (this.dontSaveStartupData) {
2721       return;
2722     }
2723     lazy.AddonManagerPrivate.setAddonStartupData(this.id, this.startupData);
2724   }
2726   async parseManifest() {
2727     await this.startupClearCachePromise;
2728     return StartupCache.manifests.get(this.manifestCacheKey, () =>
2729       super.parseManifest()
2730     );
2731   }
2733   async cachePermissions() {
2734     let manifestData = await this.parseManifest();
2736     manifestData.originPermissions = this.allowedOrigins.patterns.map(
2737       pat => pat.pattern
2738     );
2739     manifestData.permissions = this.permissions;
2740     return StartupCache.manifests.set(this.manifestCacheKey, manifestData);
2741   }
2743   async loadManifest() {
2744     let manifest = await super.loadManifest();
2746     this.ensureNoErrors();
2748     return manifest;
2749   }
2751   get extensionPageCSP() {
2752     const { content_security_policy } = this.manifest;
2753     // While only manifest v3 should contain an object,
2754     // we'll remain lenient here.
2755     if (
2756       content_security_policy &&
2757       typeof content_security_policy === "object"
2758     ) {
2759       return content_security_policy.extension_pages;
2760     }
2761     return content_security_policy;
2762   }
2764   get backgroundScripts() {
2765     return this.manifest.background?.scripts;
2766   }
2768   get backgroundTypeModule() {
2769     return this.manifest.background?.type === "module";
2770   }
2772   get backgroundWorkerScript() {
2773     return this.manifest.background?.service_worker;
2774   }
2776   get optionalPermissions() {
2777     return this.manifest.optional_permissions;
2778   }
2780   get privateBrowsingAllowed() {
2781     return this.policy.privateBrowsingAllowed;
2782   }
2784   canAccessWindow(window) {
2785     return this.policy.canAccessWindow(window);
2786   }
2788   // TODO bug 1699481: move this logic to WebExtensionPolicy
2789   canAccessContainer(userContextId) {
2790     userContextId = userContextId ?? 0; // firefox-default has userContextId as 0.
2791     let defaultRestrictedContainers = JSON.parse(
2792       lazy.userContextIsolationDefaultRestricted
2793     );
2794     let extensionRestrictedContainers = JSON.parse(
2795       Services.prefs.getStringPref(
2796         `extensions.userContextIsolation.${this.id}.restricted`,
2797         "[]"
2798       )
2799     );
2800     if (
2801       extensionRestrictedContainers.includes(userContextId) ||
2802       defaultRestrictedContainers.includes(userContextId)
2803     ) {
2804       return false;
2805     }
2807     return true;
2808   }
2810   // Representation of the extension to send to content
2811   // processes. This should include anything the content process might
2812   // need.
2813   serialize() {
2814     return {
2815       id: this.id,
2816       uuid: this.uuid,
2817       name: this.name,
2818       type: this.type,
2819       manifestVersion: this.manifestVersion,
2820       extensionPageCSP: this.extensionPageCSP,
2821       instanceId: this.instanceId,
2822       resourceURL: this.resourceURL,
2823       contentScripts: this.contentScripts,
2824       webAccessibleResources: this.webAccessibleResources,
2825       allowedOrigins: this.allowedOrigins.patterns.map(pat => pat.pattern),
2826       permissions: this.permissions,
2827       optionalPermissions: this.optionalPermissions,
2828       isPrivileged: this.isPrivileged,
2829       temporarilyInstalled: this.temporarilyInstalled,
2830     };
2831   }
2833   // Extended serialized data which is only needed in the extensions process,
2834   // and is never deserialized in web content processes.
2835   // Keep in sync with BrowserExtensionContent in ExtensionChild.jsm
2836   serializeExtended() {
2837     return {
2838       backgroundScripts: this.backgroundScripts,
2839       backgroundWorkerScript: this.backgroundWorkerScript,
2840       backgroundTypeModule: this.backgroundTypeModule,
2841       childModules: this.modules && this.modules.child,
2842       dependencies: this.dependencies,
2843       persistentBackground: this.persistentBackground,
2844       schemaURLs: this.schemaURLs,
2845     };
2846   }
2848   broadcast(msg, data) {
2849     return new Promise(resolve => {
2850       let { ppmm } = Services;
2851       let children = new Set();
2852       for (let i = 0; i < ppmm.childCount; i++) {
2853         children.add(ppmm.getChildAt(i));
2854       }
2856       let maybeResolve;
2857       function listener(data) {
2858         children.delete(data.target);
2859         maybeResolve();
2860       }
2861       function observer(subject, topic, data) {
2862         children.delete(subject);
2863         maybeResolve();
2864       }
2866       maybeResolve = () => {
2867         if (children.size === 0) {
2868           ppmm.removeMessageListener(msg + "Complete", listener);
2869           Services.obs.removeObserver(observer, "message-manager-close");
2870           Services.obs.removeObserver(observer, "message-manager-disconnect");
2871           resolve();
2872         }
2873       };
2874       ppmm.addMessageListener(msg + "Complete", listener, true);
2875       Services.obs.addObserver(observer, "message-manager-close");
2876       Services.obs.addObserver(observer, "message-manager-disconnect");
2878       ppmm.broadcastAsyncMessage(msg, data);
2879     });
2880   }
2882   setSharedData(key, value) {
2883     key = `extension/${this.id}/${key}`;
2884     this.sharedDataKeys.add(key);
2886     sharedData.set(key, value);
2887   }
2889   getSharedData(key, value) {
2890     key = `extension/${this.id}/${key}`;
2891     return sharedData.get(key);
2892   }
2894   initSharedData() {
2895     this.setSharedData("", this.serialize());
2896     this.setSharedData("extendedData", this.serializeExtended());
2897     this.setSharedData("locales", this.localeData.serialize());
2898     this.setSharedData("manifest", this.manifest);
2899     this.updateContentScripts();
2900   }
2902   updateContentScripts() {
2903     this.setSharedData("contentScripts", this.registeredContentScripts);
2904   }
2906   runManifest(manifest) {
2907     let promises = [];
2908     let addPromise = (name, fn) => {
2909       promises.push(this.addStartupStatePromise(name, fn));
2910     };
2912     for (let directive in manifest) {
2913       if (manifest[directive] !== null) {
2914         addPromise(`asyncEmitManifestEntry("${directive}")`, () =>
2915           Management.asyncEmitManifestEntry(this, directive)
2916         );
2917       }
2918     }
2920     activeExtensionIDs.add(this.id);
2921     sharedData.set("extensions/activeIDs", activeExtensionIDs);
2923     pendingExtensions.delete(this.id);
2924     sharedData.set("extensions/pending", pendingExtensions);
2926     Services.ppmm.sharedData.flush();
2927     this.broadcast("Extension:Startup", this.id);
2929     return Promise.all(promises);
2930   }
2932   /**
2933    * Call the close() method on the given object when this extension
2934    * is shut down.  This can happen during browser shutdown, or when
2935    * an extension is manually disabled or uninstalled.
2936    *
2937    * @param {object} obj
2938    *        An object on which to call the close() method when this
2939    *        extension is shut down.
2940    */
2941   callOnClose(obj) {
2942     this.onShutdown.add(obj);
2943   }
2945   forgetOnClose(obj) {
2946     this.onShutdown.delete(obj);
2947   }
2949   get builtinMessages() {
2950     return new Map([["@@extension_id", this.uuid]]);
2951   }
2953   // Reads the locale file for the given Gecko-compatible locale code, or if
2954   // no locale is given, the available locale closest to the UI locale.
2955   // Sets the currently selected locale on success.
2956   async initLocale(locale = undefined) {
2957     if (locale === undefined) {
2958       let locales = await this.promiseLocales();
2960       let matches = Services.locale.negotiateLanguages(
2961         Services.locale.appLocalesAsBCP47,
2962         Array.from(locales.keys()),
2963         this.defaultLocale
2964       );
2966       locale = matches[0];
2967     }
2969     return super.initLocale(locale);
2970   }
2972   /**
2973    * Clear cached resources associated to the extension principal
2974    * when an extension is installed (in case we were unable to do that at
2975    * uninstall time) or when it is being upgraded or downgraded.
2976    *
2977    * @param {string|undefined} reason
2978    *        BOOTSTRAP_REASON string, if provided. The value is expected to be
2979    *        `undefined` for extension objects without a corresponding AddonManager
2980    *        addon wrapper (e.g. test extensions created using `ExtensionTestUtils`
2981    *        without `useAddonManager` optional property).
2982    *
2983    * @returns {Promise<void>}
2984    *        Promise resolved when the nsIClearDataService async method call
2985    *        has been completed.
2986    */
2987   async clearCache(reason) {
2988     switch (reason) {
2989       case "ADDON_INSTALL":
2990       case "ADDON_UPGRADE":
2991       case "ADDON_DOWNGRADE":
2992         return clearCacheForExtensionPrincipal(this.principal);
2993     }
2994   }
2996   /**
2997    * Update site permissions as necessary.
2998    *
2999    * @param {string|undefined} reason
3000    *        If provided, this is a BOOTSTRAP_REASON string.  If reason is undefined,
3001    *        addon permissions are being added or removed that may effect the site permissions.
3002    */
3003   updatePermissions(reason) {
3004     const { principal } = this;
3006     const testPermission = perm =>
3007       Services.perms.testPermissionFromPrincipal(principal, perm);
3009     const addUnlimitedStoragePermissions = () => {
3010       // Set the indexedDB permission and a custom "WebExtensions-unlimitedStorage" to
3011       // remember that the permission hasn't been selected manually by the user.
3012       Services.perms.addFromPrincipal(
3013         principal,
3014         "WebExtensions-unlimitedStorage",
3015         Services.perms.ALLOW_ACTION
3016       );
3017       Services.perms.addFromPrincipal(
3018         principal,
3019         "persistent-storage",
3020         Services.perms.ALLOW_ACTION
3021       );
3022     };
3024     // Only update storage permissions when the extension changes in
3025     // some way.
3026     if (reason !== "APP_STARTUP" && reason !== "APP_SHUTDOWN") {
3027       if (this.hasPermission("unlimitedStorage")) {
3028         addUnlimitedStoragePermissions();
3029       } else {
3030         // Remove the indexedDB permission if it has been enabled using the
3031         // unlimitedStorage WebExtensions permissions.
3032         Services.perms.removeFromPrincipal(
3033           principal,
3034           "WebExtensions-unlimitedStorage"
3035         );
3036         Services.perms.removeFromPrincipal(principal, "persistent-storage");
3037       }
3038     } else if (
3039       reason === "APP_STARTUP" &&
3040       this.hasPermission("unlimitedStorage") &&
3041       testPermission("persistent-storage") !== Services.perms.ALLOW_ACTION
3042     ) {
3043       // If the extension does have the unlimitedStorage permission, but the
3044       // expected site permissions are missing during the app startup, then
3045       // add them back (See Bug 1454192).
3046       addUnlimitedStoragePermissions();
3047     }
3049     // Never change geolocation permissions at shutdown, since it uses a
3050     // session-only permission.
3051     if (reason !== "APP_SHUTDOWN") {
3052       if (this.hasPermission("geolocation")) {
3053         if (testPermission("geo") === Services.perms.UNKNOWN_ACTION) {
3054           Services.perms.addFromPrincipal(
3055             principal,
3056             "geo",
3057             Services.perms.ALLOW_ACTION,
3058             Services.perms.EXPIRE_SESSION
3059           );
3060         }
3061       } else if (
3062         reason !== "APP_STARTUP" &&
3063         testPermission("geo") === Services.perms.ALLOW_ACTION
3064       ) {
3065         Services.perms.removeFromPrincipal(principal, "geo");
3066       }
3067     }
3068   }
3070   async startup() {
3071     this.state = "Startup";
3073     // readyPromise is resolved with the policy upon success,
3074     // and with null if startup was interrupted.
3075     let resolveReadyPromise;
3076     let readyPromise = new Promise(resolve => {
3077       resolveReadyPromise = resolve;
3078     });
3080     // Create a temporary policy object for the devtools and add-on
3081     // manager callers that depend on it being available early.
3082     this.policy = new WebExtensionPolicy({
3083       id: this.id,
3084       mozExtensionHostname: this.uuid,
3085       baseURL: this.resourceURL,
3086       isPrivileged: this.isPrivileged,
3087       temporarilyInstalled: this.temporarilyInstalled,
3088       allowedOrigins: new MatchPatternSet([]),
3089       localizeCallback() {},
3090       readyPromise,
3091     });
3093     this.policy.extension = this;
3094     if (!WebExtensionPolicy.getByID(this.id)) {
3095       this.policy.active = true;
3096     }
3098     pendingExtensions.set(this.id, {
3099       mozExtensionHostname: this.uuid,
3100       baseURL: this.resourceURL,
3101       isPrivileged: this.isPrivileged,
3102     });
3103     sharedData.set("extensions/pending", pendingExtensions);
3105     lazy.ExtensionTelemetry.extensionStartup.stopwatchStart(this);
3106     try {
3107       this.state = "Startup: Loading manifest";
3108       await this.loadManifest();
3109       this.state = "Startup: Loaded manifest";
3111       if (!this.hasShutdown) {
3112         this.state = "Startup: Init locale";
3113         await this.initLocale();
3114         this.state = "Startup: Initted locale";
3115       }
3117       this.ensureNoErrors();
3119       if (this.hasShutdown) {
3120         // Startup was interrupted and shutdown() has taken care of unloading
3121         // the extension and running cleanup logic.
3122         return;
3123       }
3125       await this.clearCache(this.startupReason);
3126       this._setupStartupPermissions();
3128       GlobalManager.init(this);
3130       if (this.hasPermission("scripting")) {
3131         this.state = "Startup: Initialize scripting store";
3132         // We have to await here because `initSharedData` depends on the data
3133         // fetched from the scripting store. This has to be done early because
3134         // we need the data to run the content scripts in existing pages at
3135         // startup.
3136         try {
3137           await lazy.ExtensionScriptingStore.initExtension(this);
3138           this.state = "Startup: Scripting store initialized";
3139         } catch (err) {
3140           this.logError(`Failed to initialize scripting store: ${err}`);
3141         }
3142       }
3144       this.initSharedData();
3146       this.policy.active = false;
3147       this.policy = lazy.ExtensionProcessScript.initExtension(this);
3148       this.policy.extension = this;
3150       this.updatePermissions(this.startupReason);
3152       // Select the storage.local backend if it is already known,
3153       // and start the data migration if needed.
3154       if (this.hasPermission("storage")) {
3155         if (!lazy.ExtensionStorageIDB.isBackendEnabled) {
3156           this.setSharedData("storageIDBBackend", false);
3157         } else if (lazy.ExtensionStorageIDB.isMigratedExtension(this)) {
3158           this.setSharedData("storageIDBBackend", true);
3159           this.setSharedData(
3160             "storageIDBPrincipal",
3161             lazy.ExtensionStorageIDB.getStoragePrincipal(this)
3162           );
3163         } else if (
3164           this.startupReason === "ADDON_INSTALL" &&
3165           !Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)
3166         ) {
3167           // If the extension has been just installed, set it as migrated,
3168           // because there will not be any data to migrate.
3169           lazy.ExtensionStorageIDB.setMigratedExtensionPref(this, true);
3170           this.setSharedData("storageIDBBackend", true);
3171           this.setSharedData(
3172             "storageIDBPrincipal",
3173             lazy.ExtensionStorageIDB.getStoragePrincipal(this)
3174           );
3175         }
3176       }
3178       // Initialize DNR for the extension, only if the extension
3179       // has the required DNR permissions and without blocking
3180       // the extension startup on DNR being fully initialized.
3181       if (
3182         this.hasPermission("declarativeNetRequest") ||
3183         this.hasPermission("declarativeNetRequestWithHostAccess")
3184       ) {
3185         lazy.ExtensionDNR.ensureInitialized(this);
3186       }
3188       resolveReadyPromise(this.policy);
3190       // The "startup" Management event sent on the extension instance itself
3191       // is emitted just before the Management "startup" event,
3192       // and it is used to run code that needs to be executed before
3193       // any of the "startup" listeners.
3194       this.emit("startup", this);
3196       this.startupStates.clear();
3197       await Promise.all([
3198         this.addStartupStatePromise("Startup: Emit startup", () =>
3199           Management.emit("startup", this)
3200         ),
3201         this.addStartupStatePromise("Startup: Run manifest", () =>
3202           this.runManifest(this.manifest)
3203         ),
3204       ]);
3205       this.state = "Startup: Ran manifest";
3207       Management.emit("ready", this);
3208       this.emit("ready");
3210       this.state = "Startup: Complete";
3211     } catch (e) {
3212       this.state = `Startup: Error: ${e}`;
3214       Cu.reportError(e);
3216       if (this.policy) {
3217         this.policy.active = false;
3218       }
3220       this.cleanupGeneratedFile();
3222       throw e;
3223     } finally {
3224       lazy.ExtensionTelemetry.extensionStartup.stopwatchFinish(this);
3225       // Mark readyPromise as resolved in case it has not happened before,
3226       // e.g. due to an early return or an error.
3227       resolveReadyPromise(null);
3228     }
3229   }
3231   _setupStartupPermissions() {
3232     // We automatically add permissions to system/built-in extensions.
3233     // Extensions expliticy stating not_allowed will never get permission.
3234     let isAllowed = this.permissions.has(PRIVATE_ALLOWED_PERMISSION);
3235     if (this.manifest.incognito === "not_allowed") {
3236       // If an extension previously had permission, but upgrades/downgrades to
3237       // a version that specifies "not_allowed" in manifest, remove the
3238       // permission.
3239       if (isAllowed) {
3240         lazy.ExtensionPermissions.remove(this.id, {
3241           permissions: [PRIVATE_ALLOWED_PERMISSION],
3242           origins: [],
3243         });
3244         this.permissions.delete(PRIVATE_ALLOWED_PERMISSION);
3245       }
3246     } else if (!isAllowed && this.isPrivileged && !this.temporarilyInstalled) {
3247       // Add to EP so it is preserved after ADDON_INSTALL.  We don't wait on the add here
3248       // since we are pushing the value into this.permissions.  EP will eventually save.
3249       lazy.ExtensionPermissions.add(this.id, {
3250         permissions: [PRIVATE_ALLOWED_PERMISSION],
3251         origins: [],
3252       });
3253       this.permissions.add(PRIVATE_ALLOWED_PERMISSION);
3254     }
3256     // Allow other extensions to access static themes in private browsing windows
3257     // (See Bug 1790115).
3258     if (this.type === "theme") {
3259       this.permissions.add(PRIVATE_ALLOWED_PERMISSION);
3260     }
3262     // We only want to update the SVG_CONTEXT_PROPERTIES_PERMISSION during install and
3263     // upgrade/downgrade startups.
3264     if (INSTALL_AND_UPDATE_STARTUP_REASONS.has(this.startupReason)) {
3265       if (isMozillaExtension(this)) {
3266         // Add to EP so it is preserved after ADDON_INSTALL.  We don't wait on the add here
3267         // since we are pushing the value into this.permissions.  EP will eventually save.
3268         lazy.ExtensionPermissions.add(this.id, {
3269           permissions: [SVG_CONTEXT_PROPERTIES_PERMISSION],
3270           origins: [],
3271         });
3272         this.permissions.add(SVG_CONTEXT_PROPERTIES_PERMISSION);
3273       } else {
3274         lazy.ExtensionPermissions.remove(this.id, {
3275           permissions: [SVG_CONTEXT_PROPERTIES_PERMISSION],
3276           origins: [],
3277         });
3278         this.permissions.delete(SVG_CONTEXT_PROPERTIES_PERMISSION);
3279       }
3280     }
3282     // Ensure devtools permission is set
3283     if (
3284       this.manifest.devtools_page &&
3285       !this.manifest.optional_permissions.includes("devtools")
3286     ) {
3287       lazy.ExtensionPermissions.add(this.id, {
3288         permissions: ["devtools"],
3289         origins: [],
3290       });
3291       this.permissions.add("devtools");
3292     }
3293   }
3295   cleanupGeneratedFile() {
3296     if (!this.cleanupFile) {
3297       return;
3298     }
3300     let file = this.cleanupFile;
3301     this.cleanupFile = null;
3303     Services.obs.removeObserver(this, "xpcom-shutdown");
3305     return this.broadcast("Extension:FlushJarCache", { path: file.path })
3306       .then(() => {
3307         // We can't delete this file until everyone using it has
3308         // closed it (because Windows is dumb). So we wait for all the
3309         // child processes (including the parent) to flush their JAR
3310         // caches. These caches may keep the file open.
3311         file.remove(false);
3312       })
3313       .catch(Cu.reportError);
3314   }
3316   async shutdown(reason) {
3317     this.state = "Shutdown";
3319     this.hasShutdown = true;
3321     if (!this.policy) {
3322       return;
3323     }
3325     if (
3326       this.hasPermission("storage") &&
3327       lazy.ExtensionStorageIDB.selectedBackendPromises.has(this)
3328     ) {
3329       this.state = "Shutdown: Storage";
3331       // Wait the data migration to complete.
3332       try {
3333         await lazy.ExtensionStorageIDB.selectedBackendPromises.get(this);
3334       } catch (err) {
3335         Cu.reportError(
3336           `Error while waiting for extension data migration on shutdown: ${this.policy.debugName} - ${err.message}::${err.stack}`
3337         );
3338       }
3339       this.state = "Shutdown: Storage complete";
3340     }
3342     if (this.rootURI instanceof Ci.nsIJARURI) {
3343       this.state = "Shutdown: Flush jar cache";
3344       let file = this.rootURI.JARFile.QueryInterface(Ci.nsIFileURL).file;
3345       Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {
3346         path: file.path,
3347       });
3348       this.state = "Shutdown: Flushed jar cache";
3349     }
3351     const isAppShutdown = reason === "APP_SHUTDOWN";
3352     if (this.cleanupFile || !isAppShutdown) {
3353       StartupCache.clearAddonData(this.id);
3354     }
3356     activeExtensionIDs.delete(this.id);
3357     sharedData.set("extensions/activeIDs", activeExtensionIDs);
3359     for (let key of this.sharedDataKeys) {
3360       sharedData.delete(key);
3361     }
3363     Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
3365     this.updatePermissions(reason);
3367     // The service worker registrations related to the extensions are unregistered
3368     // only when the extension is not shutting down as part of the application
3369     // shutdown (a previously registered service worker is expected to stay
3370     // active across browser restarts), the service worker may have been
3371     // registered through the manifest.json background.service_worker property
3372     // or from an extension page through the service worker API if allowed
3373     // through the about:config pref.
3374     if (!isAppShutdown) {
3375       this.state = "Shutdown: ServiceWorkers";
3376       // TODO: ServiceWorkerCleanUp may go away once Bug 1183245 is fixed.
3377       await lazy.ServiceWorkerCleanUp.removeFromPrincipal(this.principal);
3378       this.state = "Shutdown: ServiceWorkers completed";
3379     }
3381     if (!this.manifest) {
3382       this.state = "Shutdown: Complete: No manifest";
3383       this.policy.active = false;
3385       return this.cleanupGeneratedFile();
3386     }
3388     GlobalManager.uninit(this);
3390     for (let obj of this.onShutdown) {
3391       obj.close();
3392     }
3394     ParentAPIManager.shutdownExtension(this.id, reason);
3396     Management.emit("shutdown", this);
3397     this.emit("shutdown", isAppShutdown);
3399     const TIMED_OUT = Symbol();
3401     this.state = "Shutdown: Emit shutdown";
3402     let result = await Promise.race([
3403       this.broadcast("Extension:Shutdown", { id: this.id }),
3404       promiseTimeout(CHILD_SHUTDOWN_TIMEOUT_MS).then(() => TIMED_OUT),
3405     ]);
3406     this.state = `Shutdown: Emitted shutdown: ${result === TIMED_OUT}`;
3407     if (result === TIMED_OUT) {
3408       Cu.reportError(
3409         `Timeout while waiting for extension child to shutdown: ${this.policy.debugName}`
3410       );
3411     }
3413     this.policy.active = false;
3415     this.state = `Shutdown: Complete (${this.cleanupFile})`;
3416     return this.cleanupGeneratedFile();
3417   }
3419   observe(subject, topic, data) {
3420     if (topic === "xpcom-shutdown") {
3421       this.cleanupGeneratedFile();
3422     }
3423   }
3425   get name() {
3426     return this.manifest.name;
3427   }
3429   get optionalOrigins() {
3430     if (this._optionalOrigins == null) {
3431       let { origins } = this.manifestOptionalPermissions;
3432       this._optionalOrigins = new MatchPatternSet(origins, {
3433         restrictSchemes: this.restrictSchemes,
3434         ignorePath: true,
3435       });
3436     }
3437     return this._optionalOrigins;
3438   }
3440   get hasBrowserActionUI() {
3441     return this.manifest.browser_action || this.manifest.action;
3442   }
3445 class Dictionary extends ExtensionData {
3446   constructor(addonData, startupReason) {
3447     super(addonData.resourceURI);
3448     this.id = addonData.id;
3449     this.startupData = addonData.startupData;
3450   }
3452   static getBootstrapScope() {
3453     return new DictionaryBootstrapScope();
3454   }
3456   async startup(reason) {
3457     this.dictionaries = {};
3458     for (let [lang, path] of Object.entries(this.startupData.dictionaries)) {
3459       let uri = Services.io.newURI(
3460         path.slice(0, -4) + ".aff",
3461         null,
3462         this.rootURI
3463       );
3464       this.dictionaries[lang] = uri;
3466       lazy.spellCheck.addDictionary(lang, uri);
3467     }
3469     Management.emit("ready", this);
3470   }
3472   async shutdown(reason) {
3473     if (reason !== "APP_SHUTDOWN") {
3474       lazy.AddonManagerPrivate.unregisterDictionaries(this.dictionaries);
3475     }
3476   }
3479 class Langpack extends ExtensionData {
3480   constructor(addonData, startupReason) {
3481     super(addonData.resourceURI);
3482     this.startupData = addonData.startupData;
3483     this.manifestCacheKey = [addonData.id, addonData.version];
3484   }
3486   static getBootstrapScope() {
3487     return new LangpackBootstrapScope();
3488   }
3490   async promiseLocales(locale) {
3491     let locales = await StartupCache.locales.get(
3492       [this.id, "@@all_locales"],
3493       () => this._promiseLocaleMap()
3494     );
3496     return this._setupLocaleData(locales);
3497   }
3499   parseManifest() {
3500     return StartupCache.manifests.get(this.manifestCacheKey, () =>
3501       super.parseManifest()
3502     );
3503   }
3505   async startup(reason) {
3506     this.chromeRegistryHandle = null;
3507     if (this.startupData.chromeEntries.length) {
3508       const manifestURI = Services.io.newURI(
3509         "manifest.json",
3510         null,
3511         this.rootURI
3512       );
3513       this.chromeRegistryHandle = lazy.aomStartup.registerChrome(
3514         manifestURI,
3515         this.startupData.chromeEntries
3516       );
3517     }
3519     const langpackId = this.startupData.langpackId;
3520     const l10nRegistrySources = this.startupData.l10nRegistrySources;
3522     lazy.resourceProtocol.setSubstitution(langpackId, this.rootURI);
3524     const fileSources = Object.entries(l10nRegistrySources).map(entry => {
3525       const [sourceName, basePath] = entry;
3526       return new L10nFileSource(
3527         `${sourceName}-${langpackId}`,
3528         langpackId,
3529         this.startupData.languages,
3530         `resource://${langpackId}/${basePath}localization/{locale}/`
3531       );
3532     });
3534     L10nRegistry.getInstance().registerSources(fileSources);
3536     Services.obs.notifyObservers(
3537       { wrappedJSObject: { langpack: this } },
3538       "webextension-langpack-startup"
3539     );
3540   }
3542   async shutdown(reason) {
3543     if (reason === "APP_SHUTDOWN") {
3544       // If we're shutting down, let's not bother updating the state of each
3545       // system.
3546       return;
3547     }
3549     const sourcesToRemove = Object.keys(
3550       this.startupData.l10nRegistrySources
3551     ).map(sourceName => `${sourceName}-${this.startupData.langpackId}`);
3552     L10nRegistry.getInstance().removeSources(sourcesToRemove);
3554     if (this.chromeRegistryHandle) {
3555       this.chromeRegistryHandle.destruct();
3556       this.chromeRegistryHandle = null;
3557     }
3559     lazy.resourceProtocol.setSubstitution(this.startupData.langpackId, null);
3560   }
3563 // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
3564 class SitePermission extends ExtensionData {
3565   constructor(addonData, startupReason) {
3566     super(addonData.resourceURI);
3567     this.id = addonData.id;
3568     this.hasShutdown = false;
3569   }
3571   async loadManifest() {
3572     let [manifestData] = await Promise.all([this.parseManifest()]);
3574     if (!manifestData) {
3575       return;
3576     }
3578     this.manifest = manifestData.manifest;
3579     this.type = manifestData.type;
3580     this.sitePermissions = this.manifest.site_permissions;
3581     // 1 install_origins is mandatory for this addon type
3582     this.siteOrigin = this.manifest.install_origins[0];
3584     return this.manifest;
3585   }
3587   static getBootstrapScope() {
3588     return new SitePermissionBootstrapScope();
3589   }
3591   // Array of principals that may be set by the addon.
3592   getSupportedPrincipals() {
3593     if (!this.siteOrigin) {
3594       return [];
3595     }
3596     const uri = Services.io.newURI(this.siteOrigin);
3597     return [
3598       Services.scriptSecurityManager.createContentPrincipal(uri, {}),
3599       Services.scriptSecurityManager.createContentPrincipal(uri, {
3600         privateBrowsingId: 1,
3601       }),
3602     ];
3603   }
3605   async startup(reason) {
3606     await this.loadManifest();
3608     this.ensureNoErrors();
3610     let site_permissions = await lazy.SCHEMA_SITE_PERMISSIONS;
3611     let perms = await lazy.ExtensionPermissions.get(this.id);
3613     if (this.hasShutdown) {
3614       // Startup was interrupted and shutdown() has taken care of unloading
3615       // the extension and running cleanup logic.
3616       return;
3617     }
3619     let privateAllowed = perms.permissions.includes(PRIVATE_ALLOWED_PERMISSION);
3620     let principals = this.getSupportedPrincipals();
3622     // Remove any permissions not contained in site_permissions
3623     for (let principal of principals) {
3624       let existing = Services.perms.getAllForPrincipal(principal);
3625       for (let perm of existing) {
3626         if (
3627           site_permissions.includes(perm) &&
3628           !this.sitePermissions.includes(perm)
3629         ) {
3630           Services.perms.removeFromPrincipal(principal, perm);
3631         }
3632       }
3633     }
3635     // Ensure all permissions in site_permissions have been set, but do not
3636     // overwrite the permission so the user can override the values in preferences.
3637     for (let perm of this.sitePermissions) {
3638       for (let principal of principals) {
3639         let permission = Services.perms.testExactPermissionFromPrincipal(
3640           principal,
3641           perm
3642         );
3643         if (permission == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
3644           let { privateBrowsingId } = principal.originAttributes;
3645           let allow = privateBrowsingId == 0 || privateAllowed;
3646           Services.perms.addFromPrincipal(
3647             principal,
3648             perm,
3649             allow ? Services.perms.ALLOW_ACTION : Services.perms.DENY_ACTION,
3650             Services.perms.EXPIRE_NEVER
3651           );
3652         }
3653       }
3654     }
3656     Services.obs.notifyObservers(
3657       { wrappedJSObject: { sitepermissions: this } },
3658       "webextension-sitepermissions-startup"
3659     );
3660   }
3662   async shutdown(reason) {
3663     this.hasShutdown = true;
3664     // Permissions are retained across restarts
3665     if (reason == "APP_SHUTDOWN") {
3666       return;
3667     }
3668     let principals = this.getSupportedPrincipals();
3670     for (let perm of this.sitePermissions || []) {
3671       for (let principal of principals) {
3672         Services.perms.removeFromPrincipal(principal, perm);
3673       }
3674     }
3675   }