Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* This file is the main entry point for extensions. When an extension
* loads, its bootstrap.js file creates a Extension instance
* and calls .startup() on it. It calls .shutdown() when the extension
* unloads. Extension manages any extension-specific state in
* the chrome process.
*
* TODO(rpl): we are current restricting the extensions to a single process
* (set as the current default value of the "dom.ipc.processCount.extension"
* preference), if we switch to use more than one extension process, we have to
* be sure that all the browser's frameLoader are associated to the same process,
* e.g. by enabling the `maychangeremoteness` attribute, and/or setting
* `initialBrowsingContextGroupId` attribute to the correct value.
*
* At that point we are going to keep track of the existing browsers associated to
* a webextension to ensure that they are all running in the same process (and we
* are also going to do the same with the browser element provided to the
* addon debugging Remote Debugging actor, e.g. because the addon has been
* reloaded by the user, we have to ensure that the new extension pages are going
* to run in the same process of the existing addon debugging browser element).
*/
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs";
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
import { Log } from "resource://gre/modules/Log.sys.mjs";
/** @type {Lazy} */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs",
ExtensionMenus: "resource://gre/modules/ExtensionMenus.sys.mjs",
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
ExtensionPreferencesManager:
"resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
ExtensionProcessScript:
"resource://gre/modules/ExtensionProcessScript.sys.mjs",
ExtensionScriptingStore:
"resource://gre/modules/ExtensionScriptingStore.sys.mjs",
ExtensionStorage: "resource://gre/modules/ExtensionStorage.sys.mjs",
ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.sys.mjs",
ExtensionUserScripts: "resource://gre/modules/ExtensionUserScripts.sys.mjs",
ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
LightweightThemeManager:
"resource://gre/modules/LightweightThemeManager.sys.mjs",
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
SITEPERMS_ADDON_TYPE:
"resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
Schemas: "resource://gre/modules/Schemas.sys.mjs",
ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.sys.mjs",
PERMISSION_L10N: "resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
permissionToL10nId:
"resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
QuarantinedDomains: "resource://gre/modules/ExtensionPermissions.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "resourceProtocol", () =>
Services.io
.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler)
);
XPCOMUtils.defineLazyServiceGetters(lazy, {
aomStartup: [
"@mozilla.org/addons/addon-manager-startup;1",
"amIAddonManagerStartup",
],
spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"processCount",
"dom.ipc.processCount.extension"
);
// Temporary pref to be turned on when ready.
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"userContextIsolation",
"extensions.userContextIsolation.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"userContextIsolationDefaultRestricted",
"extensions.userContextIsolation.defaults.restricted",
"[]"
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"dnrEnabled",
"extensions.dnr.enabled",
true
);
// All functionality is gated by the "userScripts" permission, and forgetting
// about its existence is enough to hide all userScripts functionality.
// MV3 userScripts API in development (bug 1875475), off by default.
// Not to be confused with MV2 and extensions.webextensions.userScripts.enabled!
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"userScriptsMV3Enabled",
"extensions.userScripts.mv3.enabled",
false
);
// This pref modifies behavior for MV2. MV3 is enabled regardless.
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"eventPagesEnabled",
"extensions.eventPages.enabled"
);
// This pref is used to check if storage.sync is still the Kinto-based backend
// (GeckoView should be the only one still using it).
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"storageSyncOldKintoBackend",
"webextensions.storage.sync.kinto",
false
);
// Deprecation of browser_style, through .supported & .same_as_mv2 prefs:
// - true true = warn only: deprecation message only (no behavioral changes).
// - true false = deprecate: default to false, even if default was true in MV2.
// - false = remove: always use false, even when true is specified.
// (if .same_as_mv2 is set, also warn if the default changed)
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"browserStyleMV3supported",
"extensions.browser_style_mv3.supported",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"browserStyleMV3sameAsMV2",
"extensions.browser_style_mv3.same_as_mv2",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"processCrashThreshold",
"extensions.webextensions.crash.threshold",
// The default number of times an extension process is allowed to crash
// within a timeframe.
5
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"processCrashTimeframe",
"extensions.webextensions.crash.timeframe",
// The default timeframe used to count crashes, in milliseconds.
30 * 1000
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"installIncludesOrigins",
"extensions.originControls.grantByDefault",
false
);
var {
GlobalManager,
IconDetails,
ParentAPIManager,
StartupCache,
apiManager: Management,
} = ExtensionParent;
export { Management };
const { getUniqueId, promiseTimeout } = ExtensionUtils;
const { EventEmitter, redefineGetter, updateAllowedOrigins } = ExtensionCommon;
ChromeUtils.defineLazyGetter(
lazy,
"LocaleData",
() => ExtensionCommon.LocaleData
);
ChromeUtils.defineLazyGetter(lazy, "NO_PROMPT_PERMISSIONS", async () => {
// Wait until all extension API schemas have been loaded and parsed.
await Management.lazyInit();
return new Set(
lazy.Schemas.getPermissionNames([
"PermissionNoPrompt",
"OptionalPermissionNoPrompt",
"PermissionPrivileged",
])
);
});
const { sharedData } = Services.ppmm;
const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed";
const SVG_CONTEXT_PROPERTIES_PERMISSION =
"internal:svgContextPropertiesAllowed";
// The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
// storage used by the browser.storage.local API is not directly accessible from the extension code,
// it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.sys.mjs).
const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;
// The maximum time to wait for extension child shutdown blockers to complete.
const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
// Permissions that are only available to privileged extensions.
const PRIVILEGED_PERMS = new Set([
"activityLog",
"mozillaAddons",
"networkStatus",
"normandyAddonStudy",
"telemetry",
]);
const PRIVILEGED_PERMS_ANDROID_ONLY = new Set([
"geckoViewAddons",
"nativeMessagingFromContent",
"nativeMessaging",
]);
const PRIVILEGED_PERMS_DESKTOP_ONLY = new Set(["normandyAddonStudy"]);
if (AppConstants.platform == "android") {
for (const perm of PRIVILEGED_PERMS_ANDROID_ONLY) {
PRIVILEGED_PERMS.add(perm);
}
}
if (
AppConstants.MOZ_APP_NAME != "firefox" ||
AppConstants.platform == "android"
) {
for (const perm of PRIVILEGED_PERMS_DESKTOP_ONLY) {
PRIVILEGED_PERMS.delete(perm);
}
}
// Permissions that are not available in manifest version 2.
const PERMS_NOT_IN_MV2 = new Set([
// MV2 had a userScripts API, tied to "user_scripts" manifest key. In MV3 the
// userScripts API availability is gated by the "userScripts" permission.
"userScripts",
]);
// Message included in warnings and errors related to privileged permissions and
// privileged manifest properties. Provides a link to the firefox-source-docs.mozilla.org
// section related to developing and sign Privileged Add-ons.
const PRIVILEGED_ADDONS_DEVDOCS_MESSAGE =
"See https://mzl.la/3NS9KJd for more details about how to develop a privileged add-on.";
const INSTALL_AND_UPDATE_STARTUP_REASONS = new Set([
"ADDON_INSTALL",
"ADDON_UPGRADE",
"ADDON_DOWNGRADE",
]);
const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
const PERMISSION_KEY_DELIMITER = "^";
// These are used for manipulating jar entry paths, which always use Unix
// separators (originally copied from `ospath_unix.jsm` as part of the "OS.Path
// to PathUtils" migration).
/**
* Return the final part of the path.
* The final part of the path is everything after the last "/".
*/
function basename(path) {
return path.slice(path.lastIndexOf("/") + 1);
}
/**
* Return the directory part of the path.
* The directory part of the path is everything before the last
* "/". If the last few characters of this part are also "/",
* they are ignored.
*
* If the path contains no directory, return ".".
*/
function dirname(path) {
let index = path.lastIndexOf("/");
if (index == -1) {
return ".";
}
while (index >= 0 && path[index] == "/") {
--index;
}
return path.slice(0, index + 1);
}
// Returns true if the extension is owned by Mozilla (is either privileged,
// using one of the @mozilla.com/@mozilla.org protected addon id suffixes).
//
// This method throws if the extension's startupReason is not one of the
// expected ones (either ADDON_INSTALL, ADDON_UPGRADE or ADDON_DOWNGRADE).
//
// TODO(Bug 1835787): Consider to remove the restriction based on the
// startupReason now that the recommendationState property is always
// included in the addonData with any of the startupReason.
function isMozillaExtension(extension) {
const { addonData, id, isPrivileged, startupReason } = extension;
if (!INSTALL_AND_UPDATE_STARTUP_REASONS.has(startupReason)) {
throw new Error(
`isMozillaExtension called with unexpected startupReason: ${startupReason}`
);
}
if (isPrivileged) {
return true;
}
if (id.endsWith("@mozilla.com") || id.endsWith("@mozilla.org")) {
return true;
}
// This check is a subset of what is being checked in AddonWrapper's
// recommendationStates (states expire dates for line extensions are
// not considered important in determining that the extension is
// provided by mozilla, and so they are omitted here on purpose).
const isMozillaLineExtension =
addonData.recommendationState?.states?.includes("line");
const isSigned =
addonData.signedState > lazy.AddonManager.SIGNEDSTATE_MISSING;
return isSigned && isMozillaLineExtension;
}
/**
* Classify an individual permission from a webextension manifest
* as a host/origin permission, an api permission, or a regular permission.
*
* @param {string} perm The permission string to classify
* @param {boolean} restrictSchemes
* @param {boolean} isPrivileged whether or not the webextension is privileged
*
* @returns {object}
* An object with exactly one of the following properties:
* "origin" to indicate this is a host/origin permission.
* "api" to indicate this is an api permission
* (as used for webextensions experiments).
* "permission" to indicate this is a regular permission.
* "invalid" to indicate that the given permission cannot be used.
*/
function classifyPermission(perm, restrictSchemes, isPrivileged) {
let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
if (!match) {
try {
let { pattern } = new MatchPattern(perm, {
restrictSchemes,
ignorePath: true,
});
return { origin: pattern };
} catch (e) {
return { invalid: perm };
}
} else if (match[1] == "experiments" && match[2]) {
return { api: match[2] };
} else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) {
return { invalid: perm, privileged: true };
} else if (perm.startsWith("declarativeNetRequest") && !lazy.dnrEnabled) {
return { invalid: perm };
} else if (perm === "userScripts" && !lazy.userScriptsMV3Enabled) {
return { invalid: perm };
}
return { permission: perm };
}
const LOGGER_ID_BASE = "addons.webextension.";
const UUID_MAP_PREF = "extensions.webextensions.uuids";
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
const COMMENT_REGEXP = new RegExp(
String.raw`
^
(
(?:
[^"\n] |
" (?:[^"\\\n] | \\.)* "
)*?
)
//.*
`.replace(/\s+/g, ""),
"gm"
);
// All moz-extension URIs use a machine-specific UUID rather than the
// extension's own ID in the host component. This makes it more
// difficult for web pages to detect whether a user has a given add-on
// installed (by trying to load a moz-extension URI referring to a
// web_accessible_resource from the extension). UUIDMap.get()
// returns the UUID for a given add-on ID.
var UUIDMap = {
_read() {
let pref = Services.prefs.getStringPref(UUID_MAP_PREF, "{}");
try {
return JSON.parse(pref);
} catch (e) {
Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
return {};
}
},
_write(map) {
Services.prefs.setStringPref(UUID_MAP_PREF, JSON.stringify(map));
},
get(id, create = true) {
let map = this._read();
if (id in map) {
return map[id];
}
let uuid = null;
if (create) {
uuid = Services.uuid.generateUUID().number;
uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
map[id] = uuid;
this._write(map);
}
return uuid;
},
remove(id) {
let map = this._read();
delete map[id];
this._write(map);
},
};
function clearCacheForExtensionPrincipal(principal, clearAll = false) {
if (!principal.schemeIs("moz-extension")) {
return Promise.reject(new Error("Unexpected non extension principal"));
}
// TODO(Bug 1750053): replace the two specific flags with a "clear all caches one"
// (along with covering the other kind of cached data with tests).
const clearDataFlags = clearAll
? Ci.nsIClearDataService.CLEAR_ALL_CACHES
: Ci.nsIClearDataService.CLEAR_IMAGE_CACHE |
Ci.nsIClearDataService.CLEAR_CSS_CACHE |
Ci.nsIClearDataService.CLEAR_JS_CACHE;
return new Promise(resolve =>
Services.clearData.deleteDataFromPrincipal(
principal,
false,
clearDataFlags,
() => resolve()
)
);
}
/**
* Observer AddonManager events and translate them into extension events,
* as well as handle any last cleanup after uninstalling an extension.
*/
var ExtensionAddonObserver = {
initialized: false,
init() {
if (!this.initialized) {
lazy.AddonManager.addAddonListener(this);
this.initialized = true;
}
},
// AddonTestUtils will call this as necessary.
uninit() {
if (this.initialized) {
lazy.AddonManager.removeAddonListener(this);
this.initialized = false;
}
},
onEnabling(addon) {
if (addon.type !== "extension") {
return;
}
Management._callHandlers([addon.id], "enabling", "onEnabling");
},
onDisabled(addon) {
if (addon.type !== "extension") {
return;
}
if (Services.appinfo.inSafeMode) {
// Ensure ExtensionPreferencesManager updates its data and
// modules can run any disable logic they need to. We only
// handle safeMode here because there is a bunch of additional
// logic that happens in Extension.shutdown when running in
// normal mode.
Management._callHandlers([addon.id], "disable", "onDisable");
}
},
onUninstalling(addon) {
let extension = GlobalManager.extensionMap.get(addon.id);
if (extension) {
// Let any other interested listeners respond
// (e.g., display the uninstall URL)
Management.emit("uninstalling", extension);
}
},
onUninstalled(addon) {
this.clearOnUninstall(addon.id);
},
/**
* Clears persistent state from the add-on post install.
*
* @param {string} addonId The ID of the addon that has been uninstalled.
*/
clearOnUninstall(addonId) {
const tasks = [];
function addShutdownBlocker(name, promise) {
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(name, promise);
tasks.push({ name, promise });
}
function notifyUninstallTaskObservers() {
Management.emit("cleanupAfterUninstall", addonId, tasks);
}
// Cleanup anything that is used by non-extension addon types
// since only extensions have uuid's.
addShutdownBlocker(
`Clear ExtensionPermissions for ${addonId}`,
lazy.ExtensionPermissions.removeAll(addonId)
);
lazy.QuarantinedDomains.clearUserPref(addonId);
let uuid = UUIDMap.get(addonId, false);
if (!uuid) {
notifyUninstallTaskObservers();
return;
}
let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
let principal = Services.scriptSecurityManager.createContentPrincipal(
baseURI,
{}
);
// Clear all cached resources (e.g. CSS and images);
addShutdownBlocker(
`Clear cache for ${addonId}`,
clearCacheForExtensionPrincipal(principal, /* clearAll */ true)
);
// Clear all the registered service workers for the extension
// principal (the one that may have been registered through the
// manifest.json file and the ones that may have been registered
// from an extension page through the service worker API).
//
// Any stored data would be cleared below (if the pref
// "extensions.webextensions.keepStorageOnUninstall has not been
// explicitly set to true, which is usually only done in
// tests and by some extensions developers for testing purpose).
//
// TODO: ServiceWorkerCleanUp may go away once Bug 1183245
// is fixed, and so this may actually go away, replaced by
// marking the registration as disabled or to be removed on
// shutdown (where we do know if the extension is shutting
// down because is being uninstalled) and then cleared from
// the persisted serviceworker registration on the next
// startup.
addShutdownBlocker(
`Clear ServiceWorkers for ${addonId}`,
lazy.ServiceWorkerCleanUp.removeFromPrincipal(principal)
);
// Clear the persisted menus created with the menus/contextMenus API (if any).
addShutdownBlocker(
`Clear menus store for ${addonId}`,
lazy.ExtensionMenus.clearPersistedMenusOnUninstall(addonId)
);
// Clear the persisted dynamic content scripts created with the scripting
// API (if any).
addShutdownBlocker(
`Clear scripting store for ${addonId}`,
lazy.ExtensionScriptingStore.clearOnUninstall(addonId)
);
// Clear MV3 userScripts API data, if any.
addShutdownBlocker(
`Clear user scripts for ${addonId}`,
lazy.ExtensionUserScripts.clearOnUninstall(addonId)
);
// Clear the DNR API's rules data persisted on disk (if any).
addShutdownBlocker(
`Clear declarativeNetRequest store for ${addonId}`,
lazy.ExtensionDNRStore.clearOnUninstall(uuid)
);
if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) {
// Clear browser.storage.local backends.
addShutdownBlocker(
`Clear Extension Storage ${addonId} (File Backend)`,
lazy.ExtensionStorage.clear(addonId, { shouldNotifyListeners: false })
);
// Clear browser.storage.sync rust-based backend.
// (storage.sync clearOnUninstall will resolve and log an error on the
// browser console in case of unexpected failures).
if (!lazy.storageSyncOldKintoBackend) {
addShutdownBlocker(
`Clear Extension StorageSync ${addonId}`,
lazy.extensionStorageSync.clearOnUninstall(addonId)
);
}
// Clear any IndexedDB and Cache API storage created by the extension.
// If LSNG is enabled, this also clears localStorage.
Services.qms.clearStoragesForPrincipal(principal);
// Clear any storage.local data stored in the IDBBackend.
let storagePrincipal =
Services.scriptSecurityManager.createContentPrincipal(baseURI, {
userContextId: WEBEXT_STORAGE_USER_CONTEXT_ID,
});
Services.qms.clearStoragesForPrincipal(storagePrincipal);
lazy.ExtensionStorageIDB.clearMigratedExtensionPref(addonId);
// If LSNG is not enabled, we need to clear localStorage explicitly using
// the old API.
if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
// Clear localStorage created by the extension
let storage = Services.domStorageManager.getStorage(
null,
principal,
principal
);
if (storage) {
storage.clear();
}
}
// Remove any permissions related to the unlimitedStorage permission
// if we are also removing all the data stored by the extension.
Services.perms.removeFromPrincipal(
principal,
"WebExtensions-unlimitedStorage"
);
Services.perms.removeFromPrincipal(principal, "persistent-storage");
}
// Clear any protocol handler permissions granted to this add-on.
let permissions = Services.perms.getAllWithTypePrefix(
PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER
);
for (let perm of permissions) {
if (perm.principal.equalsURI(baseURI)) {
Services.perms.removePermission(perm);
}
}
if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) {
// Clear the entry in the UUID map
UUIDMap.remove(addonId);
}
notifyUninstallTaskObservers();
},
onPropertyChanged(addon, properties) {
let extension = GlobalManager.extensionMap.get(addon.id);
if (!extension) {
return;
}
if (properties.includes("quarantineIgnoredByUser")) {
extension.ignoreQuarantine = addon.quarantineIgnoredByUser;
extension.policy.ignoreQuarantine = addon.quarantineIgnoredByUser;
extension.setSharedData("", extension.serialize());
Services.ppmm.sharedData.flush();
extension.emit("update-ignore-quarantine");
extension.broadcast("Extension:UpdateIgnoreQuarantine", {
id: extension.id,
ignoreQuarantine: addon.quarantineIgnoredByUser,
});
}
if (properties.includes("blocklistState")) {
extension.blocklistState = addon.blocklistState;
extension.emit("update-blocklist-state");
}
},
};
ExtensionAddonObserver.init();
/**
* Observer ExtensionProcess crashes and notify all the extensions
* using a Management event named "extension-process-crash".
*/
export var ExtensionProcessCrashObserver = {
initialized: false,
// For Android apps we initially consider the app as always starting
// in the background, then we expect to be setting it to foreground
// when GeckoView LifecycleListener onResume method is called on the
// Android app first startup. After the application has got on the
// foreground for the first time then onPause/onResumed LifecycleListener
// are called, the application-foreground/-background topics will be
// notified to Gecko and this flag will be updated accordingly.
_appInForeground: AppConstants.platform !== "android",
_isAndroid: AppConstants.platform === "android",
_processSpawningDisabled: false,
// Technically there is at most one child extension process,
// but we may need to adjust this assumption to account for more
// than one if that ever changes in the future.
currentProcessChildID: undefined,
lastCrashedProcessChildID: undefined,
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
// Collect the timestamps of the crashes happened over the last
// `processCrashTimeframe` milliseconds.
lastCrashTimestamps: [],
logger: Log.repository.getLogger("addons.process-crash-observer"),
init() {
if (!this.initialized) {
Services.obs.addObserver(this, "ipc:content-created");
Services.obs.addObserver(this, "process-type-set");
Services.obs.addObserver(this, "ipc:content-shutdown");
if (this._isAndroid) {
Services.obs.addObserver(this, "geckoview-initial-foreground");
Services.obs.addObserver(this, "application-foreground");
Services.obs.addObserver(this, "application-background");
}
this.initialized = true;
}
},
uninit() {
if (this.initialized) {
try {
Services.obs.removeObserver(this, "ipc:content-created");
Services.obs.removeObserver(this, "process-type-set");
Services.obs.removeObserver(this, "ipc:content-shutdown");
if (this._isAndroid) {
Services.obs.removeObserver(this, "geckoview-initial-foreground");
Services.obs.removeObserver(this, "application-foreground");
Services.obs.removeObserver(this, "application-background");
}
} catch (err) {
// Removing the observer may fail if they are not registered anymore,
// this shouldn't happen in practice, but let's still log the error
// in case it does.
Cu.reportError(err);
}
this.initialized = false;
}
},
observe(subject, topic, data) {
let childID = data;
switch (topic) {
case "geckoview-initial-foreground":
this._appInForeground = true;
this.logger.debug(
`Detected Android application moved in the foreground (geckoview-initial-foreground)`
);
break;
case "application-foreground":
// Intentional fall-through
case "application-background":
this._appInForeground = topic === "application-foreground";
this.logger.debug(
`Detected Android application moved in the ${
this._appInForeground ? "foreground" : "background"
}`
);
if (this._appInForeground) {
Management.emit("application-foreground", {
appInForeground: this._appInForeground,
childID: this.currentProcessChildID,
processSpawningDisabled: this.processSpawningDisabled,
});
}
break;
case "process-type-set":
// Intentional fall-through
case "ipc:content-created": {
let pp = subject.QueryInterface(Ci.nsIDOMProcessParent);
if (pp.remoteType === "extension") {
this.currentProcessChildID = childID;
Glean.extensions.processEvent[
this.appInForeground ? "created_fg" : "created_bg"
].add(1);
}
break;
}
case "ipc:content-shutdown": {
if (Services.startup.shuttingDown) {
// The application is shutting down, don't bother
// signaling process crashes anymore.
return;
}
if (this.currentProcessChildID !== childID) {
// Ignore non-extension child process shutdowns.
return;
}
// At this point we are sure that the current extension
// process is gone, and so even if the process did shutdown
// cleanly instead of crashing, we can clear the property
// that keeps track of the current extension process childID.
this.currentProcessChildID = undefined;
subject.QueryInterface(Ci.nsIPropertyBag2);
if (!subject.get("abnormal")) {
// Ignore non-abnormal child process shutdowns.
return;
}
this.lastCrashedProcessChildID = childID;
const now = Cu.now();
// Filter crash timestamps older than processCrashTimeframe.
this.lastCrashTimestamps = this.lastCrashTimestamps.filter(
timestamp => now - timestamp < lazy.processCrashTimeframe
);
// Push the new timeframe.
this.lastCrashTimestamps.push(now);
// Set the flag that disable process spawning when we exceed the
// `processCrashThreshold`.
this._processSpawningDisabled =
this.lastCrashTimestamps.length > lazy.processCrashThreshold;
this.logger.debug(
`Extension process crashed ${this.lastCrashTimestamps.length} times over the last ${lazy.processCrashTimeframe}ms`
);
const { appInForeground } = this;
if (this.processSpawningDisabled) {
if (appInForeground) {
Glean.extensions.processEvent.crashed_over_threshold_fg.add(1);
} else {
Glean.extensions.processEvent.crashed_over_threshold_bg.add(1);
}
this.logger.warn(
`Extension process respawning disabled because it crashed too often in the last ${lazy.processCrashTimeframe}ms (${this.lastCrashTimestamps.length} > ${lazy.processCrashThreshold}).`
);
}
Glean.extensions.processEvent[
appInForeground ? "crashed_fg" : "crashed_bg"
].add(1);
Management.emit("extension-process-crash", {
childID,
processSpawningDisabled: this.processSpawningDisabled,
appInForeground,
});
break;
}
}
},
enableProcessSpawning() {
const crashCounter = this.lastCrashTimestamps.length;
this.lastCrashTimestamps = [];
this.logger.debug(`reset crash counter (was ${crashCounter})`);
this._processSpawningDisabled = false;
Management.emit("extension-enable-process-spawning");
},
get appInForeground() {
// Only account for application in the background for
// android builds.
return this._isAndroid ? this._appInForeground : true;
},
get processSpawningDisabled() {
return this._processSpawningDisabled;
},
};
ExtensionProcessCrashObserver.init();
const manifestTypes = new Map([
["theme", "manifest.ThemeManifest"],
["locale", "manifest.WebExtensionLangpackManifest"],
["dictionary", "manifest.WebExtensionDictionaryManifest"],
["extension", "manifest.WebExtensionManifest"],
]);
/**
* Represents the data contained in an extension, contained either
* in a directory or a zip file, which may or may not be installed.
* This class implements the functionality of the Extension class,
* primarily related to manifest parsing and localization, which is
* useful prior to extension installation or initialization.
*
* No functionality of this class is guaranteed to work before
* `loadManifest` has been called, and completed.
*/
export class ExtensionData {
/**
* Note: These fields are only available and meant to be used on Extension
* instances, declared here because methods from this class reference them.
*/
/** @type {object} TODO: move to the Extension class, bug 1871094. */
addonData;
/** @type {nsIURI} */
baseURI;
/** @type {nsIPrincipal} */
principal;
/** @type {boolean} */
temporarilyInstalled;
constructor(rootURI, isPrivileged = false) {
this.rootURI = rootURI;
this.resourceURL = rootURI.spec;
this.isPrivileged = isPrivileged;
this.manifest = null;
this.type = null;
this.id = null;
this.uuid = null;
this.localeData = null;
this.fluentL10n = null;
this._promiseLocales = null;
this.apiNames = new Set();
this.dependencies = new Set();
this.permissions = new Set();
this.startupData = null;
this.errors = [];
this.warnings = [];
this.eventPagesEnabled = lazy.eventPagesEnabled;
}
/**
* A factory function that allows the construction of ExtensionData, with
* the isPrivileged flag computed asynchronously.
*
* @param {object} options
* @param {nsIURI} options.rootURI
* The URI pointing to the extension root.
* @param {function(type, id): boolean} options.checkPrivileged
* An (async) function that takes the addon type and addon ID and returns
* whether the given add-on is privileged.
* @param {boolean} options.temporarilyInstalled
* whether the given add-on is installed as temporary.
* @returns {Promise<ExtensionData>}
*/
static async constructAsync({
rootURI,
checkPrivileged,
temporarilyInstalled,
}) {
let extension = new ExtensionData(rootURI);
// checkPrivileged depends on the extension type and id.
await extension.initializeAddonTypeAndID();
let { type, id } = extension;
extension.isPrivileged = await checkPrivileged(type, id);
extension.temporarilyInstalled = temporarilyInstalled;
return extension;
}
static getIsPrivileged({ signedState, builtIn, temporarilyInstalled }) {
return (
signedState === lazy.AddonManager.SIGNEDSTATE_PRIVILEGED ||
signedState === lazy.AddonManager.SIGNEDSTATE_SYSTEM ||
builtIn ||
(lazy.AddonSettings.EXPERIMENTS_ENABLED && temporarilyInstalled)
);
}
get builtinMessages() {
return null;
}
get logger() {
let id = this.id || "<unknown>";
return Log.repository.getLogger(LOGGER_ID_BASE + id);
}
/**
* Report an error about the extension's manifest file.
*
* @param {string} message The error message
*/
manifestError(message) {
this.packagingError(`Reading manifest: ${message}`);
}
/**
* Report a warning about the extension's manifest file.
*
* @param {string} message The warning message
*/
manifestWarning(message) {
this.packagingWarning(`Reading manifest: ${message}`);
}
// Report an error about the extension's general packaging.
packagingError(message) {
this.errors.push(message);
this.logError(message);
}
packagingWarning(message) {
this.warnings.push(message);
this.logWarning(message);
}
logWarning(message) {
this._logMessage(message, "warn");
}
logError(message) {
this._logMessage(message, "error");
}
_logMessage(message, severity) {
this.logger[severity](`Loading extension '${this.id}': ${message}`);
}
ensureNoErrors() {
if (this.errors.length) {
// startup() repeatedly checks whether there are errors after parsing the
// extension/manifest before proceeding with starting up.
throw new Error(this.errors.join("\n"));
}
}
/**
* Returns the moz-extension: URL for the given path within this
* extension.
*
* Must not be called unless either the `id` or `uuid` property has
* already been set.
*
* @param {string} path The path portion of the URL.
* @returns {string}
*/
getURL(path = "") {
if (!(this.id || this.uuid)) {
throw new Error(
"getURL may not be called before an `id` or `uuid` has been set"
);
}
if (!this.uuid) {
this.uuid = UUIDMap.get(this.id);
}
return `moz-extension://${this.uuid}/${path}`;
}
/**
* Discovers the file names within a directory or JAR file.
*
* @param {string} path
* The path to the directory or jar file to look at.
* @param {boolean} [directoriesOnly]
* If true, this will return only the directories present within the directory.
* @returns {Promise<string[]>}
* An array of names of files/directories (only the name, not the path).
*/
async _readDirectory(path, directoriesOnly = false) {
if (this.rootURI instanceof Ci.nsIFileURL) {
let uri = Services.io.newURI("./" + path, null, this.rootURI);
let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
let results = [];
try {
let children = await IOUtils.getChildren(fullPath);
for (let child of children) {
if (
!directoriesOnly ||
(await IOUtils.stat(child)).type == "directory"
) {
results.push(PathUtils.filename(child));
}
}
} catch (ex) {
// Fall-through, return what we have.
}
return results;
}
let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
// Append the sub-directory path to the base JAR URI and normalize the
// result.
let entry = `${uri.JAREntry}/${path}/`
.replace(/\/\/+/g, "/")
.replace(/^\//, "");
uri = Services.io.newURI(`jar:${uri.JARFile.spec}!/${entry}`);
let results = [];
for (let name of lazy.aomStartup.enumerateJARSubtree(uri)) {
if (!name.startsWith(entry)) {
throw new Error("Unexpected ZipReader entry");
}
// The enumerator returns the full path of all entries.
// Trim off the leading path, and filter out entries from
// subdirectories.
name = name.slice(entry.length);
if (
name &&
!/\/./.test(name) &&
(!directoriesOnly || name.endsWith("/"))
) {
results.push(name.replace("/", ""));
}
}
return results;
}
readJSON(path) {
return new Promise((resolve, reject) => {
let uri = this.rootURI.resolve(`./${path}`);
lazy.NetUtil.asyncFetch(
{ uri, loadUsingSystemPrincipal: true },
(inputStream, status) => {
if (!Components.isSuccessCode(status)) {
// Convert status code to a string
let e = Components.Exception("", status);
reject(new Error(`Error while loading '${uri}' (${e.name})`));
return;
}
try {
let text = lazy.NetUtil.readInputStreamToString(
inputStream,
inputStream.available(),
{ charset: "utf-8" }
);
text = text.replace(COMMENT_REGEXP, "$1");
resolve(JSON.parse(text));
} catch (e) {
reject(e);
}
}
);
});
}
get restrictSchemes() {
return !(this.isPrivileged && this.hasPermission("mozillaAddons"));
}
get optionsPageProperties() {
let page = this.manifest.options_ui?.page ?? this.manifest.options_page;
if (!page) {
return null;
}
return {
page,
open_in_tab: this.manifest.options_ui
? (this.manifest.options_ui.open_in_tab ?? false)
: true,
// `options_ui.browser_style` is assigned the proper default value
// (true for MV2 and false for MV3 when not explicitly set),
// in `#parseBrowserStyleInManifest` (called when we are loading
// and parse manifest data from the `parseManifest` method).
browser_style: this.manifest.options_ui?.browser_style ?? false,
};
}
/**
* Given an array of host and permissions, generate a structured permissions object
* that contains seperate host origins and permissions arrays.
*
* @param {Array} permissionsArray
* @param {Array} [hostPermissions]
* @returns {object} permissions object
*/
permissionsObject(permissionsArray = [], hostPermissions = []) {
let permissions = new Set();
let origins = new Set();
let { restrictSchemes, isPrivileged } = this;
let isMV2 = this.manifestVersion === 2;
for (let perm of permissionsArray.concat(hostPermissions)) {
let type = classifyPermission(perm, restrictSchemes, isPrivileged);
if (type.origin) {
origins.add(perm);
} else if (type.permission) {
if (isMV2 && PERMS_NOT_IN_MV2.has(perm)) {
// Skip, without warning (parseManifest warns if needed).
continue;
}
permissions.add(perm);
}
}
return {
permissions,
origins,
};
}
/**
* Returns an object representing any capabilities that the extension
* has access to based on fixed properties in the manifest. The result
* includes the contents of the "permissions" property as well as other
* capabilities that are derived from manifest fields that users should
* be informed of (e.g., origins where content scripts are injected).
*
* For MV3 extensions with origin controls, this does not include origins.
*/
getRequiredPermissions() {
if (this.type !== "extension") {
return null;
}
let { permissions } = this.permissionsObject(this.manifest.permissions);
if (
this.manifest.devtools_page &&
!this.manifest.optional_permissions.includes("devtools")
) {
permissions.add("devtools");
}
return {
permissions: Array.from(permissions),
origins: this.originControls ? [] : this.getManifestOrigins(),
};
}
/**
* @returns {string[]} all origins that are referenced in manifest via
* permissions, host_permissions, or content_scripts keys.
*/
getManifestOrigins() {
if (this.type !== "extension") {
return null;
}
let { origins } = this.permissionsObject(
this.manifest.permissions,
this.manifest.host_permissions
);
for (let entry of this.manifest.content_scripts || []) {
for (let origin of entry.matches) {
origins.add(origin);
}
}
return Array.from(origins);
}
/**
* @returns {MatchPatternSet} MatchPatternSet for only the origins that are
* referenced in manifest via permissions, host_permissions, or content_scripts keys.
*/
getManifestOriginsMatchPatternSet() {
if (this.type !== "extension") {
return null;
}
if (this._manifestOriginsMatchPatternSet) {
return this._manifestOriginsMatchPatternSet;
}
this._manifestOriginsMatchPatternSet = new MatchPatternSet(
this.getManifestOrigins(),
{
restrictSchemes: this.restrictSchemes,
ignorePath: true,
}
);
return this._manifestOriginsMatchPatternSet;
}
/**
* Returns additional permissions that extensions is requesting based on its
* manifest. For now, this is host_permissions (and content scripts) in mv3.
*/
getRequestedPermissions() {
if (this.type !== "extension") {
return null;
}
if (this.originControls && lazy.installIncludesOrigins) {
return { permissions: [], origins: this.getManifestOrigins() };
}
return { permissions: [], origins: [] };
}
/**
* Returns optional permissions from the manifest, including host permissions
* if originControls is true.
*/
get manifestOptionalPermissions() {
if (this.type !== "extension") {
return null;
}
let { permissions, origins } = this.permissionsObject(
this.manifest.optional_permissions,
this.manifest.optional_host_permissions
);
if (this.originControls) {
for (let origin of this.getManifestOrigins()) {
origins.add(origin);
}
}
return {
permissions: Array.from(permissions),
origins: Array.from(origins),
};
}
/**
* Returns an object representing all capabilities this extension has
* access to, including fixed ones from the manifest as well as dynamically
* granted permissions.
*/
get activePermissions() {
if (this.type !== "extension") {
return null;
}
let result = {
origins: this.allowedOrigins.patterns
.map(matcher => matcher.pattern)
// moz-extension://id/* is always added to allowedOrigins, but it
// is not a valid host permission in the API. So, remove it.
.filter(pattern => !pattern.startsWith("moz-extension:")),
apis: [...this.apiNames],
};
const EXP_PATTERN = /^experiments\.\w+/;
result.permissions = [...this.permissions].filter(
p => !result.origins.includes(p) && !EXP_PATTERN.test(p)
);
return result;
}
// Returns whether the front end should prompt for this permission
static async shouldPromptFor(permission) {
return !(await lazy.NO_PROMPT_PERMISSIONS).has(permission);
}
// Compute the difference between two sets of permissions, suitable
// for presenting to the user.
static comparePermissions(oldPermissions, newPermissions) {
let oldMatcher = new MatchPatternSet(oldPermissions.origins, {
restrictSchemes: false,
});
return {
// formatPermissionStrings ignores any scheme, so only look at the domain.
origins: newPermissions.origins.filter(
perm =>
!oldMatcher.subsumesDomain(
new MatchPattern(perm, { restrictSchemes: false })
)
),
permissions: newPermissions.permissions.filter(
perm => !oldPermissions.permissions.includes(perm)
),
};
}
// Return those permissions in oldPermissions that also exist in newPermissions.
static intersectPermissions(oldPermissions, newPermissions) {
let matcher = new MatchPatternSet(newPermissions.origins, {
restrictSchemes: false,
});
return {
origins: oldPermissions.origins.filter(perm =>
matcher.subsumesDomain(
new MatchPattern(perm, { restrictSchemes: false })
)
),
permissions: oldPermissions.permissions.filter(perm =>
newPermissions.permissions.includes(perm)
),
};
}
/**
* When updating the addon, find and migrate permissions that have moved from required
* to optional. This also handles any updates required for permission removal.
*
* @param {string} id The id of the addon being updated
* @param {object} oldPermissions
* @param {object} oldOptionalPermissions
* @param {object} newPermissions
* @param {object} newOptionalPermissions
*/
static async migratePermissions(
id,
oldPermissions,
oldOptionalPermissions,
newPermissions,
newOptionalPermissions
) {
let migrated = ExtensionData.intersectPermissions(
oldPermissions,
newOptionalPermissions
);
// If a permission is optional in this version and was mandatory in the previous
// version, it was already accepted by the user at install time so add it to the
// list of granted optional permissions now.
await lazy.ExtensionPermissions.add(id, migrated);
// Now we need to update ExtensionPreferencesManager, removing any settings
// for old permissions that no longer exist.
let permSet = new Set(
newPermissions.permissions.concat(newOptionalPermissions.permissions)
);
let oldPerms = oldPermissions.permissions.concat(
oldOptionalPermissions.permissions
);
let removed = oldPerms.filter(x => !permSet.has(x));
// Force the removal here to ensure the settings are removed prior
// to startup. This will remove both required or optional permissions,
// whereas the call from within ExtensionPermissions would only result
// in a removal for optional permissions that were removed.
await lazy.ExtensionPreferencesManager.removeSettingsForPermissions(
id,
removed
);
// Remove any optional permissions that have been removed from the manifest.
await lazy.ExtensionPermissions.remove(id, {
permissions: removed,
origins: [],
});
}
canUseAPIExperiment() {
return (
this.type == "extension" &&
(this.isPrivileged ||
// TODO(Bug 1771341): Allowing the "experiment_apis" property when only
// AddonSettings.EXPERIMENTS_ENABLED is true is currently needed to allow,
// while running under automation, the test harness extensions (like mochikit
// and specialpowers) to use that privileged manifest property.
lazy.AddonSettings.EXPERIMENTS_ENABLED)
);
}
canUseThemeExperiment() {
return (
["extension", "theme"].includes(this.type) &&
(this.isPrivileged ||
// "theme_experiment" MDN docs are currently explicitly mentioning this is expected
// to be allowed also for non-signed extensions installed non-temporarily on builds
// where the signature checks can be disabled).
//
// NOTE: be careful to don't regress "theme_experiment" (see Bug 1773076) while changing
// AddonSettings.EXPERIMENTS_ENABLED (e.g. as part of fixing Bug 1771341).
lazy.AddonSettings.EXPERIMENTS_ENABLED)
);
}
get manifestVersion() {
return this.manifest.manifest_version;
}
get persistentBackground() {
let { manifest } = this;
if (
!manifest.background ||
(manifest.background.service_worker &&
WebExtensionPolicy.backgroundServiceWorkerEnabled) ||
this.manifestVersion > 2
) {
return false;
}
// V2 addons can only use event pages if the pref is also flipped and
// persistent is explicilty set to false.
return !this.eventPagesEnabled || manifest.background.persistent;
}
/**
* backgroundState can be starting, running, suspending or stopped.
* It is undefined if the extension has no background page.
* See ext-backgroundPage.js for more details.
*
* @param {string} state starting, running, suspending or stopped
*/
set backgroundState(state) {
this._backgroundState = state;
}
get backgroundState() {
return this._backgroundState;
}
async getExtensionVersionWithoutValidation() {
return (await this.readJSON("manifest.json")).version;
}
/**
* Load a locale and return a localized manifest. The extension must
* be initialized, and manifest parsed prior to calling.
*
* @param {string} locale to load, if necessary.
* @returns {Promise<object>} normalized manifest.
*/
async getLocalizedManifest(locale) {
if (!this.type || !this.localeData) {
throw new Error("The extension has not been initialized.");
}
// Upon update or reinstall, the Extension.manifest may be read from
// StartupCache.manifest, however rawManifest is *not*. We need the
// raw manifest in order to get a localized manifest.
if (!this.rawManifest) {
this.rawManifest = await this.readJSON("manifest.json");
}
if (!this.localeData.has(locale)) {
// Locales are not avialable until some additional
// initialization is done. We could just call initAllLocales,
// but that is heavy handed, especially when we likely only
// need one out of 20.
let locales = await this.promiseLocales();
if (locales.get(locale)) {
await this.initLocale(locale);
}
if (!this.localeData.has(locale)) {
throw new Error(`The extension does not contain the locale ${locale}`);
}
}
let normalized = await this._getNormalizedManifest(locale);
if (normalized.error) {
throw new Error(normalized.error);
}
return normalized.value;
}
async _getNormalizedManifest(locale) {
let manifestType = manifestTypes.get(this.type);
let context = {
url: this.baseURI && this.baseURI.spec,
principal: this.principal,
logError: error => {
this.manifestWarning(error);
},
preprocessors: {},
manifestVersion: this.manifestVersion,
// We introduced this context param in Bug 1831417.
ignoreUnrecognizedProperties: false,
};
if (this.fluentL10n || this.localeData) {
context.preprocessors.localize = value => this.localize(value, locale);
}
return lazy.Schemas.normalize(this.rawManifest, manifestType, context);
}
#parseBrowserStyleInManifest(manifest, manifestKey, defaultValueInMV2) {
const obj = manifest[manifestKey];
if (!obj) {
return;
}
const browserStyleIsVoid = obj.browser_style == null;
obj.browser_style ??= defaultValueInMV2;
if (this.manifestVersion < 3 || !obj.browser_style) {
// MV2 (true or false), or MV3 (false set explicitly or default false).
// No changes in observed behavior, return now to avoid logspam.
return;
}
// Now there are two cases (MV3 only):
// - browser_style was not specified, but defaults to true.
// - browser_style was set to true by the extension.
//
// These will eventually be deprecated. For the deprecation plan, see
let warning;
if (!lazy.browserStyleMV3supported) {
obj.browser_style = false;
if (browserStyleIsVoid && !lazy.browserStyleMV3sameAsMV2) {
// defaultValueInMV2 is true, but there was no intent to use these
// defaults. Don't warn.
return;
}
warning = `"browser_style:true" is no longer supported in Manifest Version 3.`;
} else {
warning = `"browser_style:true" has been deprecated in Manifest Version 3 and will be unsupported in the near future.`;
}
if (browserStyleIsVoid) {
warning += ` While "${manifestKey}.browser_style" was not explicitly specified in manifest.json, its default value was true.`;
if (!lazy.browserStyleMV3sameAsMV2) {
obj.browser_style = false;
warning += ` The default value of "${manifestKey}.browser_style" has changed from true to false in Manifest Version 3.`;
} else {
warning += ` Its default will change to false in Manifest Version 3 starting from Firefox 115.`;
}
}
this.manifestWarning(
`Warning processing ${manifestKey}.browser_style: ${warning}`
);
}
// AMO enforces a maximum length of 45 on the name since at least 2017, via
// To avoid breaking add-ons that do not go through AMO (e.g. temporarily
// loaded extensions), we enforce the limit by truncating and warning if
// needed, instead enforcing a maxLength on "name" in schemas/manifest.json.
//
// We set the limit to 75, which is a safe limit that matches the CWS,
static EXT_NAME_MAX_LEN = 75;
async initializeAddonTypeAndID() {
if (this.type) {
// Already initialized.
return;
}
this.rawManifest = await this.readJSON("manifest.json");
let manifest = this.rawManifest;
if (manifest.