1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 var EXPORTED_SYMBOLS = [
15 "ExtensionAddonObserver",
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
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.
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).
43 const { XPCOMUtils } = ChromeUtils.importESModule(
44 "resource://gre/modules/XPCOMUtils.sys.mjs"
46 const { AppConstants } = ChromeUtils.importESModule(
47 "resource://gre/modules/AppConstants.sys.mjs"
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",
59 "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
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
82 basename: "resource://gre/modules/osfile/ospath_unix.jsm",
83 dirname: "resource://gre/modules/osfile/ospath_unix.jsm",
86 XPCOMUtils.defineLazyGetter(lazy, "resourceProtocol", () =>
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, {
104 "@mozilla.org/addons/addon-manager-startup;1",
105 "amIAddonManagerStartup",
107 spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
110 XPCOMUtils.defineLazyPreferenceGetter(
113 "dom.ipc.processCount.extension"
116 // Temporary pref to be turned on when ready.
117 XPCOMUtils.defineLazyPreferenceGetter(
119 "userContextIsolation",
120 "extensions.userContextIsolation.enabled",
124 XPCOMUtils.defineLazyPreferenceGetter(
126 "userContextIsolationDefaultRestricted",
127 "extensions.userContextIsolation.defaults.restricted",
131 // This pref modifies behavior for MV2. MV3 is enabled regardless.
132 XPCOMUtils.defineLazyPreferenceGetter(
135 "extensions.eventPages.enabled"
142 apiManager: Management,
145 const { getUniqueId, promiseTimeout } = ExtensionUtils;
147 const { EventEmitter, updateAllowedOrigins } = ExtensionCommon;
149 XPCOMUtils.defineLazyGetter(
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();
159 lazy.Schemas.getPermissionNames([
160 "PermissionNoPrompt",
161 "OptionalPermissionNoPrompt",
162 "PermissionPrivileged",
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([
193 "normandyAddonStudy",
198 const PRIVILEGED_PERMS_ANDROID_ONLY = new Set([
200 "nativeMessagingFromContent",
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);
213 AppConstants.MOZ_APP_NAME != "firefox" ||
214 AppConstants.platform == "android"
216 for (const perm of PRIVILEGED_PERMS_DESKTOP_ONLY) {
217 PRIVILEGED_PERMS.delete(perm);
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([
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)) {
254 `isMozillaExtension called with unexpected startupReason: ${startupReason}`
262 if (id.endsWith("@mozilla.com") || id.endsWith("@mozilla.org")) {
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(
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)) {
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
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.
304 function classifyPermission(perm, restrictSchemes, isPrivileged) {
305 let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
308 let { pattern } = new MatchPattern(perm, {
312 return { origin: pattern };
314 return { invalid: perm };
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 };
321 perm.startsWith("declarativeNetRequest") &&
322 !isDNRPermissionAllowed(perm)
324 return { invalid: perm };
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(
340 " (?:[^"\\\n] | \\.)* "
345 `.replace(/\s+/g, ""),
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.
357 let pref = Services.prefs.getStringPref(UUID_MAP_PREF, "{}");
359 return JSON.parse(pref);
361 Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
367 Services.prefs.setStringPref(UUID_MAP_PREF, JSON.stringify(map));
370 get(id, create = true) {
371 let map = this._read();
379 uuid = Services.uuid.generateUUID().number;
380 uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
389 let map = this._read();
395 function clearCacheForExtensionPrincipal(principal, clearAll = false) {
396 if (!principal.schemeIs("moz-extension")) {
397 return Promise.reject(new Error("Unexpected non extension principal"));
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(
418 * Observer AddonManager events and translate them into extension events,
419 * as well as handle any last cleanup after uninstalling an extension.
421 var ExtensionAddonObserver = {
425 if (!this.initialized) {
426 lazy.AddonManager.addAddonListener(this);
427 this.initialized = true;
431 // AddonTestUtils will call this as necessary.
433 if (this.initialized) {
434 lazy.AddonManager.removeAddonListener(this);
435 this.initialized = false;
440 if (addon.type !== "extension") {
443 Management._callHandlers([addon.id], "enabling", "onEnabling");
447 if (addon.type !== "extension") {
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
456 Management._callHandlers([addon.id], "disable", "onDisable");
460 onUninstalling(addon) {
461 let extension = GlobalManager.extensionMap.get(addon.id);
463 // Let any other interested listeners respond
464 // (e.g., display the uninstall URL)
465 Management.emit("uninstalling", extension);
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);
479 let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
480 let principal = Services.scriptSecurityManager.createContentPrincipal(
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)
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).
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).
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
508 lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
509 `Clear ServiceWorkers for ${addon.id}`,
510 lazy.ServiceWorkerCleanUp.removeFromPrincipal(principal)
513 // Clear the persisted dynamic content scripts created with the scripting
515 lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
516 `Clear scripting store for ${addon.id}`,
517 lazy.ExtensionScriptingStore.clearOnUninstall(addon.id)
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)
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 })
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(
541 userContextId: WEBEXT_STORAGE_USER_CONTEXT_ID,
544 Services.qms.clearStoragesForPrincipal(storagePrincipal);
546 lazy.ExtensionStorageIDB.clearMigratedExtensionPref(addon.id);
548 // If LSNG is not enabled, we need to clear localStorage explicitly using
550 if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
551 // Clear localStorage created by the extension
552 let storage = Services.domStorageManager.getStorage(
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(
566 "WebExtensions-unlimitedStorage"
568 Services.perms.removeFromPrincipal(principal, "persistent-storage");
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
575 for (let perm of permissions) {
576 if (perm.principal.equalsURI(baseURI)) {
577 Services.perms.removePermission(perm);
581 if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) {
582 // Clear the entry in the UUID map
583 UUIDMap.remove(addon.id);
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.
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;
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;
631 this.eventPagesEnabled = lazy.eventPagesEnabled;
635 * A factory function that allows the construction of ExtensionData, with
636 * the isPrivileged flag computed asynchronously.
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}
648 static async constructAsync({
651 temporarilyInstalled,
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;
662 static getIsPrivileged({ signedState, builtIn, temporarilyInstalled }) {
664 signedState === lazy.AddonManager.SIGNEDSTATE_PRIVILEGED ||
665 signedState === lazy.AddonManager.SIGNEDSTATE_SYSTEM ||
667 (lazy.AddonSettings.EXPERIMENTS_ENABLED && temporarilyInstalled)
671 get builtinMessages() {
676 let id = this.id || "<unknown>";
677 return lazy.Log.repository.getLogger(LOGGER_ID_BASE + id);
681 * Report an error about the extension's manifest file.
683 * @param {string} message The error message
685 manifestError(message) {
686 this.packagingError(`Reading manifest: ${message}`);
690 * Report a warning about the extension's manifest file.
692 * @param {string} message The warning message
694 manifestWarning(message) {
695 this.packagingWarning(`Reading manifest: ${message}`);
698 // Report an error about the extension's general packaging.
699 packagingError(message) {
700 this.errors.push(message);
701 this.logError(message);
704 packagingWarning(message) {
705 this.warnings.push(message);
706 this.logWarning(message);
709 logWarning(message) {
710 this._logMessage(message, "warn");
714 this._logMessage(message, "error");
717 _logMessage(message, severity) {
718 this.logger[severity](`Loading extension '${this.id}': ${message}`);
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"));
730 * Returns the moz-extension: URL for the given path within this
733 * Must not be called unless either the `id` or `uuid` property has
736 * @param {string} path The path portion of the URL.
740 if (!(this.id || this.uuid)) {
742 "getURL may not be called before an `id` or `uuid` has been set"
746 this.uuid = UUIDMap.get(this.id);
748 return `moz-extension://${this.uuid}/${path}`;
752 * Discovers the file names within a directory or JAR file.
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).
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;
768 let children = await IOUtils.getChildren(fullPath);
769 for (let child of children) {
772 (await IOUtils.stat(child)).type == "directory"
774 results.push(PathUtils.filename(child));
778 // Fall-through, return what we have.
783 let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
785 // Append the sub-directory path to the base JAR URI and normalize the
787 let entry = `${uri.JAREntry}/${path}/`
788 .replace(/\/\/+/g, "/")
790 uri = Services.io.newURI(`jar:${uri.JARFile.spec}!/${entry}`);
793 for (let name of lazy.aomStartup.enumerateJARSubtree(uri)) {
794 if (!name.startsWith(entry)) {
795 throw new Error("Unexpected ZipReader entry");
798 // The enumerator returns the full path of all entries.
799 // Trim off the leading path, and filter out entries from
801 name = name.slice(entry.length);
805 (!directoriesOnly || name.endsWith("/"))
807 results.push(name.replace("/", ""));
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})`));
828 let text = lazy.NetUtil.readInputStreamToString(
830 inputStream.available(),
834 text = text.replace(COMMENT_REGEXP, "$1");
836 resolve(JSON.parse(text));
845 get restrictSchemes() {
846 return !(this.isPrivileged && this.hasPermission("mozillaAddons"));
850 * Given an array of host and permissions, generate a structured permissions object
851 * that contains seperate host origins and permissions arrays.
853 * @param {Array} permissionsArray
854 * @param {Array} [hostPermissions]
855 * @returns {object} permissions object
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);
866 } else if (type.permission) {
867 permissions.add(perm);
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).
884 get manifestPermissions() {
885 if (this.type !== "extension") {
889 let { permissions } = this.permissionsObject(this.manifest.permissions);
892 this.manifest.devtools_page &&
893 !this.manifest.optional_permissions.includes("devtools")
895 permissions.add("devtools");
899 permissions: Array.from(permissions),
900 origins: this.originControls ? [] : this.getManifestOrigins(),
905 * @returns {string[]} all origins that are referenced in manifest via
906 * permissions, host_permissions, or content_scripts keys.
908 getManifestOrigins() {
909 if (this.type !== "extension") {
913 let { origins } = this.permissionsObject(
914 this.manifest.permissions,
915 this.manifest.host_permissions
918 for (let entry of this.manifest.content_scripts || []) {
919 for (let origin of entry.matches) {
924 return Array.from(origins);
928 * Returns optional permissions from the manifest, including host permissions
929 * if originControls is true.
931 get manifestOptionalPermissions() {
932 if (this.type !== "extension") {
936 let { permissions, origins } = this.permissionsObject(
937 this.manifest.optional_permissions
939 if (this.originControls) {
940 for (let origin of this.getManifestOrigins()) {
946 permissions: Array.from(permissions),
947 origins: Array.from(origins),
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.
956 get activePermissions() {
957 if (this.type !== "extension") {
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],
970 const EXP_PATTERN = /^experiments\.\w+/;
971 result.permissions = [...this.permissions].filter(
972 p => !result.origins.includes(p) && !EXP_PATTERN.test(p)
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);
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,
989 // formatPermissionStrings ignores any scheme, so only look at the domain.
990 origins: newPermissions.origins.filter(
992 !oldMatcher.subsumesDomain(
993 new MatchPattern(perm, { restrictSchemes: false })
996 permissions: newPermissions.permissions.filter(
997 perm => !oldPermissions.permissions.includes(perm)
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,
1009 origins: oldPermissions.origins.filter(perm =>
1010 matcher.subsumesDomain(
1011 new MatchPattern(perm, { restrictSchemes: false })
1014 permissions: oldPermissions.permissions.filter(perm =>
1015 newPermissions.permissions.includes(perm)
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.
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
1030 static async migratePermissions(
1033 oldOptionalPermissions,
1035 newOptionalPermissions
1037 let migrated = ExtensionData.intersectPermissions(
1039 newOptionalPermissions
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)
1051 let oldPerms = oldPermissions.permissions.concat(
1052 oldOptionalPermissions.permissions
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(
1065 // Remove any optional permissions that have been removed from the manifest.
1066 await lazy.ExtensionPermissions.remove(id, {
1067 permissions: removed,
1072 canUseAPIExperiment() {
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)
1084 canUseThemeExperiment() {
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).
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)
1098 get manifestVersion() {
1099 return this.manifest.manifest_version;
1102 get persistentBackground() {
1103 let { manifest } = this;
1105 !manifest.background ||
1106 manifest.background.service_worker ||
1107 this.manifestVersion > 2
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;
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.
1121 * @param {string} state starting, running, suspending or stopped
1123 set backgroundState(state) {
1124 this._backgroundState = state;
1127 get backgroundState() {
1128 return this._backgroundState;
1131 async getExtensionVersionWithoutValidation() {
1132 return (await this.readJSON("manifest.json")).version;
1136 * Load a locale and return a localized manifest. The extension must
1137 * be initialized, and manifest parsed prior to calling.
1139 * @param {string} locale to load, if necessary.
1140 * @returns {object} normalized manifest.
1142 async getLocalizedManifest(locale) {
1143 if (!this.type || !this.localeData) {
1144 throw new Error("The extension has not been initialized.");
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");
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);
1162 if (!this.localeData.has(locale)) {
1163 throw new Error(`The extension does not contain the locale ${locale}`);
1166 let normalized = await this._getNormalizedManifest(locale);
1167 if (normalized.error) {
1168 throw new Error(normalized.error);
1170 return normalized.value;
1173 async _getNormalizedManifest(locale) {
1174 let manifestType = manifestTypes.get(this.type);
1177 url: this.baseURI && this.baseURI.spec,
1178 principal: this.principal,
1179 logError: error => {
1180 this.manifestWarning(error);
1183 manifestVersion: this.manifestVersion,
1186 if (this.fluentL10n || this.localeData) {
1187 context.preprocessors.localize = (value, context) =>
1188 this.localize(value, locale);
1191 return lazy.Schemas.normalize(this.rawManifest, manifestType, context);
1194 async initializeAddonTypeAndID() {
1196 // Already initialized.
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";
1212 this.type = "extension";
1217 manifest.browser_specific_settings?.gecko ||
1218 manifest.applications?.gecko;
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") {
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();
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
1249 if (this.constructor != ExtensionData) {
1250 this.fluentL10n = new Localization(manifest.l10n_resources, true);
1252 } else if (this.temporarilyInstalled) {
1254 `Using 'l10n_resources' requires a privileged add-on. ` +
1255 PRIVILEGED_ADDONS_DEVDOCS_MESSAGE
1258 // Warn but don't make this fatal.
1259 this.manifestWarning(
1260 "Ignoring l10n_resources in unprivileged extension"
1265 let normalized = await this._getNormalizedManifest();
1266 if (normalized.error) {
1267 this.manifestError(normalized.error);
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.
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"`
1289 manifest.applications = manifest.browser_specific_settings;
1293 this.manifestVersion < 3 &&
1294 manifest.background &&
1295 !this.eventPagesEnabled &&
1296 !manifest.background.persistent
1298 this.logWarning("Event pages are not currently supported.");
1302 this.isPrivileged &&
1304 (manifest.action || manifest.browser_action || manifest.page_action)
1307 "Cannot use browser and/or page actions in hidden add-ons"
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.
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,
1335 webAccessibleResources,
1338 if (this.type === "extension") {
1339 let { isPrivileged } = this;
1340 let restrictSchemes = !(
1341 isPrivileged && manifest.permissions.includes("mozillaAddons")
1344 // Privileged and temporary extensions can opt out of originControls.
1346 (isPrivileged || this.temporarilyInstalled) &&
1347 manifest.granted_host_permissions
1349 result.originControls = false;
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",
1360 if (!acceptedExtensions.split(",").includes(this.id)) {
1362 "Only specific extensions are allowed to access the geckoProfiler."
1368 let type = classifyPermission(perm, restrictSchemes, isPrivileged);
1371 if (!result.originControls) {
1372 originPermissions.add(perm);
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) {
1381 `Using the privileged permission '${perm}' requires a privileged add-on. ` +
1382 PRIVILEGED_ADDONS_DEVDOCS_MESSAGE
1386 this.manifestWarning(`Invalid extension permission: ${perm}`);
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);
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);
1406 for (let origin of perms.origins) {
1407 originPermissions.add(origin);
1411 for (let api of apiNames) {
1412 dependencies.add(`${api}@experiments.addons.mozilla.org`);
1415 let moduleData = data => ({
1416 url: this.rootURI.resolve(data.script),
1417 events: data.events,
1419 scopes: data.scopes,
1422 let computeModuleInit = (scope, modules) => {
1423 let manager = new ExtensionCommon.SchemaAPIManager(scope);
1424 return manager.initModuleJSON([modules]);
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 || [],
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)) {
1456 this.readJSON(data.schema).then(json =>
1457 lazy.Schemas.processSchema(json)
1463 parentModules[name] = moduleData(data.parent);
1467 childModules[name] = moduleData(data.child);
1472 child: computeModuleInit("addon_child", childModules),
1473 parent: computeModuleInit("addon_parent", parentModules),
1475 } else if (this.temporarilyInstalled) {
1476 // Hard error for un-privileged temporary installs using experimental apis.
1478 `Using 'experiment_apis' requires a privileged add-on. ` +
1479 PRIVILEGED_ADDONS_DEVDOCS_MESSAGE
1482 this.manifestWarning(
1483 `Using experimental APIs requires a privileged add-on.`
1488 // Normalize all patterns to contain a single leading /
1489 if (manifest.web_accessible_resources) {
1490 // Normalize into V3 objects
1492 this.manifestVersion >= 3
1493 ? manifest.web_accessible_resources
1494 : [{ resources: manifest.web_accessible_resources }];
1495 webAccessibleResources.push(
1497 obj.resources = obj.resources.map(path =>
1498 path.replace(/^\/*/, "/")
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 || {}
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]]);
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 = "";
1544 // Add any additional sources listed in the manifest
1545 if (manifest.sources) {
1546 for (const [sourceName, { base_path }] of Object.entries(
1549 l10nRegistrySources[sourceName] = base_path;
1553 // 4. Save the list of languages handled by this langpack.
1554 const languages = Object.keys(manifest.languages);
1556 this.startupData = {
1559 l10nRegistrySources,
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);
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)) {
1577 `Invalid dictionary path specified for '${lang}': ${path}`
1580 if (!entries.includes(affixPath)) {
1582 `Invalid dictionary path specified for '${lang}': Missing affix file: ${path}`
1586 dictionaries[lang] = path;
1589 this.startupData = { dictionaries };
1592 if (schemaPromises.size) {
1593 let schemas = new Map();
1594 for (let [url, promise] of schemaPromises) {
1595 schemas.set(url, await promise);
1597 result.schemaURLs = schemas;
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(),
1611 if (!manifestData) {
1615 // Do not override the add-on id that has been already assigned.
1617 this.id = manifestData.id;
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,
1640 return this.manifest;
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(".")) {
1654 value = value[prop];
1657 return value != null;
1660 if (this.permissions.has(perm)) {
1664 if (includeOptional && this.manifest.optional_permissions.includes(perm)) {
1672 let apiManagers = [Management];
1674 for (let id of this.dependencies) {
1675 let policy = WebExtensionPolicy.getByID(id);
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}`);
1686 this.experimentAPIManager = new ExtensionCommon.LazyAPIManager(
1688 this.modules.parent,
1692 apiManagers.push(this.experimentAPIManager);
1695 if (apiManagers.length == 1) {
1696 return apiManagers[0];
1699 return new ExtensionCommon.MultiAPIManager("main", apiManagers.reverse());
1702 localizeMessage(...args) {
1703 return this.localeData.localizeMessage(...args);
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;
1719 if (this.localeData) {
1720 str = this.localeData.localize(str, locale);
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);
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;
1743 this.addonData.locationHidden ||
1744 (this.isPrivileged && this.manifest.hidden)
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, "-");
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`;
1763 let messages = await this.readJSON(file);
1764 return this.localeData.addLocale(locale, messages, this);
1766 this.packagingError(`Loading locale file ${file}: ${e}`);
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);
1783 _setupLocaleData(locales) {
1784 if (this.localeData) {
1785 return this.localeData.locales;
1788 this.localeData = new lazy.LocaleData({
1789 defaultLocale: this.defaultLocale,
1791 builtinMessages: this.builtinMessages,
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:
1802 // Map(gecko-locale-code -> locale-directory-name)
1804 if (!this._promiseLocales) {
1805 this._promiseLocales = (async () => {
1806 let locales = this._promiseLocaleMap();
1807 return this._setupLocaleData(locales);
1811 return this._promiseLocales;
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();
1822 Array.from(locales.keys(), locale => this.readLocaleFile(locale))
1825 let defaultLocale = this.defaultLocale;
1826 if (defaultLocale) {
1827 if (!locales.has(defaultLocale)) {
1829 'Value for "default_locale" property must correspond to ' +
1830 'a directory in "_locales/". Not found: ' +
1831 JSON.stringify(`_locales/${this.manifest.default_locale}/`)
1834 } else if (locales.size) {
1836 'The "default_locale" property is required when a ' +
1837 '"_locales/" directory is present.'
1841 return this.localeData.messages;
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.
1848 // Pre-loads the default locale for fallback message processing, regardless
1849 // of the locale specified.
1851 // If no locales are unavailable, resolves to |null|.
1852 async initLocale(locale = this.defaultLocale) {
1853 if (locale == null) {
1857 let promises = [this.readLocaleFile(locale)];
1859 let { defaultLocale } = this;
1860 if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
1861 promises.push(this.readLocaleFile(defaultLocale));
1864 let results = await Promise.all(promises);
1866 this.localeData.selectedLocale = locale;
1871 * @param {string} origin
1872 * @returns {boolean} If this is one of the "all sites" permission.
1874 static isAllSitesPermission(origin) {
1876 let info = ExtensionData.classifyOriginPermissions([origin], true);
1877 return !!info.allUrls;
1879 // Passed string is not an origin permission.
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
1894 * Classify host permissions
1896 * @param {Array<string>} origins
1897 * permission origins
1898 * @param {boolean} ignoreNonWebSchemes
1899 * return only these schemes: *, http, https, ws, wss
1901 * @returns {HostPermissions}
1903 static classifyOriginPermissions(origins = [], ignoreNonWebSchemes = false) {
1905 wildcards = 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;
1921 // Privileged extensions may request access to "about:"-URLs, such as
1923 let match = /^([a-z*]+):\/\/([^/]*)\/|^about:/.exec(permission);
1925 throw new Error(`Unparseable host permission ${permission}`);
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)) {
1935 if (!host || host == "*") {
1937 allUrls = permission;
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)}`);
1946 let pat = new MatchPattern(permission, {
1948 // Safe because used just for normalization, not for granting access.
1949 restrictSchemes: false,
1951 sitesMap.set(pat.pattern, `${scheme}://${host}`);
1954 return { allUrls, wildcards, sites, wildcardsMap, sitesMap };
1958 * Formats all the strings for a permissions dialog/notification.
1960 * @param {object} info Information about the permissions being requested.
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
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.
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.
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
2002 * "object.msgs" is an array of localized strings describing required permissions
2004 * "object.optionalPermissions" is a map of permission name to localized
2005 * strings describing the permission.
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.
2011 static formatPermissionStrings(
2015 collapseOrigins = false,
2016 buildOptionalOrigins = false,
2017 getKeyForPermission = perm => `webextPerms.description.${perm}`,
2022 optionalPermissions: {},
2023 optionalOrigins: {},
2026 const haveAccessKeys = AppConstants.platform !== "android";
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"
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]}`,
2056 result.text = bundle.GetStringFromName(
2057 `webextSitePerms.descriptionGatedPerms`
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) {
2069 bundle.GetStringFromName(
2070 `webextSitePerms.description.${permission}`
2075 `site_permission ${permission} missing readable text property`
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);
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]);
2097 let perms = info.permissions || { origins: [], permissions: [] };
2098 let optional_permissions = info.optionalPermissions || {
2103 // First classify our host permissions
2104 let { allUrls, wildcards, sites } = ExtensionData.classifyOriginPermissions(
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.
2113 bundle.GetStringFromName("webextPerms.hostDescription.allUrls")
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) {
2122 ...items.map(item => bundle.formatStringFromName(itemKey, [item]))
2125 if (list.length < 5 || !collapseOrigins) {
2128 formatItems(list.slice(0, 3));
2130 let remaining = list.length - 3;
2132 lazy.PluralForm.get(
2134 bundle.GetStringFromName(moreKey)
2135 ).replace("#1", remaining)
2141 Array.from(wildcards),
2142 "webextPerms.hostDescription.wildcard",
2143 "webextPerms.hostDescription.tooManyWildcards"
2147 "webextPerms.hostDescription.oneSite",
2148 "webextPerms.hostDescription.tooManySites"
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)) {
2156 bundle.formatStringFromName(getKeyForPermission(NATIVE_MSG_PERM), [
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()) {
2168 if (permission == NATIVE_MSG_PERM) {
2173 bundle.GetStringFromName(getKeyForPermission(permission))
2176 // We deliberately do not include all permissions in the prompt.
2177 // So if we don't find one then just skip it.
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[
2187 ] = bundle.formatStringFromName(getKeyForPermission(permission), [
2193 result.optionalPermissions[permission] = bundle.GetStringFromName(
2194 getKeyForPermission(permission)
2197 // We deliberately do not have strings for all permissions.
2198 // So if we don't find one then just skip it.
2202 let optionalInfo = ExtensionData.classifyOriginPermissions(
2203 optional_permissions.origins,
2206 if (optionalInfo.allUrls) {
2207 result.optionalOrigins[optionalInfo.allUrls] = bundle.GetStringFromName(
2208 "webextPerms.hostDescription.allUrls"
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;
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;
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"
2235 result.cancelText = bundle.GetStringFromName(
2236 "webextPerms.sideloadCancel.label"
2238 if (haveAccessKeys) {
2239 result.acceptKey = bundle.GetStringFromName(
2240 "webextPerms.sideloadEnable.accessKey"
2242 result.cancelKey = bundle.GetStringFromName(
2243 "webextPerms.sideloadCancel.accessKey"
2246 } else if (info.type == "update") {
2247 headerKey = "webextPerms.updateText2";
2249 result.acceptText = bundle.GetStringFromName(
2250 "webextPerms.updateAccept.label"
2252 if (haveAccessKeys) {
2253 result.acceptKey = bundle.GetStringFromName(
2254 "webextPerms.updateAccept.accessKey"
2257 } else if (info.type == "optional") {
2258 headerKey = "webextPerms.optionalPermsHeader";
2260 result.listIntro = bundle.GetStringFromName(
2261 "webextPerms.optionalPermsListIntro"
2263 result.acceptText = bundle.GetStringFromName(
2264 "webextPerms.optionalPermsAllow.label"
2266 result.cancelText = bundle.GetStringFromName(
2267 "webextPerms.optionalPermsDeny.label"
2269 if (haveAccessKeys) {
2270 result.acceptKey = bundle.GetStringFromName(
2271 "webextPerms.optionalPermsAllow.accessKey"
2273 result.cancelKey = bundle.GetStringFromName(
2274 "webextPerms.optionalPermsDeny.accessKey"
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";
2287 result.header = bundle.formatStringFromName(headerKey, ["<>"]);
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 });
2311 if (this.extension) {
2312 return { state: this.extension.state };
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
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(
2332 data.oldPermissions,
2333 data.oldOptionalPermissions,
2334 data.userPermissions || emptyPermissions,
2335 data.optionalPermissions || emptyPermissions
2339 return Management.emit("update", {
2341 resourceURI: data.resourceURI,
2342 isPrivileged: data.isPrivileged,
2346 startup(data, reason) {
2347 // eslint-disable-next-line no-use-before-define
2348 this.extension = new Extension(
2350 this.BOOTSTRAP_REASON_TO_STRING_MAP[reason],
2353 return this.extension.startup();
2356 async shutdown(data, reason) {
2357 let result = await this.extension.shutdown(
2358 this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]
2360 this.extension = null;
2365 XPCOMUtils.defineLazyGetter(
2366 BootstrapScope.prototype,
2367 "BOOTSTRAP_REASON_TO_STRING_MAP",
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",
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]);
2394 shutdown(data, reason) {
2395 this.dictionary.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2396 this.dictionary = null;
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]);
2411 shutdown(data, reason) {
2412 this.langpack.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2413 this.langpack = null;
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]
2430 shutdown(data, reason) {
2431 this.sitepermission.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
2432 this.sitepermission = null;
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
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;
2468 if (addonData.TEST_NO_ADDON_MANAGER) {
2469 this.dontSaveStartupData = true;
2471 if (addonData.TEST_NO_DELAYED_STARTUP) {
2472 this.testNoDelayedStartup = true;
2475 this.addonData = addonData;
2476 this.startupData = addonData.startupData || {};
2477 this.startupReason = startupReason;
2478 this.updateReason = updateReason;
2482 ["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)
2484 this.startupClearCachePromise = StartupCache.clearAddonData(addonData.id);
2487 this.remote = !WebExtensionPolicy.isExtensionProcess;
2488 this.remoteType = this.remote ? lazy.E10SUtils.EXTENSION_REMOTE_TYPE : null;
2490 if (this.remote && lazy.processCount !== 1) {
2492 "Out-of-process WebExtensions are not supported with multiple child processes"
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;
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);
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", {
2542 origins: permissions.origins,
2543 permissions: permissions.permissions,
2548 this.cachePermissions();
2549 this.updatePermissions();
2552 this.on("remove-permissions", (ignoreEvent, permissions) => {
2553 for (let perm of permissions.permissions) {
2554 this.permissions.delete(perm);
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", {
2566 origins: permissions.origins,
2567 permissions: permissions.permissions,
2572 this.cachePermissions();
2573 this.updatePermissions();
2575 /* eslint-enable mozilla/balanced-listeners */
2578 set state(startupState) {
2579 this.startupStates.clear();
2580 this.startupStates.add(startupState);
2584 return `${Array.from(this.startupStates).join(", ")}`;
2587 async addStartupStatePromise(name, fn) {
2588 this.startupStates.add(name);
2592 this.startupStates.delete(name);
2596 // Some helpful properties added elsewhere:
2598 static getBootstrapScope() {
2599 return new BootstrapScope();
2602 get browsingContextGroupId() {
2603 return this.policy.browsingContextGroupId;
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;
2612 if (!frameLoader && view.xulBrowser) {
2613 frameLoader = view.xulBrowser.frameLoader;
2616 return frameLoader || ExtensionParent.DebugUtils.getFrameLoader(this.id);
2619 get backgroundContext() {
2620 for (let view of this.views) {
2622 view.viewType === "background" ||
2623 view.viewType === "background_worker"
2632 return this.emitter.on(hook, f);
2636 return this.emitter.off(hook, f);
2640 return this.emitter.once(hook, f);
2643 emit(event, ...args) {
2644 if (PROXIED_EVENTS.has(event)) {
2645 Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {
2651 return this.emitter.emit(event, ...args);
2654 receiveMessage({ name, data }) {
2655 if (name === this.MESSAGE_EMIT_EVENT) {
2656 this.emitter.emit(data.event, ...data.args);
2660 testMessage(...args) {
2661 this.emit("test-harness-message", ...args);
2664 createPrincipal(uri = this.baseURI, originAttributes = {}) {
2665 return Services.scriptSecurityManager.createContentPrincipal(
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;
2679 checkLoadURI(uri, options = {}) {
2680 return ExtensionCommon.checkLoadURI(uri, this.principal, options);
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)) {
2691 return ExtensionCommon.checkLoadURL(url, this.principal, options);
2694 async promiseLocales(locale) {
2695 let locales = await StartupCache.locales.get(
2696 [this.id, "@@all_locales"],
2697 () => this._promiseLocaleMap()
2700 return this._setupLocaleData(locales);
2703 readLocaleFile(locale) {
2704 return StartupCache.locales
2705 .get([this.id, this.version, locale], () => super.readLocaleFile(locale))
2707 this.localeData.messages.set(locale, result);
2711 get manifestCacheKey() {
2712 return [this.id, this.version, Services.locale.appLocaleAsBCP47];
2715 get temporarilyInstalled() {
2716 return !!this.addonData.temporarilyInstalled;
2720 if (this.dontSaveStartupData) {
2723 lazy.AddonManagerPrivate.setAddonStartupData(this.id, this.startupData);
2726 async parseManifest() {
2727 await this.startupClearCachePromise;
2728 return StartupCache.manifests.get(this.manifestCacheKey, () =>
2729 super.parseManifest()
2733 async cachePermissions() {
2734 let manifestData = await this.parseManifest();
2736 manifestData.originPermissions = this.allowedOrigins.patterns.map(
2739 manifestData.permissions = this.permissions;
2740 return StartupCache.manifests.set(this.manifestCacheKey, manifestData);
2743 async loadManifest() {
2744 let manifest = await super.loadManifest();
2746 this.ensureNoErrors();
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.
2756 content_security_policy &&
2757 typeof content_security_policy === "object"
2759 return content_security_policy.extension_pages;
2761 return content_security_policy;
2764 get backgroundScripts() {
2765 return this.manifest.background?.scripts;
2768 get backgroundTypeModule() {
2769 return this.manifest.background?.type === "module";
2772 get backgroundWorkerScript() {
2773 return this.manifest.background?.service_worker;
2776 get optionalPermissions() {
2777 return this.manifest.optional_permissions;
2780 get privateBrowsingAllowed() {
2781 return this.policy.privateBrowsingAllowed;
2784 canAccessWindow(window) {
2785 return this.policy.canAccessWindow(window);
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
2794 let extensionRestrictedContainers = JSON.parse(
2795 Services.prefs.getStringPref(
2796 `extensions.userContextIsolation.${this.id}.restricted`,
2801 extensionRestrictedContainers.includes(userContextId) ||
2802 defaultRestrictedContainers.includes(userContextId)
2810 // Representation of the extension to send to content
2811 // processes. This should include anything the content process might
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,
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() {
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,
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));
2857 function listener(data) {
2858 children.delete(data.target);
2861 function observer(subject, topic, data) {
2862 children.delete(subject);
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");
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);
2882 setSharedData(key, value) {
2883 key = `extension/${this.id}/${key}`;
2884 this.sharedDataKeys.add(key);
2886 sharedData.set(key, value);
2889 getSharedData(key, value) {
2890 key = `extension/${this.id}/${key}`;
2891 return sharedData.get(key);
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();
2902 updateContentScripts() {
2903 this.setSharedData("contentScripts", this.registeredContentScripts);
2906 runManifest(manifest) {
2908 let addPromise = (name, fn) => {
2909 promises.push(this.addStartupStatePromise(name, fn));
2912 for (let directive in manifest) {
2913 if (manifest[directive] !== null) {
2914 addPromise(`asyncEmitManifestEntry("${directive}")`, () =>
2915 Management.asyncEmitManifestEntry(this, directive)
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);
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.
2937 * @param {object} obj
2938 * An object on which to call the close() method when this
2939 * extension is shut down.
2942 this.onShutdown.add(obj);
2945 forgetOnClose(obj) {
2946 this.onShutdown.delete(obj);
2949 get builtinMessages() {
2950 return new Map([["@@extension_id", this.uuid]]);
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()),
2966 locale = matches[0];
2969 return super.initLocale(locale);
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.
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).
2983 * @returns {Promise<void>}
2984 * Promise resolved when the nsIClearDataService async method call
2985 * has been completed.
2987 async clearCache(reason) {
2989 case "ADDON_INSTALL":
2990 case "ADDON_UPGRADE":
2991 case "ADDON_DOWNGRADE":
2992 return clearCacheForExtensionPrincipal(this.principal);
2997 * Update site permissions as necessary.
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.
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(
3014 "WebExtensions-unlimitedStorage",
3015 Services.perms.ALLOW_ACTION
3017 Services.perms.addFromPrincipal(
3019 "persistent-storage",
3020 Services.perms.ALLOW_ACTION
3024 // Only update storage permissions when the extension changes in
3026 if (reason !== "APP_STARTUP" && reason !== "APP_SHUTDOWN") {
3027 if (this.hasPermission("unlimitedStorage")) {
3028 addUnlimitedStoragePermissions();
3030 // Remove the indexedDB permission if it has been enabled using the
3031 // unlimitedStorage WebExtensions permissions.
3032 Services.perms.removeFromPrincipal(
3034 "WebExtensions-unlimitedStorage"
3036 Services.perms.removeFromPrincipal(principal, "persistent-storage");
3039 reason === "APP_STARTUP" &&
3040 this.hasPermission("unlimitedStorage") &&
3041 testPermission("persistent-storage") !== Services.perms.ALLOW_ACTION
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();
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(
3057 Services.perms.ALLOW_ACTION,
3058 Services.perms.EXPIRE_SESSION
3062 reason !== "APP_STARTUP" &&
3063 testPermission("geo") === Services.perms.ALLOW_ACTION
3065 Services.perms.removeFromPrincipal(principal, "geo");
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;
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({
3084 mozExtensionHostname: this.uuid,
3085 baseURL: this.resourceURL,
3086 isPrivileged: this.isPrivileged,
3087 temporarilyInstalled: this.temporarilyInstalled,
3088 allowedOrigins: new MatchPatternSet([]),
3089 localizeCallback() {},
3093 this.policy.extension = this;
3094 if (!WebExtensionPolicy.getByID(this.id)) {
3095 this.policy.active = true;
3098 pendingExtensions.set(this.id, {
3099 mozExtensionHostname: this.uuid,
3100 baseURL: this.resourceURL,
3101 isPrivileged: this.isPrivileged,
3103 sharedData.set("extensions/pending", pendingExtensions);
3105 lazy.ExtensionTelemetry.extensionStartup.stopwatchStart(this);
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";
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.
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
3137 await lazy.ExtensionScriptingStore.initExtension(this);
3138 this.state = "Startup: Scripting store initialized";
3140 this.logError(`Failed to initialize scripting store: ${err}`);
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);
3160 "storageIDBPrincipal",
3161 lazy.ExtensionStorageIDB.getStoragePrincipal(this)
3164 this.startupReason === "ADDON_INSTALL" &&
3165 !Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)
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);
3172 "storageIDBPrincipal",
3173 lazy.ExtensionStorageIDB.getStoragePrincipal(this)
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.
3182 this.hasPermission("declarativeNetRequest") ||
3183 this.hasPermission("declarativeNetRequestWithHostAccess")
3185 lazy.ExtensionDNR.ensureInitialized(this);
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();
3198 this.addStartupStatePromise("Startup: Emit startup", () =>
3199 Management.emit("startup", this)
3201 this.addStartupStatePromise("Startup: Run manifest", () =>
3202 this.runManifest(this.manifest)
3205 this.state = "Startup: Ran manifest";
3207 Management.emit("ready", this);
3210 this.state = "Startup: Complete";
3212 this.state = `Startup: Error: ${e}`;
3217 this.policy.active = false;
3220 this.cleanupGeneratedFile();
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);
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
3240 lazy.ExtensionPermissions.remove(this.id, {
3241 permissions: [PRIVATE_ALLOWED_PERMISSION],
3244 this.permissions.delete(PRIVATE_ALLOWED_PERMISSION);
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],
3253 this.permissions.add(PRIVATE_ALLOWED_PERMISSION);
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);
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],
3272 this.permissions.add(SVG_CONTEXT_PROPERTIES_PERMISSION);
3274 lazy.ExtensionPermissions.remove(this.id, {
3275 permissions: [SVG_CONTEXT_PROPERTIES_PERMISSION],
3278 this.permissions.delete(SVG_CONTEXT_PROPERTIES_PERMISSION);
3282 // Ensure devtools permission is set
3284 this.manifest.devtools_page &&
3285 !this.manifest.optional_permissions.includes("devtools")
3287 lazy.ExtensionPermissions.add(this.id, {
3288 permissions: ["devtools"],
3291 this.permissions.add("devtools");
3295 cleanupGeneratedFile() {
3296 if (!this.cleanupFile) {
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 })
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.
3313 .catch(Cu.reportError);
3316 async shutdown(reason) {
3317 this.state = "Shutdown";
3319 this.hasShutdown = true;
3326 this.hasPermission("storage") &&
3327 lazy.ExtensionStorageIDB.selectedBackendPromises.has(this)
3329 this.state = "Shutdown: Storage";
3331 // Wait the data migration to complete.
3333 await lazy.ExtensionStorageIDB.selectedBackendPromises.get(this);
3336 `Error while waiting for extension data migration on shutdown: ${this.policy.debugName} - ${err.message}::${err.stack}`
3339 this.state = "Shutdown: Storage complete";
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", {
3348 this.state = "Shutdown: Flushed jar cache";
3351 const isAppShutdown = reason === "APP_SHUTDOWN";
3352 if (this.cleanupFile || !isAppShutdown) {
3353 StartupCache.clearAddonData(this.id);
3356 activeExtensionIDs.delete(this.id);
3357 sharedData.set("extensions/activeIDs", activeExtensionIDs);
3359 for (let key of this.sharedDataKeys) {
3360 sharedData.delete(key);
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";
3381 if (!this.manifest) {
3382 this.state = "Shutdown: Complete: No manifest";
3383 this.policy.active = false;
3385 return this.cleanupGeneratedFile();
3388 GlobalManager.uninit(this);
3390 for (let obj of this.onShutdown) {
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),
3406 this.state = `Shutdown: Emitted shutdown: ${result === TIMED_OUT}`;
3407 if (result === TIMED_OUT) {
3409 `Timeout while waiting for extension child to shutdown: ${this.policy.debugName}`
3413 this.policy.active = false;
3415 this.state = `Shutdown: Complete (${this.cleanupFile})`;
3416 return this.cleanupGeneratedFile();
3419 observe(subject, topic, data) {
3420 if (topic === "xpcom-shutdown") {
3421 this.cleanupGeneratedFile();
3426 return this.manifest.name;
3429 get optionalOrigins() {
3430 if (this._optionalOrigins == null) {
3431 let { origins } = this.manifestOptionalPermissions;
3432 this._optionalOrigins = new MatchPatternSet(origins, {
3433 restrictSchemes: this.restrictSchemes,
3437 return this._optionalOrigins;
3440 get hasBrowserActionUI() {
3441 return this.manifest.browser_action || this.manifest.action;
3445 class Dictionary extends ExtensionData {
3446 constructor(addonData, startupReason) {
3447 super(addonData.resourceURI);
3448 this.id = addonData.id;
3449 this.startupData = addonData.startupData;
3452 static getBootstrapScope() {
3453 return new DictionaryBootstrapScope();
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",
3464 this.dictionaries[lang] = uri;
3466 lazy.spellCheck.addDictionary(lang, uri);
3469 Management.emit("ready", this);
3472 async shutdown(reason) {
3473 if (reason !== "APP_SHUTDOWN") {
3474 lazy.AddonManagerPrivate.unregisterDictionaries(this.dictionaries);
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];
3486 static getBootstrapScope() {
3487 return new LangpackBootstrapScope();
3490 async promiseLocales(locale) {
3491 let locales = await StartupCache.locales.get(
3492 [this.id, "@@all_locales"],
3493 () => this._promiseLocaleMap()
3496 return this._setupLocaleData(locales);
3500 return StartupCache.manifests.get(this.manifestCacheKey, () =>
3501 super.parseManifest()
3505 async startup(reason) {
3506 this.chromeRegistryHandle = null;
3507 if (this.startupData.chromeEntries.length) {
3508 const manifestURI = Services.io.newURI(
3513 this.chromeRegistryHandle = lazy.aomStartup.registerChrome(
3515 this.startupData.chromeEntries
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}`,
3529 this.startupData.languages,
3530 `resource://${langpackId}/${basePath}localization/{locale}/`
3534 L10nRegistry.getInstance().registerSources(fileSources);
3536 Services.obs.notifyObservers(
3537 { wrappedJSObject: { langpack: this } },
3538 "webextension-langpack-startup"
3542 async shutdown(reason) {
3543 if (reason === "APP_SHUTDOWN") {
3544 // If we're shutting down, let's not bother updating the state of each
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;
3559 lazy.resourceProtocol.setSubstitution(this.startupData.langpackId, null);
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;
3571 async loadManifest() {
3572 let [manifestData] = await Promise.all([this.parseManifest()]);
3574 if (!manifestData) {
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;
3587 static getBootstrapScope() {
3588 return new SitePermissionBootstrapScope();
3591 // Array of principals that may be set by the addon.
3592 getSupportedPrincipals() {
3593 if (!this.siteOrigin) {
3596 const uri = Services.io.newURI(this.siteOrigin);
3598 Services.scriptSecurityManager.createContentPrincipal(uri, {}),
3599 Services.scriptSecurityManager.createContentPrincipal(uri, {
3600 privateBrowsingId: 1,
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.
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) {
3627 site_permissions.includes(perm) &&
3628 !this.sitePermissions.includes(perm)
3630 Services.perms.removeFromPrincipal(principal, perm);
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(
3643 if (permission == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
3644 let { privateBrowsingId } = principal.originAttributes;
3645 let allow = privateBrowsingId == 0 || privateAllowed;
3646 Services.perms.addFromPrincipal(
3649 allow ? Services.perms.ALLOW_ACTION : Services.perms.DENY_ACTION,
3650 Services.perms.EXPIRE_NEVER
3656 Services.obs.notifyObservers(
3657 { wrappedJSObject: { sitepermissions: this } },
3658 "webextension-sitepermissions-startup"
3662 async shutdown(reason) {
3663 this.hasShutdown = true;
3664 // Permissions are retained across restarts
3665 if (reason == "APP_SHUTDOWN") {
3668 let principals = this.getSupportedPrincipals();
3670 for (let perm of this.sitePermissions || []) {
3671 for (let principal of principals) {
3672 Services.perms.removeFromPrincipal(principal, perm);