summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/internal/XPIProvider.jsm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/mozapps/extensions/internal/XPIProvider.jsm3378
1 files changed, 3378 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
new file mode 100644
index 0000000000..94e5e26636
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -0,0 +1,3378 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * This file contains most of the logic required to load and run
+ * extensions at startup. Anything which is not required immediately at
+ * startup should go in XPIInstall.jsm or XPIDatabase.jsm if at all
+ * possible, in order to minimize the impact on startup performance.
+ */
+
+/**
+ * @typedef {number} integer
+ */
+
+/* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */
+
+var EXPORTED_SYMBOLS = ["XPIProvider", "XPIInternal"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AddonManager, AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ Dictionary: "resource://gre/modules/Extension.sys.mjs",
+ Extension: "resource://gre/modules/Extension.sys.mjs",
+ ExtensionData: "resource://gre/modules/Extension.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
+ Langpack: "resource://gre/modules/Extension.sys.mjs",
+ SitePermission: "resource://gre/modules/Extension.sys.mjs",
+ TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ XPIDatabase: "resource://gre/modules/addons/XPIDatabase.jsm",
+ XPIDatabaseReconcile: "resource://gre/modules/addons/XPIDatabase.jsm",
+ XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ aomStartup: [
+ "@mozilla.org/addons/addon-manager-startup;1",
+ "amIAddonManagerStartup",
+ ],
+ resProto: [
+ "@mozilla.org/network/protocol;1?name=resource",
+ "nsISubstitutingProtocolHandler",
+ ],
+ spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
+ timerManager: [
+ "@mozilla.org/updates/timer-manager;1",
+ "nsIUpdateTimerManager",
+ ],
+});
+
+const nsIFile = Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithPath"
+);
+const FileInputStream = Components.Constructor(
+ "@mozilla.org/network/file-input-stream;1",
+ "nsIFileInputStream",
+ "init"
+);
+
+const PREF_DB_SCHEMA = "extensions.databaseSchema";
+const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
+const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes";
+const PREF_EM_STARTUP_SCAN_SCOPES = "extensions.startupScanScopes";
+// xpinstall.signatures.required only supported in dev builds
+const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required";
+const PREF_LANGPACK_SIGNATURES = "extensions.langpacks.signatures.required";
+const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons";
+const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
+const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
+
+const PREF_EM_LAST_APP_BUILD_ID = "extensions.lastAppBuildId";
+
+// Specify a list of valid built-in add-ons to load.
+const BUILT_IN_ADDONS_URI = "chrome://browser/content/built_in_addons.json";
+
+const DIR_EXTENSIONS = "extensions";
+const DIR_SYSTEM_ADDONS = "features";
+const DIR_APP_SYSTEM_PROFILE = "system-extensions";
+const DIR_STAGE = "staged";
+const DIR_TRASH = "trash";
+
+const FILE_XPI_STATES = "addonStartup.json.lz4";
+
+const KEY_PROFILEDIR = "ProfD";
+const KEY_ADDON_APP_DIR = "XREAddonAppDir";
+const KEY_APP_DISTRIBUTION = "XREAppDist";
+const KEY_APP_FEATURES = "XREAppFeat";
+
+const KEY_APP_PROFILE = "app-profile";
+const KEY_APP_SYSTEM_PROFILE = "app-system-profile";
+const KEY_APP_SYSTEM_ADDONS = "app-system-addons";
+const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults";
+const KEY_APP_BUILTINS = "app-builtin";
+const KEY_APP_GLOBAL = "app-global";
+const KEY_APP_SYSTEM_LOCAL = "app-system-local";
+const KEY_APP_SYSTEM_SHARE = "app-system-share";
+const KEY_APP_SYSTEM_USER = "app-system-user";
+const KEY_APP_TEMPORARY = "app-temporary";
+
+const TEMPORARY_ADDON_SUFFIX = "@temporary-addon";
+
+const STARTUP_MTIME_SCOPES = [
+ KEY_APP_GLOBAL,
+ KEY_APP_SYSTEM_LOCAL,
+ KEY_APP_SYSTEM_SHARE,
+ KEY_APP_SYSTEM_USER,
+];
+
+const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
+const XPI_PERMISSION = "install";
+
+const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;
+
+const DB_SCHEMA = 35;
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "enabledScopesPref",
+ PREF_EM_ENABLED_SCOPES,
+ AddonManager.SCOPE_ALL
+);
+
+Object.defineProperty(lazy, "enabledScopes", {
+ get() {
+ // The profile location is always enabled
+ return lazy.enabledScopesPref | AddonManager.SCOPE_PROFILE;
+ },
+});
+
+function encoded(strings, ...values) {
+ let result = [];
+
+ for (let [i, string] of strings.entries()) {
+ result.push(string);
+ if (i < values.length) {
+ result.push(encodeURIComponent(values[i]));
+ }
+ }
+
+ return result.join("");
+}
+
+const BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6,
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+};
+
+// All addonTypes supported by the XPIProvider. These values can be passed to
+// AddonManager.getAddonsByTypes in order to get XPIProvider.getAddonsByTypes
+// to return only supported add-ons. Without these, it is possible for
+// AddonManager.getAddonsByTypes to return addons from other providers, or even
+// add-on types that are no longer supported by XPIProvider.
+const ALL_XPI_TYPES = new Set([
+ "dictionary",
+ "extension",
+ "locale",
+ // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
+ "sitepermission-deprecated",
+ "theme",
+]);
+
+/**
+ * Valid IDs fit this pattern.
+ */
+var gIDTest =
+ /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
+
+const { Log } = ChromeUtils.importESModule(
+ "resource://gre/modules/Log.sys.mjs"
+);
+const LOGGER_ID = "addons.xpi";
+
+// Create a new logger for use by all objects in this Addons XPI Provider module
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+/**
+ * Spins the event loop until the given promise resolves, and then eiter returns
+ * its success value or throws its rejection value.
+ *
+ * @param {Promise} promise
+ * The promise to await.
+ * @returns {any}
+ * The promise's resolution value, if any.
+ */
+function awaitPromise(promise) {
+ let success = undefined;
+ let result = null;
+
+ promise.then(
+ val => {
+ success = true;
+ result = val;
+ },
+ val => {
+ success = false;
+ result = val;
+ }
+ );
+
+ Services.tm.spinEventLoopUntil(
+ "XPIProvider.jsm:awaitPromise",
+ () => success !== undefined
+ );
+
+ if (!success) {
+ throw result;
+ }
+ return result;
+}
+
+/**
+ * Returns a nsIFile instance for the given path, relative to the given
+ * base file, if provided.
+ *
+ * @param {string} path
+ * The (possibly relative) path of the file.
+ * @param {nsIFile} [base]
+ * An optional file to use as a base path if `path` is relative.
+ * @returns {nsIFile}
+ */
+function getFile(path, base = null) {
+ // First try for an absolute path, as we get in the case of proxy
+ // files. Ideally we would try a relative path first, but on Windows,
+ // paths which begin with a drive letter are valid as relative paths,
+ // and treated as such.
+ try {
+ return new nsIFile(path);
+ } catch (e) {
+ // Ignore invalid relative paths. The only other error we should see
+ // here is EOM, and either way, any errors that we care about should
+ // be re-thrown below.
+ }
+
+ // If the path isn't absolute, we must have a base path.
+ let file = base.clone();
+ file.appendRelativePath(path);
+ return file;
+}
+
+/**
+ * Returns true if the given file, based on its name, should be treated
+ * as an XPI. If the file does not have an appropriate extension, it is
+ * assumed to be an unpacked add-on.
+ *
+ * @param {string} filename
+ * The filename to check.
+ * @param {boolean} [strict = false]
+ * If true, this file is in a location maintained by the browser, and
+ * must have a strict, lower-case ".xpi" extension.
+ * @returns {boolean}
+ * True if the file is an XPI.
+ */
+function isXPI(filename, strict) {
+ if (strict) {
+ return filename.endsWith(".xpi");
+ }
+ let ext = filename.slice(-4).toLowerCase();
+ return ext === ".xpi" || ext === ".zip";
+}
+
+/**
+ * Returns the extension expected ID for a given file in an extension install
+ * directory.
+ *
+ * @param {nsIFile} file
+ * The extension XPI file or unpacked directory.
+ * @returns {AddonId?}
+ * The add-on ID, if valid, or null otherwise.
+ */
+function getExpectedID(file) {
+ let { leafName } = file;
+ let id = isXPI(leafName, true) ? leafName.slice(0, -4) : leafName;
+ if (gIDTest.test(id)) {
+ return id;
+ }
+ return null;
+}
+
+/**
+ * Evaluates whether an add-on is allowed to run in safe mode.
+ *
+ * @param {AddonInternal} aAddon
+ * The add-on to check
+ * @returns {boolean}
+ * True if the add-on should run in safe mode
+ */
+function canRunInSafeMode(aAddon) {
+ let location = aAddon.location || null;
+ if (!location) {
+ return false;
+ }
+
+ // Even though the updated system add-ons aren't generally run in safe mode we
+ // include them here so their uninstall functions get called when switching
+ // back to the default set.
+
+ // TODO product should make the call about temporary add-ons running
+ // in safe mode. assuming for now that they are.
+ return location.isTemporary || location.isSystem || location.isBuiltin;
+}
+
+/**
+ * Gets an nsIURI for a file within another file, either a directory or an XPI
+ * file. If aFile is a directory then this will return a file: URI, if it is an
+ * XPI file then it will return a jar: URI.
+ *
+ * @param {nsIFile} aFile
+ * The file containing the resources, must be either a directory or an
+ * XPI file
+ * @param {string} aPath
+ * The path to find the resource at, "/" separated. If aPath is empty
+ * then the uri to the root of the contained files will be returned
+ * @returns {nsIURI}
+ * An nsIURI pointing at the resource
+ */
+function getURIForResourceInFile(aFile, aPath) {
+ if (!isXPI(aFile.leafName)) {
+ let resource = aFile.clone();
+ if (aPath) {
+ aPath.split("/").forEach(part => resource.append(part));
+ }
+
+ return Services.io.newFileURI(resource);
+ }
+
+ return buildJarURI(aFile, aPath);
+}
+
+/**
+ * Creates a jar: URI for a file inside a ZIP file.
+ *
+ * @param {nsIFile} aJarfile
+ * The ZIP file as an nsIFile
+ * @param {string} aPath
+ * The path inside the ZIP file
+ * @returns {nsIURI}
+ * An nsIURI for the file
+ */
+function buildJarURI(aJarfile, aPath) {
+ let uri = Services.io.newFileURI(aJarfile);
+ uri = "jar:" + uri.spec + "!/" + aPath;
+ return Services.io.newURI(uri);
+}
+
+function maybeResolveURI(uri) {
+ if (uri.schemeIs("resource")) {
+ return Services.io.newURI(lazy.resProto.resolveURI(uri));
+ }
+ return uri;
+}
+
+/**
+ * Iterates over the entries in a given directory.
+ *
+ * Fails silently if the given directory does not exist.
+ *
+ * @param {nsIFile} aDir
+ * Directory to iterate.
+ */
+function* iterDirectory(aDir) {
+ let dirEnum;
+ try {
+ dirEnum = aDir.directoryEntries;
+ let file;
+ while ((file = dirEnum.nextFile)) {
+ yield file;
+ }
+ } catch (e) {
+ if (aDir.exists()) {
+ logger.warn(`Can't iterate directory ${aDir.path}`, e);
+ }
+ } finally {
+ if (dirEnum) {
+ dirEnum.close();
+ }
+ }
+}
+
+/**
+ * Migrate data about an addon to match the change made in bug 857456
+ * in which "webextension-foo" types were converted to "foo" and the
+ * "loader" property was added to distinguish different addon types.
+ *
+ * @param {Object} addon The addon info to migrate.
+ * @returns {boolean} True if the addon data was converted, false if not.
+ */
+function migrateAddonLoader(addon) {
+ if (addon.hasOwnProperty("loader")) {
+ return false;
+ }
+
+ switch (addon.type) {
+ case "extension":
+ case "dictionary":
+ case "locale":
+ case "theme":
+ addon.loader = "bootstrap";
+ break;
+
+ case "webextension":
+ addon.type = "extension";
+ addon.loader = null;
+ break;
+
+ case "webextension-dictionary":
+ addon.type = "dictionary";
+ addon.loader = null;
+ break;
+
+ case "webextension-langpack":
+ addon.type = "locale";
+ addon.loader = null;
+ break;
+
+ case "webextension-theme":
+ addon.type = "theme";
+ addon.loader = null;
+ break;
+
+ default:
+ logger.warn(`Not converting unknown addon type ${addon.type}`);
+ }
+ return true;
+}
+
+/**
+ * The on-disk state of an individual XPI, created from an Object
+ * as stored in the addonStartup.json file.
+ */
+const JSON_FIELDS = Object.freeze([
+ "dependencies",
+ "enabled",
+ "file",
+ "loader",
+ "lastModifiedTime",
+ "path",
+ "recommendationState",
+ "rootURI",
+ "runInSafeMode",
+ "signedState",
+ "signedDate",
+ "startupData",
+ "telemetryKey",
+ "type",
+ "version",
+]);
+
+class XPIState {
+ constructor(location, id, saved = {}) {
+ this.location = location;
+ this.id = id;
+
+ // Set default values.
+ this.type = "extension";
+
+ for (let prop of JSON_FIELDS) {
+ if (prop in saved) {
+ this[prop] = saved[prop];
+ }
+ }
+
+ // Builds prior to be 1512436 did not include the rootURI property.
+ // If we're updating from such a build, add that property now.
+ if (!("rootURI" in this) && this.file) {
+ this.rootURI = getURIForResourceInFile(this.file, "").spec;
+ }
+
+ if (!this.telemetryKey) {
+ this.telemetryKey = this.getTelemetryKey();
+ }
+
+ if (
+ saved.currentModifiedTime &&
+ saved.currentModifiedTime != this.lastModifiedTime
+ ) {
+ this.lastModifiedTime = saved.currentModifiedTime;
+ } else if (saved.currentModifiedTime === null) {
+ this.missing = true;
+ }
+ }
+
+ // Compatibility shim getters for legacy callers in XPIDatabase.jsm.
+ get mtime() {
+ return this.lastModifiedTime;
+ }
+ get active() {
+ return this.enabled;
+ }
+
+ /**
+ * @property {string} path
+ * The full on-disk path of the add-on.
+ */
+ get path() {
+ return this.file && this.file.path;
+ }
+ set path(path) {
+ this.file = path ? getFile(path, this.location.dir) : null;
+ }
+
+ /**
+ * @property {string} relativePath
+ * The path to the add-on relative to its parent location, or
+ * the full path if its parent location has no on-disk path.
+ */
+ get relativePath() {
+ if (this.location.dir && this.location.dir.contains(this.file)) {
+ let path = this.file.getRelativePath(this.location.dir);
+ if (AppConstants.platform == "win") {
+ path = path.replace(/\//g, "\\");
+ }
+ return path;
+ }
+ return this.path;
+ }
+
+ /**
+ * Returns a JSON-compatible representation of this add-on's state
+ * data, to be saved to addonStartup.json.
+ *
+ * @returns {Object}
+ */
+ toJSON() {
+ let json = {
+ dependencies: this.dependencies,
+ enabled: this.enabled,
+ lastModifiedTime: this.lastModifiedTime,
+ loader: this.loader,
+ path: this.relativePath,
+ recommendationState: this.recommendationState,
+ rootURI: this.rootURI,
+ runInSafeMode: this.runInSafeMode,
+ signedState: this.signedState,
+ signedDate: this.signedDate,
+ telemetryKey: this.telemetryKey,
+ version: this.version,
+ };
+ if (this.type != "extension") {
+ json.type = this.type;
+ }
+ if (this.startupData) {
+ json.startupData = this.startupData;
+ }
+ return json;
+ }
+
+ get isWebExtension() {
+ return this.loader == null;
+ }
+
+ get isPrivileged() {
+ return lazy.ExtensionData.getIsPrivileged({
+ signedState: this.signedState,
+ builtIn: this.location.isBuiltin,
+ temporarilyInstalled: this.location.isTemporary,
+ });
+ }
+
+ /**
+ * Update the last modified time for an add-on on disk.
+ *
+ * @param {nsIFile} aFile
+ * The location of the add-on.
+ * @returns {boolean}
+ * True if the time stamp has changed.
+ */
+ getModTime(aFile) {
+ let mtime = 0;
+ try {
+ // Clone the file object so we always get the actual mtime, rather
+ // than whatever value it may have cached.
+ mtime = aFile.clone().lastModifiedTime;
+ } catch (e) {
+ logger.warn("Can't get modified time of ${path}", aFile, e);
+ }
+
+ let changed = mtime != this.lastModifiedTime;
+ this.lastModifiedTime = mtime;
+ return changed;
+ }
+
+ /**
+ * Returns a string key by which to identify this add-on in telemetry
+ * and crash reports.
+ *
+ * @returns {string}
+ */
+ getTelemetryKey() {
+ return encoded`${this.id}:${this.version}`;
+ }
+
+ get resolvedRootURI() {
+ return maybeResolveURI(Services.io.newURI(this.rootURI));
+ }
+
+ /**
+ * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true,
+ * update the last-modified time. This should probably be made async, but for now we
+ * don't want to maintain parallel sync and async versions of the scan.
+ *
+ * Caller is responsible for doing XPIStates.save() if necessary.
+ *
+ * @param {DBAddonInternal} aDBAddon
+ * The DBAddonInternal for this add-on.
+ * @param {boolean} [aUpdated = false]
+ * The add-on was updated, so we must record new modified time.
+ */
+ syncWithDB(aDBAddon, aUpdated = false) {
+ logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon));
+ // If the add-on changes from disabled to enabled, we should re-check the modified time.
+ // If this is a newly found add-on, it won't have an 'enabled' field but we
+ // did a full recursive scan in that case, so we don't need to do it again.
+ // We don't use aDBAddon.active here because it's not updated until after restart.
+ let mustGetMod = aDBAddon.visible && !aDBAddon.disabled && !this.enabled;
+
+ this.enabled = aDBAddon.visible && !aDBAddon.disabled;
+
+ this.version = aDBAddon.version;
+ this.type = aDBAddon.type;
+ this.loader = aDBAddon.loader;
+
+ if (aDBAddon.startupData) {
+ this.startupData = aDBAddon.startupData;
+ }
+
+ this.telemetryKey = this.getTelemetryKey();
+
+ this.dependencies = aDBAddon.dependencies;
+ this.runInSafeMode = canRunInSafeMode(aDBAddon);
+ this.signedState = aDBAddon.signedState;
+ this.signedDate = aDBAddon.signedDate;
+ this.file = aDBAddon._sourceBundle;
+ this.rootURI = aDBAddon.rootURI;
+ this.recommendationState = aDBAddon.recommendationState;
+
+ if ((aUpdated || mustGetMod) && this.file) {
+ this.getModTime(this.file);
+ if (this.lastModifiedTime != aDBAddon.updateDate) {
+ aDBAddon.updateDate = this.lastModifiedTime;
+ if (lazy.XPIDatabase.initialized) {
+ lazy.XPIDatabase.saveChanges();
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Manages the state data for add-ons in a given install location.
+ *
+ * @param {string} name
+ * The name of the install location (e.g., "app-profile").
+ * @param {string | nsIFile | null} path
+ * The on-disk path of the install location. May be null for some
+ * locations which do not map to a specific on-disk path.
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ * @param {object} [saved]
+ * The persisted JSON state data to restore.
+ */
+class XPIStateLocation extends Map {
+ constructor(name, path, scope, saved) {
+ super();
+
+ this.name = name;
+ this.scope = scope;
+ if (path instanceof Ci.nsIFile) {
+ this.dir = path;
+ this.path = path.path;
+ } else {
+ this.path = path;
+ this.dir = this.path && new nsIFile(this.path);
+ }
+ this.staged = {};
+ this.changed = false;
+
+ if (saved) {
+ this.restore(saved);
+ }
+
+ this._installer = undefined;
+ }
+
+ hasPrecedence(otherLocation) {
+ let locations = Array.from(XPIStates.locations());
+ return locations.indexOf(this) <= locations.indexOf(otherLocation);
+ }
+
+ get installer() {
+ if (this._installer === undefined) {
+ this._installer = this.makeInstaller();
+ }
+ return this._installer;
+ }
+
+ makeInstaller() {
+ return null;
+ }
+
+ restore(saved) {
+ if (!this.path && saved.path) {
+ this.path = saved.path;
+ this.dir = new nsIFile(this.path);
+ }
+ this.staged = saved.staged || {};
+ this.changed = saved.changed || false;
+
+ for (let [id, data] of Object.entries(saved.addons || {})) {
+ let xpiState = this._addState(id, data);
+
+ // Make a note that this state was restored from saved data. But
+ // only if this location hasn't moved since the last startup,
+ // since that causes problems for new system add-on bundles.
+ if (!this.path || this.path == saved.path) {
+ xpiState.wasRestored = true;
+ }
+ }
+ }
+
+ /**
+ * Returns a JSON-compatible representation of this location's state
+ * data, to be saved to addonStartup.json.
+ *
+ * @returns {Object}
+ */
+ toJSON() {
+ let json = {
+ addons: {},
+ staged: this.staged,
+ };
+
+ if (this.path) {
+ json.path = this.path;
+ }
+
+ if (STARTUP_MTIME_SCOPES.includes(this.name)) {
+ json.checkStartupModifications = true;
+ }
+
+ for (let [id, addon] of this.entries()) {
+ json.addons[id] = addon;
+ }
+ return json;
+ }
+
+ get hasStaged() {
+ for (let key in this.staged) {
+ return true;
+ }
+ return false;
+ }
+
+ _addState(addonId, saved) {
+ let xpiState = new XPIState(this, addonId, saved);
+ this.set(addonId, xpiState);
+ return xpiState;
+ }
+
+ /**
+ * Adds state data for the given DB add-on to the DB.
+ *
+ * @param {DBAddon} addon
+ * The DBAddon to add.
+ */
+ addAddon(addon) {
+ logger.debug(
+ "XPIStates adding add-on ${id} in ${location}: ${path}",
+ addon
+ );
+
+ XPIProvider.persistStartupData(addon);
+
+ let xpiState = this._addState(addon.id, { file: addon._sourceBundle });
+ xpiState.syncWithDB(addon, true);
+
+ XPIProvider.addTelemetry(addon.id, { location: this.name });
+ }
+
+ /**
+ * Remove the XPIState for an add-on and save the new state.
+ *
+ * @param {string} aId
+ * The ID of the add-on.
+ */
+ removeAddon(aId) {
+ if (this.has(aId)) {
+ this.delete(aId);
+ XPIStates.save();
+ }
+ }
+
+ /**
+ * Adds stub state data for the local file to the DB.
+ *
+ * @param {string} addonId
+ * The ID of the add-on represented by the given file.
+ * @param {nsIFile} file
+ * The local file or directory containing the add-on.
+ * @returns {XPIState}
+ */
+ addFile(addonId, file) {
+ let xpiState = this._addState(addonId, {
+ enabled: false,
+ file: file.clone(),
+ });
+ xpiState.getModTime(xpiState.file);
+ return xpiState;
+ }
+
+ /**
+ * Adds metadata for a staged install which should be performed after
+ * the next restart.
+ *
+ * @param {string} addonId
+ * The ID of the staged install. The leaf name of the XPI
+ * within the location's staging directory must correspond to
+ * this ID.
+ * @param {object} metadata
+ * The JSON metadata of the parsed install, to be used during
+ * the next startup.
+ */
+ stageAddon(addonId, metadata) {
+ this.staged[addonId] = metadata;
+ XPIStates.save();
+ }
+
+ /**
+ * Removes staged install metadata for the given add-on ID.
+ *
+ * @param {string} addonId
+ * The ID of the staged install.
+ */
+ unstageAddon(addonId) {
+ if (addonId in this.staged) {
+ delete this.staged[addonId];
+ XPIStates.save();
+ }
+ }
+
+ *getStagedAddons() {
+ for (let [id, metadata] of Object.entries(this.staged)) {
+ yield [id, metadata];
+ }
+ }
+
+ /**
+ * Returns true if the given addon was installed in this location by a text
+ * file pointing to its real path.
+ *
+ * @param {string} aId
+ * The ID of the addon
+ * @returns {boolean}
+ */
+ isLinkedAddon(aId) {
+ if (!this.dir) {
+ return true;
+ }
+ return this.has(aId) && !this.dir.contains(this.get(aId).file);
+ }
+
+ get isTemporary() {
+ return false;
+ }
+
+ get isSystem() {
+ return false;
+ }
+
+ get isBuiltin() {
+ return false;
+ }
+
+ get hidden() {
+ return this.isBuiltin;
+ }
+
+ // If this property is false, it does not implement readAddons()
+ // interface. This is used for the temporary and built-in locations
+ // that do not correspond to a physical location that can be scanned.
+ get enumerable() {
+ return true;
+ }
+}
+
+class TemporaryLocation extends XPIStateLocation {
+ /**
+ * @param {string} name
+ * The string identifier for the install location.
+ */
+ constructor(name) {
+ super(name, null, AddonManager.SCOPE_TEMPORARY);
+ this.locked = false;
+ }
+
+ makeInstaller() {
+ // Installs are a no-op. We only register that add-ons exist, and
+ // run them from their current location.
+ return {
+ installAddon() {},
+ uninstallAddon() {},
+ };
+ }
+
+ toJSON() {
+ return {};
+ }
+
+ get isTemporary() {
+ return true;
+ }
+
+ get enumerable() {
+ return false;
+ }
+}
+
+var TemporaryInstallLocation = new TemporaryLocation(KEY_APP_TEMPORARY);
+
+/**
+ * A "location" for addons installed from assets packged into the app.
+ */
+var BuiltInLocation = new (class _BuiltInLocation extends XPIStateLocation {
+ constructor() {
+ super(KEY_APP_BUILTINS, null, AddonManager.SCOPE_APPLICATION);
+ this.locked = false;
+ }
+
+ // The installer object is responsible for moving files around on disk
+ // when (un)installing an addon. Since this location handles only addons
+ // that are embedded within the browser, these are no-ops.
+ makeInstaller() {
+ return {
+ installAddon() {},
+ uninstallAddon() {},
+ };
+ }
+
+ get hidden() {
+ return false;
+ }
+
+ get isBuiltin() {
+ return true;
+ }
+
+ get enumerable() {
+ return false;
+ }
+
+ // Builtin addons are never linked, return false
+ // here for correct behavior elsewhere.
+ isLinkedAddon(/* aId */) {
+ return false;
+ }
+})();
+
+/**
+ * An object which identifies a directory install location for add-ons. The
+ * location consists of a directory which contains the add-ons installed in the
+ * location.
+ *
+ */
+class DirectoryLocation extends XPIStateLocation {
+ /**
+ * Each add-on installed in the location is either a directory containing the
+ * add-on's files or a text file containing an absolute path to the directory
+ * containing the add-ons files. The directory or text file must have the same
+ * name as the add-on's ID.
+ *
+ * @param {string} name
+ * The string identifier for the install location.
+ * @param {nsIFile} dir
+ * The directory for the install location.
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ * @param {boolean} [locked = true]
+ * If false, the location accepts new add-on installs.
+ * @param {boolean} [system = false]
+ * If true, the location is a system addon location.
+ */
+ constructor(name, dir, scope, locked = true, system = false) {
+ super(name, dir, scope);
+ this.locked = locked;
+ this._isSystem = system;
+ }
+
+ makeInstaller() {
+ if (this.locked) {
+ return null;
+ }
+ return new lazy.XPIInstall.DirectoryInstaller(this);
+ }
+
+ /**
+ * Reads a single-line file containing the path to a directory, and
+ * returns an nsIFile pointing to that directory, if successful.
+ *
+ * @param {nsIFile} aFile
+ * The file containing the directory path
+ * @returns {nsIFile?}
+ * An nsIFile object representing the linked directory, or null
+ * on error.
+ */
+ _readLinkFile(aFile) {
+ let linkedDirectory;
+ if (aFile.isSymlink()) {
+ linkedDirectory = aFile.clone();
+ try {
+ linkedDirectory.normalize();
+ } catch (e) {
+ logger.warn(
+ `Symbolic link ${aFile.path} points to a path ` +
+ `which does not exist`
+ );
+ return null;
+ }
+ } else {
+ let fis = new FileInputStream(aFile, -1, -1, false);
+ let line = {};
+ fis.QueryInterface(Ci.nsILineInputStream).readLine(line);
+ fis.close();
+
+ if (line.value) {
+ linkedDirectory = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ try {
+ linkedDirectory.initWithPath(line.value);
+ } catch (e) {
+ linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
+ }
+ }
+ }
+
+ if (linkedDirectory) {
+ if (!linkedDirectory.exists()) {
+ logger.warn(
+ `File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
+ "which does not exist"
+ );
+ return null;
+ }
+
+ if (!linkedDirectory.isDirectory()) {
+ logger.warn(
+ `File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
+ "which is not a directory"
+ );
+ return null;
+ }
+
+ return linkedDirectory;
+ }
+
+ logger.warn(`File pointer ${aFile.path} does not contain a path`);
+ return null;
+ }
+
+ /**
+ * Finds all the add-ons installed in this location.
+ *
+ * @returns {Map<AddonID, nsIFile>}
+ * A map of add-ons present in this location.
+ */
+ readAddons() {
+ let addons = new Map();
+
+ if (!this.dir) {
+ return addons;
+ }
+
+ // Use a snapshot of the directory contents to avoid possible issues with
+ // iterating over a directory while removing files from it (the YAFFS2
+ // embedded filesystem has this issue, see bug 772238).
+ for (let entry of Array.from(iterDirectory(this.dir))) {
+ let id = getExpectedID(entry);
+ if (!id) {
+ if (![DIR_STAGE, DIR_TRASH].includes(entry.leafName)) {
+ logger.debug(
+ "Ignoring file: name is not a valid add-on ID: ${}",
+ entry.path
+ );
+ }
+ continue;
+ }
+
+ if (id == entry.leafName && (entry.isFile() || entry.isSymlink())) {
+ let newEntry = this._readLinkFile(entry);
+ if (!newEntry) {
+ logger.debug(`Deleting stale pointer file ${entry.path}`);
+ try {
+ entry.remove(true);
+ } catch (e) {
+ logger.warn(`Failed to remove stale pointer file ${entry.path}`, e);
+ // Failing to remove the stale pointer file is ignorable
+ }
+ continue;
+ }
+
+ entry = newEntry;
+ }
+
+ addons.set(id, entry);
+ }
+ return addons;
+ }
+
+ get isSystem() {
+ return this._isSystem;
+ }
+}
+
+/**
+ * An object which identifies a built-in install location for add-ons, such
+ * as default system add-ons.
+ *
+ * This location should point either to a XPI, or a directory in a local build.
+ */
+class SystemAddonDefaults extends DirectoryLocation {
+ /**
+ * Read the manifest of allowed add-ons and build a mapping between ID and URI
+ * for each.
+ *
+ * @returns {Map<AddonID, nsIFile>}
+ * A map of add-ons present in this location.
+ */
+ readAddons() {
+ let addons = new Map();
+
+ let manifest = XPIProvider.builtInAddons;
+
+ if (!("system" in manifest)) {
+ logger.debug("No list of valid system add-ons found.");
+ return addons;
+ }
+
+ for (let id of manifest.system) {
+ let file = this.dir.clone();
+ file.append(`${id}.xpi`);
+
+ // Only attempt to load unpacked directory if unofficial build.
+ if (!AppConstants.MOZILLA_OFFICIAL && !file.exists()) {
+ file = this.dir.clone();
+ file.append(`${id}`);
+ }
+
+ addons.set(id, file);
+ }
+
+ return addons;
+ }
+
+ get isSystem() {
+ return true;
+ }
+
+ get isBuiltin() {
+ return true;
+ }
+}
+
+/**
+ * An object which identifies a directory install location for system add-ons
+ * updates.
+ */
+class SystemAddonLocation extends DirectoryLocation {
+ /**
+ * The location consists of a directory which contains the add-ons installed.
+ *
+ * @param {string} name
+ * The string identifier for the install location.
+ * @param {nsIFile} dir
+ * The directory for the install location.
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ * @param {boolean} resetSet
+ * True to throw away the current add-on set
+ */
+ constructor(name, dir, scope, resetSet) {
+ let addonSet = SystemAddonLocation._loadAddonSet();
+ let directory = null;
+
+ // The system add-on update directory is stored in a pref.
+ // Therefore, this is looked up before calling the
+ // constructor on the superclass.
+ if (addonSet.directory) {
+ directory = getFile(addonSet.directory, dir);
+ logger.info(`SystemAddonLocation scanning directory ${directory.path}`);
+ } else {
+ logger.info("SystemAddonLocation directory is missing");
+ }
+
+ super(name, directory, scope, false);
+
+ this._addonSet = addonSet;
+ this._baseDir = dir;
+
+ if (resetSet) {
+ this.installer.resetAddonSet();
+ }
+ }
+
+ makeInstaller() {
+ if (this.locked) {
+ return null;
+ }
+ return new lazy.XPIInstall.SystemAddonInstaller(this);
+ }
+
+ /**
+ * Reads the current set of system add-ons
+ *
+ * @returns {Object}
+ */
+ static _loadAddonSet() {
+ try {
+ let setStr = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_SET, null);
+ if (setStr) {
+ let addonSet = JSON.parse(setStr);
+ if (typeof addonSet == "object" && addonSet.schema == 1) {
+ return addonSet;
+ }
+ }
+ } catch (e) {
+ logger.error("Malformed system add-on set, resetting.");
+ }
+
+ return { schema: 1, addons: {} };
+ }
+
+ readAddons() {
+ // Updated system add-ons are ignored in safe mode
+ if (Services.appinfo.inSafeMode) {
+ return new Map();
+ }
+
+ let addons = super.readAddons();
+
+ // Strip out any unexpected add-ons from the list
+ for (let id of addons.keys()) {
+ if (!(id in this._addonSet.addons)) {
+ addons.delete(id);
+ }
+ }
+
+ return addons;
+ }
+
+ /**
+ * Tests whether updated system add-ons are expected.
+ *
+ * @returns {boolean}
+ */
+ isActive() {
+ return this.dir != null;
+ }
+
+ get isSystem() {
+ return true;
+ }
+
+ get isBuiltin() {
+ return true;
+ }
+}
+
+/**
+ * An object that identifies a registry install location for add-ons. The location
+ * consists of a registry key which contains string values mapping ID to the
+ * path where an add-on is installed
+ *
+ */
+class WinRegLocation extends XPIStateLocation {
+ /**
+ * @param {string} name
+ * The string identifier for the install location.
+ * @param {integer} rootKey
+ * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
+ * @param {integer} scope
+ * The scope of add-ons installed in this location.
+ */
+ constructor(name, rootKey, scope) {
+ super(name, undefined, scope);
+
+ this.locked = true;
+ this._rootKey = rootKey;
+ }
+
+ /**
+ * Retrieves the path of this Application's data key in the registry.
+ */
+ get _appKeyPath() {
+ let appVendor = Services.appinfo.vendor;
+ let appName = Services.appinfo.name;
+
+ // XXX Thunderbird doesn't specify a vendor string
+ if (appVendor == "" && AppConstants.MOZ_APP_NAME == "thunderbird") {
+ appVendor = "Mozilla";
+ }
+
+ return `SOFTWARE\\${appVendor}\\${appName}`;
+ }
+
+ /**
+ * Read the registry and build a mapping between ID and path for each
+ * installed add-on.
+ *
+ * @returns {Map<AddonID, nsIFile>}
+ * A map of add-ons in this location.
+ */
+ readAddons() {
+ let addons = new Map();
+
+ let path = `${this._appKeyPath}\\Extensions`;
+ let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+
+ // Reading the registry may throw an exception, and that's ok. In error
+ // cases, we just leave ourselves in the empty state.
+ try {
+ key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
+ } catch (e) {
+ return addons;
+ }
+
+ try {
+ let count = key.valueCount;
+ for (let i = 0; i < count; ++i) {
+ let id = key.getValueName(i);
+ let file = new nsIFile(key.readStringValue(id));
+ if (!file.exists()) {
+ logger.warn(`Ignoring missing add-on in ${file.path}`);
+ continue;
+ }
+
+ addons.set(id, file);
+ }
+ } finally {
+ key.close();
+ }
+
+ return addons;
+ }
+}
+
+/**
+ * Keeps track of the state of XPI add-ons on the file system.
+ */
+var XPIStates = {
+ // Map(location-name -> XPIStateLocation)
+ db: new Map(),
+
+ _jsonFile: null,
+
+ /**
+ * @property {Map<string, XPIState>} sideLoadedAddons
+ * A map of new add-ons detected during install location
+ * directory scans. Keys are add-on IDs, values are XPIState
+ * objects corresponding to those add-ons.
+ */
+ sideLoadedAddons: new Map(),
+
+ get size() {
+ let count = 0;
+ for (let location of this.locations()) {
+ count += location.size;
+ }
+ return count;
+ },
+
+ /**
+ * Load extension state data from addonStartup.json.
+ *
+ * @returns {Object}
+ */
+ loadExtensionState() {
+ let state;
+ try {
+ state = lazy.aomStartup.readStartupData();
+ } catch (e) {
+ logger.warn("Error parsing extensions state: ${error}", { error: e });
+ }
+
+ // When upgrading from a build prior to bug 857456, convert startup
+ // metadata.
+ let done = false;
+ for (let location of Object.values(state || {})) {
+ for (let data of Object.values(location.addons || {})) {
+ if (!migrateAddonLoader(data)) {
+ done = true;
+ break;
+ }
+ }
+ if (done) {
+ break;
+ }
+ }
+
+ logger.debug("Loaded add-on state: ${}", state);
+ return state || {};
+ },
+
+ /**
+ * Walk through all install locations, highest priority first,
+ * comparing the on-disk state of extensions to what is stored in prefs.
+ *
+ * @param {boolean} [ignoreSideloads = true]
+ * If true, ignore changes in scopes where we don't accept
+ * side-loads.
+ *
+ * @returns {boolean}
+ * True if anything has changed.
+ */
+ scanForChanges(ignoreSideloads = true) {
+ let oldState = this.initialStateData || this.loadExtensionState();
+ // We're called twice, do not restore the second time as new data
+ // may have been inserted since the first call.
+ let shouldRestoreLocationData = !this.initialStateData;
+ this.initialStateData = oldState;
+
+ let changed = false;
+ let oldLocations = new Set(Object.keys(oldState));
+
+ let startupScanScopes;
+ if (
+ Services.appinfo.appBuildID ==
+ Services.prefs.getCharPref(PREF_EM_LAST_APP_BUILD_ID, "")
+ ) {
+ startupScanScopes = Services.prefs.getIntPref(
+ PREF_EM_STARTUP_SCAN_SCOPES,
+ 0
+ );
+ } else {
+ // If the build id has changed, we need to do a full scan on first startup.
+ Services.prefs.setCharPref(
+ PREF_EM_LAST_APP_BUILD_ID,
+ Services.appinfo.appBuildID
+ );
+ startupScanScopes = AddonManager.SCOPE_ALL;
+ }
+
+ for (let loc of XPIStates.locations()) {
+ oldLocations.delete(loc.name);
+
+ if (shouldRestoreLocationData && oldState[loc.name]) {
+ loc.restore(oldState[loc.name]);
+ }
+ changed = changed || loc.changed;
+
+ // Don't bother checking scopes where we don't accept side-loads.
+ if (ignoreSideloads && !(loc.scope & startupScanScopes)) {
+ continue;
+ }
+
+ if (!loc.enumerable) {
+ continue;
+ }
+
+ // Don't bother scanning scopes where we don't have addons installed if they
+ // do not allow sideloading new addons. Once we have an addon in one of those
+ // locations, we need to check the location for changes (updates/deletions).
+ if (!loc.size && !(loc.scope & lazy.AddonSettings.SCOPES_SIDELOAD)) {
+ continue;
+ }
+
+ let knownIds = new Set(loc.keys());
+ for (let [id, file] of loc.readAddons()) {
+ knownIds.delete(id);
+
+ let xpiState = loc.get(id);
+ if (!xpiState) {
+ // If the location is not supported for sideloading, skip new
+ // addons. We handle this here so changes for existing sideloads
+ // will function.
+ if (
+ !loc.isSystem &&
+ !(loc.scope & lazy.AddonSettings.SCOPES_SIDELOAD)
+ ) {
+ continue;
+ }
+ logger.debug("New add-on ${id} in ${loc}", { id, loc: loc.name });
+
+ changed = true;
+ xpiState = loc.addFile(id, file);
+ if (!loc.isSystem) {
+ this.sideLoadedAddons.set(id, xpiState);
+ }
+ } else {
+ let addonChanged =
+ xpiState.getModTime(file) || file.path != xpiState.path;
+ xpiState.file = file.clone();
+
+ if (addonChanged) {
+ changed = true;
+ logger.debug("Changed add-on ${id} in ${loc}", {
+ id,
+ loc: loc.name,
+ });
+ } else {
+ logger.debug("Existing add-on ${id} in ${loc}", {
+ id,
+ loc: loc.name,
+ });
+ }
+ }
+ XPIProvider.addTelemetry(id, { location: loc.name });
+ }
+
+ // Anything left behind in oldState was removed from the file system.
+ for (let id of knownIds) {
+ loc.delete(id);
+ changed = true;
+ }
+ }
+
+ // If there's anything left in oldState, an install location that held add-ons
+ // was removed from the browser configuration.
+ changed = changed || oldLocations.size > 0;
+
+ logger.debug("scanForChanges changed: ${rv}, state: ${state}", {
+ rv: changed,
+ state: this.db,
+ });
+ return changed;
+ },
+
+ locations() {
+ return this.db.values();
+ },
+
+ /**
+ * @param {string} name
+ * The location name.
+ * @param {XPIStateLocation} location
+ * The location object.
+ */
+ addLocation(name, location) {
+ if (this.db.has(name)) {
+ throw new Error(`Trying to add duplicate location: ${name}`);
+ }
+ this.db.set(name, location);
+ },
+
+ /**
+ * Get the Map of XPI states for a particular location.
+ *
+ * @param {string} name
+ * The name of the install location.
+ *
+ * @returns {XPIStateLocation?}
+ * (id -> XPIState) or null if there are no add-ons in the location.
+ */
+ getLocation(name) {
+ return this.db.get(name);
+ },
+
+ /**
+ * Get the XPI state for a specific add-on in a location.
+ * If the state is not in our cache, return null.
+ *
+ * @param {string} aLocation
+ * The name of the location where the add-on is installed.
+ * @param {string} aId
+ * The add-on ID
+ *
+ * @returns {XPIState?}
+ * The XPIState entry for the add-on, or null.
+ */
+ getAddon(aLocation, aId) {
+ let location = this.db.get(aLocation);
+ return location && location.get(aId);
+ },
+
+ /**
+ * Find the highest priority location of an add-on by ID and return the
+ * XPIState.
+ * @param {string} aId
+ * The add-on IDa
+ * @param {function} aFilter
+ * An optional filter to apply to install locations. If provided,
+ * addons in locations that do not match the filter are not considered.
+ *
+ * @returns {XPIState?}
+ */
+ findAddon(aId, aFilter = location => true) {
+ // Fortunately the Map iterator returns in order of insertion, which is
+ // also our highest -> lowest priority order.
+ for (let location of this.locations()) {
+ if (!aFilter(location)) {
+ continue;
+ }
+ if (location.has(aId)) {
+ return location.get(aId);
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * Iterates over the list of all enabled add-ons in any location.
+ */
+ *enabledAddons() {
+ for (let location of this.locations()) {
+ for (let entry of location.values()) {
+ if (entry.enabled) {
+ yield entry;
+ }
+ }
+ }
+ },
+
+ /**
+ * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
+ *
+ * @param {DBAddonInternal} aAddon
+ * The add-on to add.
+ */
+ addAddon(aAddon) {
+ aAddon.location.addAddon(aAddon);
+ },
+
+ /**
+ * Save the current state of installed add-ons.
+ */
+ save() {
+ if (!this._jsonFile) {
+ this._jsonFile = new lazy.JSONFile({
+ path: PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ FILE_XPI_STATES
+ ),
+ finalizeAt: AddonManagerPrivate.finalShutdown,
+ compression: "lz4",
+ });
+ this._jsonFile.data = this;
+ }
+
+ this._jsonFile.saveSoon();
+ },
+
+ toJSON() {
+ let data = {};
+ for (let [key, loc] of this.db.entries()) {
+ if (!loc.isTemporary && (loc.size || loc.hasStaged)) {
+ data[key] = loc;
+ }
+ }
+ return data;
+ },
+
+ /**
+ * Remove the XPIState for an add-on and save the new state.
+ *
+ * @param {string} aLocation
+ * The name of the add-on location.
+ * @param {string} aId
+ * The ID of the add-on.
+ *
+ */
+ removeAddon(aLocation, aId) {
+ logger.debug(`Removing XPIState for ${aLocation}: ${aId}`);
+ let location = this.db.get(aLocation);
+ if (location) {
+ location.removeAddon(aId);
+ this.save();
+ }
+ },
+
+ /**
+ * Disable the XPIState for an add-on.
+ *
+ * @param {string} aId
+ * The ID of the add-on.
+ */
+ disableAddon(aId) {
+ logger.debug(`Disabling XPIState for ${aId}`);
+ let state = this.findAddon(aId);
+ if (state) {
+ state.enabled = false;
+ }
+ },
+};
+
+/**
+ * A helper class to manage the lifetime of and interaction with
+ * bootstrap scopes for an add-on.
+ *
+ * @param {Object} addon
+ * The add-on which owns this scope. Should be either an
+ * AddonInternal or XPIState object.
+ */
+class BootstrapScope {
+ constructor(addon) {
+ if (!addon.id || !addon.version || !addon.type) {
+ throw new Error("Addon must include an id, version, and type");
+ }
+
+ this.addon = addon;
+ this.instanceID = null;
+ this.scope = null;
+ this.started = false;
+ }
+
+ /**
+ * Returns a BootstrapScope object for the given add-on. If an active
+ * scope exists, it is returned. Otherwise a new one is created.
+ *
+ * @param {Object} addon
+ * The add-on which owns this scope, as accepted by the
+ * constructor.
+ * @returns {BootstrapScope}
+ */
+ static get(addon) {
+ let scope = XPIProvider.activeAddons.get(addon.id);
+ if (!scope) {
+ scope = new this(addon);
+ }
+ return scope;
+ }
+
+ get file() {
+ return this.addon.file || this.addon._sourceBundle;
+ }
+
+ get runInSafeMode() {
+ return "runInSafeMode" in this.addon
+ ? this.addon.runInSafeMode
+ : canRunInSafeMode(this.addon);
+ }
+
+ /**
+ * Returns state information for use by an AsyncShutdown blocker. If
+ * the wrapped bootstrap scope has a fetchState method, it is called,
+ * and its result returned. If not, returns null.
+ *
+ * @returns {Object|null}
+ */
+ fetchState() {
+ if (this.scope && this.scope.fetchState) {
+ return this.scope.fetchState();
+ }
+ return null;
+ }
+
+ /**
+ * Calls a bootstrap method for an add-on.
+ *
+ * @param {string} aMethod
+ * The name of the bootstrap method to call
+ * @param {integer} aReason
+ * The reason flag to pass to the bootstrap's startup method
+ * @param {Object} [aExtraParams = {}]
+ * An object of additional key/value pairs to pass to the method in
+ * the params argument
+ * @returns {any}
+ * The return value of the bootstrap method.
+ */
+ async callBootstrapMethod(aMethod, aReason, aExtraParams = {}) {
+ let { addon, runInSafeMode } = this;
+ if (
+ Services.appinfo.inSafeMode &&
+ !runInSafeMode &&
+ aMethod !== "uninstall"
+ ) {
+ return null;
+ }
+
+ try {
+ if (!this.scope) {
+ this.loadBootstrapScope(aReason);
+ }
+
+ if (aMethod == "startup" || aMethod == "shutdown") {
+ aExtraParams.instanceID = this.instanceID;
+ }
+
+ let method = undefined;
+ let { scope } = this;
+ try {
+ method = scope[aMethod];
+ } catch (e) {
+ // An exception will be caught if the expected method is not defined.
+ // That will be logged below.
+ }
+
+ if (aMethod == "startup") {
+ this.started = true;
+ } else if (aMethod == "shutdown") {
+ this.started = false;
+
+ // Extensions are automatically deinitialized in the correct order at shutdown.
+ if (aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
+ this._pendingDisable = true;
+ for (let addon of XPIProvider.getDependentAddons(this.addon)) {
+ if (addon.active) {
+ await lazy.XPIDatabase.updateAddonDisabledState(addon);
+ }
+ }
+ }
+ }
+
+ let params = {
+ id: addon.id,
+ version: addon.version,
+ resourceURI: addon.resolvedRootURI,
+ signedState: addon.signedState,
+ temporarilyInstalled: addon.location.isTemporary,
+ builtIn: addon.location.isBuiltin,
+ isSystem: addon.location.isSystem,
+ isPrivileged: addon.isPrivileged,
+ locationHidden: addon.location.hidden,
+ recommendationState: addon.recommendationState,
+ };
+
+ if (aMethod == "startup" && addon.startupData) {
+ params.startupData = addon.startupData;
+ }
+
+ Object.assign(params, aExtraParams);
+
+ let result;
+ if (!method) {
+ logger.warn(
+ `Add-on ${addon.id} is missing bootstrap method ${aMethod}`
+ );
+ } else {
+ logger.debug(
+ `Calling bootstrap method ${aMethod} on ${addon.id} version ${addon.version}`
+ );
+
+ this._beforeCallBootstrapMethod(aMethod, params, aReason);
+
+ try {
+ result = await method.call(scope, params, aReason);
+ } catch (e) {
+ logger.warn(
+ `Exception running bootstrap method ${aMethod} on ${addon.id}`,
+ e
+ );
+ }
+ }
+ return result;
+ } finally {
+ // Extensions are automatically initialized in the correct order at startup.
+ if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) {
+ for (let addon of XPIProvider.getDependentAddons(this.addon)) {
+ lazy.XPIDatabase.updateAddonDisabledState(addon);
+ }
+ }
+ }
+ }
+
+ // No-op method to be overridden by tests.
+ _beforeCallBootstrapMethod() {}
+
+ /**
+ * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
+ * values as constants in the scope.
+ *
+ * @param {integer?} [aReason]
+ * The reason this bootstrap is being loaded, as passed to a
+ * bootstrap method.
+ */
+ loadBootstrapScope(aReason) {
+ this.instanceID = Symbol(this.addon.id);
+ this._pendingDisable = false;
+
+ XPIProvider.activeAddons.set(this.addon.id, this);
+
+ // Mark the add-on as active for the crash reporter before loading.
+ // But not at app startup, since we'll already have added all of our
+ // annotations before starting any loads.
+ if (aReason !== BOOTSTRAP_REASONS.APP_STARTUP) {
+ XPIProvider.addAddonsToCrashReporter();
+ }
+
+ logger.debug(`Loading bootstrap scope from ${this.addon.rootURI}`);
+
+ if (this.addon.isWebExtension) {
+ switch (this.addon.type) {
+ case "extension":
+ case "theme":
+ this.scope = lazy.Extension.getBootstrapScope();
+ break;
+
+ // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
+ case "sitepermission-deprecated":
+ this.scope = lazy.SitePermission.getBootstrapScope();
+ break;
+
+ case "locale":
+ this.scope = lazy.Langpack.getBootstrapScope();
+ break;
+
+ case "dictionary":
+ this.scope = lazy.Dictionary.getBootstrapScope();
+ break;
+
+ default:
+ throw new Error(`Unknown webextension type ${this.addon.type}`);
+ }
+ } else {
+ let loader = AddonManagerPrivate.externalExtensionLoaders.get(
+ this.addon.loader
+ );
+ if (!loader) {
+ throw new Error(`Cannot find loader for ${this.addon.loader}`);
+ }
+
+ this.scope = loader.loadScope(this.addon);
+ }
+ }
+
+ /**
+ * Unloads a bootstrap scope by dropping all references to it and then
+ * updating the list of active add-ons with the crash reporter.
+ */
+ unloadBootstrapScope() {
+ XPIProvider.activeAddons.delete(this.addon.id);
+ XPIProvider.addAddonsToCrashReporter();
+
+ this.scope = null;
+ this.startupPromise = null;
+ this.instanceID = null;
+ }
+
+ /**
+ * Calls the bootstrap scope's startup method, with the given reason
+ * and extra parameters.
+ *
+ * @param {integer} reason
+ * The reason code for the startup call.
+ * @param {Object} [aExtraParams]
+ * Optional extra parameters to pass to the bootstrap method.
+ * @returns {Promise}
+ * Resolves when the startup method has run to completion, rejects
+ * if called late during shutdown.
+ */
+ async startup(reason, aExtraParams) {
+ if (this.shutdownPromise) {
+ await this.shutdownPromise;
+ }
+
+ if (
+ Services.startup.isInOrBeyondShutdownPhase(
+ Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ )
+ ) {
+ let err = new Error(
+ `XPIProvider can't start bootstrap scope for ${this.addon.id} after shutdown was already granted`
+ );
+ logger.warn("BoostrapScope startup failure: ${error}", { error: err });
+ this.startupPromise = Promise.reject(err);
+ } else {
+ this.startupPromise = this.callBootstrapMethod(
+ "startup",
+ reason,
+ aExtraParams
+ );
+ }
+
+ return this.startupPromise;
+ }
+
+ /**
+ * Calls the bootstrap scope's shutdown method, with the given reason
+ * and extra parameters.
+ *
+ * @param {integer} reason
+ * The reason code for the shutdown call.
+ * @param {Object} [aExtraParams]
+ * Optional extra parameters to pass to the bootstrap method.
+ */
+ async shutdown(reason, aExtraParams) {
+ this.shutdownPromise = this._shutdown(reason, aExtraParams);
+ await this.shutdownPromise;
+ this.shutdownPromise = null;
+ }
+
+ async _shutdown(reason, aExtraParams) {
+ await this.startupPromise;
+ return this.callBootstrapMethod("shutdown", reason, aExtraParams);
+ }
+
+ /**
+ * If the add-on is already running, calls its "shutdown" method, and
+ * unloads its bootstrap scope.
+ *
+ * @param {integer} reason
+ * The reason code for the shutdown call.
+ * @param {Object} [aExtraParams]
+ * Optional extra parameters to pass to the bootstrap method.
+ */
+ async disable() {
+ if (this.started) {
+ await this.shutdown(BOOTSTRAP_REASONS.ADDON_DISABLE);
+ // If we disable and re-enable very quickly, it's possible that
+ // the next startup() method will be called immediately after this
+ // shutdown method finishes. This almost never happens outside of
+ // tests. In tests, alas...
+ if (!this.started) {
+ this.unloadBootstrapScope();
+ }
+ }
+ }
+
+ /**
+ * Calls the bootstrap scope's install method, and optionally its
+ * startup method.
+ *
+ * @param {integer} reason
+ * The reason code for the calls.
+ * @param {boolean} [startup = false]
+ * If true, and the add-on is active, calls its startup method
+ * after its install method.
+ * @param {Object} [extraArgs]
+ * Optional extra parameters to pass to the bootstrap method.
+ * @returns {Promise}
+ * Resolves when the startup method has run to completion, if
+ * startup is required.
+ */
+ install(reason = BOOTSTRAP_REASONS.ADDON_INSTALL, startup, extraArgs) {
+ return this._install(reason, false, startup, extraArgs);
+ }
+
+ async _install(reason, callUpdate, startup, extraArgs) {
+ if (callUpdate) {
+ await this.callBootstrapMethod("update", reason, extraArgs);
+ } else {
+ this.callBootstrapMethod("install", reason, extraArgs);
+ }
+
+ if (startup && this.addon.active) {
+ await this.startup(reason, extraArgs);
+ } else if (this.addon.disabled) {
+ this.unloadBootstrapScope();
+ }
+ }
+
+ /**
+ * Calls the bootstrap scope's uninstall method, and unloads its
+ * bootstrap scope. If the extension is already running, its shutdown
+ * method is called before its uninstall method.
+ *
+ * @param {integer} reason
+ * The reason code for the calls.
+ * @param {Object} [extraArgs]
+ * Optional extra parameters to pass to the bootstrap method.
+ * @returns {Promise}
+ * Resolves when the shutdown method has run to completion, if
+ * shutdown is required, and the uninstall method has been
+ * called.
+ */
+ uninstall(reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL, extraArgs) {
+ return this._uninstall(reason, false, extraArgs);
+ }
+
+ async _uninstall(reason, callUpdate, extraArgs) {
+ if (this.started) {
+ await this.shutdown(reason, extraArgs);
+ }
+ if (!callUpdate) {
+ this.callBootstrapMethod("uninstall", reason, extraArgs);
+ }
+ this.unloadBootstrapScope();
+
+ if (this.file) {
+ lazy.XPIInstall.flushJarCache(this.file);
+ }
+ }
+
+ /**
+ * Calls the appropriate sequence of shutdown, uninstall, update,
+ * startup, and install methods for updating the current scope's
+ * add-on to the given new add-on, depending on the current state of
+ * the scope.
+ *
+ * @param {XPIState} newAddon
+ * The new add-on which is being installed, as expected by the
+ * constructor.
+ * @param {boolean} [startup = false]
+ * If true, and the new add-on is enabled, calls its startup
+ * method as its final operation.
+ * @param {function} [updateCallback]
+ * An optional callback function to call between uninstalling
+ * the old add-on and installing the new one. This callback
+ * should update any database state which is necessary for the
+ * startup of the new add-on.
+ * @returns {Promise}
+ * Resolves when all required bootstrap callbacks have
+ * completed.
+ */
+ async update(newAddon, startup = false, updateCallback) {
+ let reason = lazy.XPIInstall.newVersionReason(
+ this.addon.version,
+ newAddon.version
+ );
+
+ let callUpdate = this.addon.isWebExtension && newAddon.isWebExtension;
+
+ // BootstrapScope gets either an XPIState instance or an AddonInternal
+ // instance, when we update, we need the latter to access permissions
+ // from the manifest.
+ let existingAddon = this.addon;
+
+ let extraArgs = {
+ oldVersion: existingAddon.version,
+ newVersion: newAddon.version,
+ };
+
+ // If we're updating an extension, we may need to read data to
+ // calculate permission changes.
+ if (callUpdate && existingAddon.type === "extension") {
+ if (this.addon instanceof XPIState) {
+ // The existing addon will be cached in the database.
+ existingAddon = await lazy.XPIDatabase.getAddonByID(this.addon.id);
+ }
+
+ if (newAddon instanceof XPIState) {
+ newAddon = await lazy.XPIInstall.loadManifestFromFile(
+ newAddon.file,
+ newAddon.location
+ );
+ }
+
+ Object.assign(extraArgs, {
+ userPermissions: newAddon.userPermissions,
+ optionalPermissions: newAddon.optionalPermissions,
+ oldPermissions: existingAddon.userPermissions,
+ oldOptionalPermissions: existingAddon.optionalPermissions,
+ });
+ }
+
+ await this._uninstall(reason, callUpdate, extraArgs);
+
+ if (updateCallback) {
+ await updateCallback();
+ }
+
+ this.addon = newAddon;
+ return this._install(reason, callUpdate, startup, extraArgs);
+ }
+}
+
+let resolveDBReady;
+let dbReadyPromise = new Promise(resolve => {
+ resolveDBReady = resolve;
+});
+let resolveProviderReady;
+let providerReadyPromise = new Promise(resolve => {
+ resolveProviderReady = resolve;
+});
+
+var XPIProvider = {
+ get name() {
+ return "XPIProvider";
+ },
+
+ BOOTSTRAP_REASONS: Object.freeze(BOOTSTRAP_REASONS),
+
+ // A Map of active addons to their bootstrapScope by ID
+ activeAddons: new Map(),
+ // Per-addon telemetry information
+ _telemetryDetails: {},
+ // Have we started shutting down bootstrap add-ons?
+ _closing: false,
+
+ // Promises awaited by the XPIProvider before resolving providerReadyPromise,
+ // (pushed into the array by XPIProvider maybeInstallBuiltinAddon and startup
+ // methods).
+ startupPromises: [],
+
+ // Array of the bootstrap startup promises for the enabled addons being
+ // initiated during the XPIProvider startup.
+ //
+ // NOTE: XPIProvider will wait for these promises (and the startupPromises one)
+ // to have settled before allowing the application to proceed with shutting down
+ // (see quitApplicationGranted blocker at the end of the XPIProvider.startup).
+ enabledAddonsStartupPromises: [],
+
+ databaseReady: Promise.all([dbReadyPromise, providerReadyPromise]),
+
+ // Check if the XPIDatabase has been loaded (without actually
+ // triggering unwanted imports or I/O)
+ get isDBLoaded() {
+ // Make sure we don't touch the XPIDatabase getter before it's
+ // actually loaded, and force an early load.
+ return (
+ (Object.getOwnPropertyDescriptor(lazy, "XPIDatabase").value &&
+ lazy.XPIDatabase.initialized) ||
+ false
+ );
+ },
+
+ /**
+ * Returns true if the add-on with the given ID is currently active,
+ * without forcing the add-ons database to load.
+ *
+ * @param {string} addonId
+ * The ID of the add-on to check.
+ * @returns {boolean}
+ */
+ addonIsActive(addonId) {
+ let state = XPIStates.findAddon(addonId);
+ return state && state.enabled;
+ },
+
+ /**
+ * Returns an array of the add-on values in `enabledAddons`,
+ * sorted so that all of an add-on's dependencies appear in the array
+ * before itself.
+ *
+ * @returns {Array<object>}
+ * A sorted array of add-on objects. Each value is a copy of the
+ * corresponding value in the `enabledAddons` object, with an
+ * additional `id` property, which corresponds to the key in that
+ * object, which is the same as the add-ons ID.
+ */
+ sortBootstrappedAddons() {
+ function compare(a, b) {
+ if (a === b) {
+ return 0;
+ }
+ return a < b ? -1 : 1;
+ }
+
+ // Sort the list so that ordering is deterministic.
+ let list = Array.from(XPIStates.enabledAddons());
+ list.sort((a, b) => compare(a.id, b.id));
+
+ let addons = {};
+ for (let entry of list) {
+ addons[entry.id] = entry;
+ }
+
+ let res = new Set();
+ let seen = new Set();
+
+ let add = addon => {
+ seen.add(addon.id);
+
+ for (let id of addon.dependencies || []) {
+ if (id in addons && !seen.has(id)) {
+ add(addons[id]);
+ }
+ }
+
+ res.add(addon.id);
+ };
+
+ Object.values(addons).forEach(add);
+
+ return Array.from(res, id => addons[id]);
+ },
+
+ /*
+ * Adds metadata to the telemetry payload for the given add-on.
+ */
+ addTelemetry(aId, aPayload) {
+ if (!this._telemetryDetails[aId]) {
+ this._telemetryDetails[aId] = {};
+ }
+ Object.assign(this._telemetryDetails[aId], aPayload);
+ },
+
+ setupInstallLocations(aAppChanged) {
+ function DirectoryLoc(aName, aScope, aKey, aPaths, aLocked, aIsSystem) {
+ try {
+ var dir = lazy.FileUtils.getDir(aKey, aPaths);
+ } catch (e) {
+ return null;
+ }
+ return new DirectoryLocation(aName, dir, aScope, aLocked, aIsSystem);
+ }
+
+ function SystemDefaultsLoc(name, scope, key, paths) {
+ try {
+ var dir = lazy.FileUtils.getDir(key, paths);
+ } catch (e) {
+ return null;
+ }
+ return new SystemAddonDefaults(name, dir, scope);
+ }
+
+ function SystemLoc(aName, aScope, aKey, aPaths) {
+ try {
+ var dir = lazy.FileUtils.getDir(aKey, aPaths);
+ } catch (e) {
+ return null;
+ }
+ return new SystemAddonLocation(aName, dir, aScope, aAppChanged !== false);
+ }
+
+ function RegistryLoc(aName, aScope, aKey) {
+ if ("nsIWindowsRegKey" in Ci) {
+ return new WinRegLocation(aName, Ci.nsIWindowsRegKey[aKey], aScope);
+ }
+ }
+
+ // These must be in order of priority, highest to lowest,
+ // for processFileChanges etc. to work
+ let locations = [
+ [() => TemporaryInstallLocation, TemporaryInstallLocation.name, null],
+
+ [
+ DirectoryLoc,
+ KEY_APP_PROFILE,
+ AddonManager.SCOPE_PROFILE,
+ KEY_PROFILEDIR,
+ [DIR_EXTENSIONS],
+ false,
+ ],
+
+ [
+ DirectoryLoc,
+ KEY_APP_SYSTEM_PROFILE,
+ AddonManager.SCOPE_APPLICATION,
+ KEY_PROFILEDIR,
+ [DIR_APP_SYSTEM_PROFILE],
+ false,
+ true,
+ ],
+
+ [
+ SystemLoc,
+ KEY_APP_SYSTEM_ADDONS,
+ AddonManager.SCOPE_PROFILE,
+ KEY_PROFILEDIR,
+ [DIR_SYSTEM_ADDONS],
+ ],
+
+ [
+ SystemDefaultsLoc,
+ KEY_APP_SYSTEM_DEFAULTS,
+ AddonManager.SCOPE_PROFILE,
+ KEY_APP_FEATURES,
+ [],
+ ],
+
+ [() => BuiltInLocation, KEY_APP_BUILTINS, AddonManager.SCOPE_APPLICATION],
+
+ [
+ DirectoryLoc,
+ KEY_APP_SYSTEM_USER,
+ AddonManager.SCOPE_USER,
+ "XREUSysExt",
+ [Services.appinfo.ID],
+ true,
+ ],
+
+ [
+ RegistryLoc,
+ "winreg-app-user",
+ AddonManager.SCOPE_USER,
+ "ROOT_KEY_CURRENT_USER",
+ ],
+
+ [
+ DirectoryLoc,
+ KEY_APP_GLOBAL,
+ AddonManager.SCOPE_APPLICATION,
+ KEY_ADDON_APP_DIR,
+ [DIR_EXTENSIONS],
+ true,
+ ],
+
+ [
+ DirectoryLoc,
+ KEY_APP_SYSTEM_SHARE,
+ AddonManager.SCOPE_SYSTEM,
+ "XRESysSExtPD",
+ [Services.appinfo.ID],
+ true,
+ ],
+
+ [
+ DirectoryLoc,
+ KEY_APP_SYSTEM_LOCAL,
+ AddonManager.SCOPE_SYSTEM,
+ "XRESysLExtPD",
+ [Services.appinfo.ID],
+ true,
+ ],
+
+ [
+ RegistryLoc,
+ "winreg-app-global",
+ AddonManager.SCOPE_SYSTEM,
+ "ROOT_KEY_LOCAL_MACHINE",
+ ],
+ ];
+
+ for (let [constructor, name, scope, ...args] of locations) {
+ if (!scope || lazy.enabledScopes & scope) {
+ try {
+ let loc = constructor(name, scope, ...args);
+ if (loc) {
+ XPIStates.addLocation(name, loc);
+ }
+ } catch (e) {
+ logger.warn(
+ `Failed to add ${constructor.name} install location ${name}`,
+ e
+ );
+ }
+ }
+ }
+ },
+
+ /**
+ * Registers the built-in set of dictionaries with the spell check
+ * service.
+ */
+ registerBuiltinDictionaries() {
+ this.dictionaries = {};
+ for (let [lang, path] of Object.entries(
+ this.builtInAddons.dictionaries || {}
+ )) {
+ path = path.slice(0, -4) + ".aff";
+ let uri = Services.io.newURI(`resource://gre/${path}`);
+
+ this.dictionaries[lang] = uri;
+ lazy.spellCheck.addDictionary(lang, uri);
+ }
+ },
+
+ /**
+ * Unregisters the dictionaries in the given object, and re-registers
+ * any built-in dictionaries in their place, when they exist.
+ *
+ * @param {Object<nsIURI>} aDicts
+ * An object containing a property with a dictionary language
+ * code and a nsIURI value for each dictionary to be
+ * unregistered.
+ */
+ unregisterDictionaries(aDicts) {
+ let origDicts = lazy.spellCheck.dictionaries.slice();
+ let toRemove = [];
+
+ for (let [lang, uri] of Object.entries(aDicts)) {
+ if (
+ lazy.spellCheck.removeDictionary(lang, uri) &&
+ this.dictionaries.hasOwnProperty(lang)
+ ) {
+ lazy.spellCheck.addDictionary(lang, this.dictionaries[lang]);
+ } else {
+ toRemove.push(lang);
+ }
+ }
+
+ lazy.spellCheck.dictionaries = origDicts.filter(
+ lang => !toRemove.includes(lang)
+ );
+ },
+
+ /**
+ * Starts the XPI provider initializes the install locations and prefs.
+ *
+ * @param {boolean?} aAppChanged
+ * A tri-state value. Undefined means the current profile was created
+ * for this session, true means the profile already existed but was
+ * last used with an application with a different version number,
+ * false means that the profile was last used by this version of the
+ * application.
+ * @param {string?} [aOldAppVersion]
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param {string?} [aOldPlatformVersion]
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ */
+ startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
+ try {
+ AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
+
+ logger.debug("startup");
+
+ this.builtInAddons = {};
+ try {
+ let url = Services.io.newURI(BUILT_IN_ADDONS_URI);
+ let data = Cu.readUTF8URI(url);
+ this.builtInAddons = JSON.parse(data);
+ } catch (e) {
+ if (AppConstants.DEBUG) {
+ logger.debug("List of built-in add-ons is missing or invalid.", e);
+ }
+ }
+
+ this.registerBuiltinDictionaries();
+
+ // Clear this at startup for xpcshell test restarts
+ this._telemetryDetails = {};
+ // Register our details structure with AddonManager
+ AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails);
+
+ this.setupInstallLocations(aAppChanged);
+
+ if (!AppConstants.MOZ_REQUIRE_SIGNING || Cu.isInAutomation) {
+ Services.prefs.addObserver(PREF_XPI_SIGNATURES_REQUIRED, this);
+ }
+ Services.prefs.addObserver(PREF_LANGPACK_SIGNATURES, this);
+ Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS);
+
+ this.checkForChanges(aAppChanged, aOldAppVersion, aOldPlatformVersion);
+
+ AddonManagerPrivate.markProviderSafe(this);
+
+ const lastTheme = Services.prefs.getCharPref(
+ "extensions.activeThemeID",
+ null
+ );
+
+ if (
+ lastTheme === "recommended-1" ||
+ lastTheme === "recommended-2" ||
+ lastTheme === "recommended-3" ||
+ lastTheme === "recommended-4" ||
+ lastTheme === "recommended-5"
+ ) {
+ // The user is using a theme that was once bundled with Firefox, but no longer
+ // is. Clear their theme so that they will be forced to reset to the default.
+ this.startupPromises.push(
+ AddonManagerPrivate.notifyAddonChanged(null, "theme")
+ );
+ }
+ this.maybeInstallBuiltinAddon(
+ "default-theme@mozilla.org",
+ "1.3",
+ "resource://default-theme/"
+ );
+
+ resolveProviderReady(Promise.all(this.startupPromises));
+
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ // Annotate the crash report with relevant add-on information.
+ try {
+ Services.appinfo.annotateCrashReport(
+ "EMCheckCompatibility",
+ AddonManager.checkCompatibility
+ );
+ } catch (e) {}
+ this.addAddonsToCrashReporter();
+ }
+
+ try {
+ AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin");
+
+ for (let addon of this.sortBootstrappedAddons()) {
+ // The startup update check above may have already started some
+ // extensions, make sure not to try to start them twice.
+ let activeAddon = this.activeAddons.get(addon.id);
+ if (activeAddon && activeAddon.started) {
+ continue;
+ }
+ try {
+ let reason = BOOTSTRAP_REASONS.APP_STARTUP;
+ // Eventually set INSTALLED reason when a bootstrap addon
+ // is dropped in profile folder and automatically installed
+ if (
+ AddonManager.getStartupChanges(
+ AddonManager.STARTUP_CHANGE_INSTALLED
+ ).includes(addon.id)
+ ) {
+ reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+ } else if (
+ AddonManager.getStartupChanges(
+ AddonManager.STARTUP_CHANGE_ENABLED
+ ).includes(addon.id)
+ ) {
+ reason = BOOTSTRAP_REASONS.ADDON_ENABLE;
+ }
+ this.enabledAddonsStartupPromises.push(
+ BootstrapScope.get(addon).startup(reason)
+ );
+ } catch (e) {
+ logger.error(
+ "Failed to load bootstrap addon " +
+ addon.id +
+ " from " +
+ addon.descriptor,
+ e
+ );
+ }
+ }
+ AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end");
+ } catch (e) {
+ logger.error("bootstrap startup failed", e);
+ AddonManagerPrivate.recordException(
+ "XPI-BOOTSTRAP",
+ "startup failed",
+ e
+ );
+ }
+
+ // Let these shutdown a little earlier when they still have access to most
+ // of XPCOM
+ lazy.AsyncShutdown.quitApplicationGranted.addBlocker(
+ "XPIProvider shutdown",
+ async () => {
+ // Do not enter shutdown before we actually finished starting as this
+ // can lead to hangs as seen in bug 1814104.
+ await Promise.allSettled([
+ ...this.startupPromises,
+ ...this.enabledAddonsStartupPromises,
+ ]);
+
+ XPIProvider._closing = true;
+
+ await XPIProvider.cleanupTemporaryAddons();
+ for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) {
+ // If no scope has been loaded for this add-on then there is no need
+ // to shut it down (should only happen when a bootstrapped add-on is
+ // pending enable)
+ let activeAddon = XPIProvider.activeAddons.get(addon.id);
+ if (!activeAddon || !activeAddon.started) {
+ continue;
+ }
+
+ // If the add-on was pending disable then shut it down and remove it
+ // from the persisted data.
+ let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
+ if (addon._pendingDisable) {
+ reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
+ } else if (addon.location.name == KEY_APP_TEMPORARY) {
+ reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
+ let existing = XPIStates.findAddon(
+ addon.id,
+ loc => !loc.isTemporary
+ );
+ if (existing) {
+ reason = lazy.XPIInstall.newVersionReason(
+ addon.version,
+ existing.version
+ );
+ }
+ }
+
+ let scope = BootstrapScope.get(addon);
+ let promise = scope.shutdown(reason);
+ lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
+ `Extension shutdown: ${addon.id}`,
+ promise,
+ {
+ fetchState: scope.fetchState.bind(scope),
+ }
+ );
+ }
+ }
+ );
+
+ // Detect final-ui-startup for telemetry reporting
+ Services.obs.addObserver(function observer() {
+ AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup");
+ Services.obs.removeObserver(observer, "final-ui-startup");
+ }, "final-ui-startup");
+
+ // If we haven't yet loaded the XPI database, schedule loading it
+ // to occur once other important startup work is finished. We want
+ // this to happen relatively quickly after startup so the telemetry
+ // environment has complete addon information.
+ //
+ // Unfortunately we have to use a variety of ways do detect when it
+ // is time to load. In a regular browser process we just wait for
+ // sessionstore-windows-restored. In a browser toolbox process
+ // we wait for the toolbox to show up, based on xul-window-visible
+ // and a visible toolbox window.
+ //
+ // TelemetryEnvironment's EnvironmentAddonBuilder awaits databaseReady
+ // before releasing a blocker on AddonManager.beforeShutdown, which in its
+ // turn is a blocker of a shutdown blocker at "profile-before-change".
+ // To avoid a deadlock, trigger the DB load at "profile-before-change" if
+ // the database hasn't started loading yet.
+ //
+ // Finally, we have a test-only event called test-load-xpi-database
+ // as a temporary workaround for bug 1372845. The latter can be
+ // cleaned up when that bug is resolved.
+ if (!this.isDBLoaded) {
+ const EVENTS = [
+ "sessionstore-windows-restored",
+ "xul-window-visible",
+ "profile-before-change",
+ "test-load-xpi-database",
+ ];
+ let observer = (subject, topic, data) => {
+ if (
+ topic == "xul-window-visible" &&
+ !Services.wm.getMostRecentWindow("devtools:toolbox")
+ ) {
+ return;
+ }
+
+ for (let event of EVENTS) {
+ Services.obs.removeObserver(observer, event);
+ }
+
+ lazy.XPIDatabase.asyncLoadDB();
+ };
+ for (let event of EVENTS) {
+ Services.obs.addObserver(observer, event);
+ }
+ }
+
+ AddonManagerPrivate.recordTimestamp("XPI_startup_end");
+
+ lazy.timerManager.registerTimer(
+ "xpi-signature-verification",
+ () => {
+ lazy.XPIDatabase.verifySignatures();
+ },
+ XPI_SIGNATURE_CHECK_PERIOD
+ );
+ } catch (e) {
+ logger.error("startup failed", e);
+ AddonManagerPrivate.recordException("XPI", "startup failed", e);
+ }
+ },
+
+ /**
+ * Shuts down the database and releases all references.
+ * Return: Promise{integer} resolves / rejects with the result of
+ * flushing the XPI Database if it was loaded,
+ * 0 otherwise.
+ */
+ async shutdown() {
+ logger.debug("shutdown");
+
+ this.activeAddons.clear();
+ this.allAppGlobal = true;
+
+ // Stop anything we were doing asynchronously
+ lazy.XPIInstall.cancelAll();
+
+ for (let install of lazy.XPIInstall.installs) {
+ if (install.onShutdown()) {
+ install.onShutdown();
+ }
+ }
+
+ // If there are pending operations then we must update the list of active
+ // add-ons
+ if (Services.prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) {
+ lazy.XPIDatabase.updateActiveAddons();
+ Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
+ }
+
+ await lazy.XPIDatabase.shutdown();
+ },
+
+ cleanupTemporaryAddons() {
+ let promises = [];
+ let tempLocation = TemporaryInstallLocation;
+ for (let [id, addon] of tempLocation.entries()) {
+ tempLocation.delete(id);
+
+ let bootstrap = BootstrapScope.get(addon);
+ let existing = XPIStates.findAddon(id, loc => !loc.isTemporary);
+
+ let cleanup = () => {
+ tempLocation.installer.uninstallAddon(id);
+ tempLocation.removeAddon(id);
+ };
+
+ let promise;
+ if (existing) {
+ promise = bootstrap.update(existing, false, () => {
+ cleanup();
+ lazy.XPIDatabase.makeAddonLocationVisible(id, existing.location);
+ });
+ } else {
+ promise = bootstrap.uninstall().then(cleanup);
+ }
+ lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
+ `Temporary extension shutdown: ${addon.id}`,
+ promise
+ );
+ promises.push(promise);
+ }
+ return Promise.all(promises);
+ },
+
+ /**
+ * Adds a list of currently active add-ons to the next crash report.
+ */
+ addAddonsToCrashReporter() {
+ void (Services.appinfo instanceof Ci.nsICrashReporter);
+ if (!Services.appinfo.annotateCrashReport || Services.appinfo.inSafeMode) {
+ return;
+ }
+
+ let data = Array.from(XPIStates.enabledAddons(), a => a.telemetryKey).join(
+ ","
+ );
+
+ try {
+ Services.appinfo.annotateCrashReport("Add-ons", data);
+ } catch (e) {}
+
+ lazy.TelemetrySession.setAddOns(data);
+ },
+
+ /**
+ * Check the staging directories of install locations for any add-ons to be
+ * installed or add-ons to be uninstalled.
+ *
+ * @param {Object} aManifests
+ * A dictionary to add detected install manifests to for the purpose
+ * of passing through updated compatibility information
+ * @returns {boolean}
+ * True if an add-on was installed or uninstalled
+ */
+ processPendingFileChanges(aManifests) {
+ let changed = false;
+ for (let loc of XPIStates.locations()) {
+ aManifests[loc.name] = {};
+ // We can't install or uninstall anything in locked locations
+ if (loc.locked) {
+ continue;
+ }
+
+ // Collect any install errors for specific removal from the staged directory
+ // during cleanStagingDir. Successful installs remove the files.
+ let stagedFailureNames = [];
+ let promises = [];
+ for (let [id, metadata] of loc.getStagedAddons()) {
+ loc.unstageAddon(id);
+
+ aManifests[loc.name][id] = null;
+ promises.push(
+ lazy.XPIInstall.installStagedAddon(id, metadata, loc).then(
+ addon => {
+ aManifests[loc.name][id] = addon;
+ },
+ error => {
+ delete aManifests[loc.name][id];
+ stagedFailureNames.push(`${id}.xpi`);
+
+ logger.error(
+ `Failed to install staged add-on ${id} in ${loc.name}`,
+ error
+ );
+ }
+ )
+ );
+ }
+
+ if (promises.length) {
+ changed = true;
+ awaitPromise(Promise.all(promises));
+ }
+
+ try {
+ if (changed || stagedFailureNames.length) {
+ loc.installer.cleanStagingDir(stagedFailureNames);
+ }
+ } catch (e) {
+ // Non-critical, just saves some perf on startup if we clean this up.
+ logger.debug("Error cleaning staging dir", e);
+ }
+ }
+ return changed;
+ },
+
+ /**
+ * Installs any add-ons located in the extensions directory of the
+ * application's distribution specific directory into the profile unless a
+ * newer version already exists or the user has previously uninstalled the
+ * distributed add-on.
+ *
+ * @param {Object} aManifests
+ * A dictionary to add new install manifests to to save having to
+ * reload them later
+ * @param {string} [aAppChanged]
+ * See checkForChanges
+ * @param {string?} [aOldAppVersion]
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @returns {boolean}
+ * True if any new add-ons were installed
+ */
+ installDistributionAddons(aManifests, aAppChanged, aOldAppVersion) {
+ let distroDirs = [];
+ try {
+ distroDirs.push(
+ lazy.FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS])
+ );
+ } catch (e) {
+ return false;
+ }
+
+ let availableLocales = [];
+ for (let file of iterDirectory(distroDirs[0])) {
+ if (file.isDirectory() && file.leafName.startsWith("locale-")) {
+ availableLocales.push(file.leafName.replace("locale-", ""));
+ }
+ }
+
+ let locales = Services.locale.negotiateLanguages(
+ Services.locale.requestedLocales,
+ availableLocales,
+ undefined,
+ Services.locale.langNegStrategyMatching
+ );
+
+ // Also install addons from subdirectories that correspond to the requested
+ // locales. This allows for installing language packs and dictionaries.
+ for (let locale of locales) {
+ let langPackDir = distroDirs[0].clone();
+ langPackDir.append(`locale-${locale}`);
+ distroDirs.push(langPackDir);
+ }
+
+ let changed = false;
+ for (let distroDir of distroDirs) {
+ logger.warn(`Checking ${distroDir.path} for addons`);
+ for (let file of iterDirectory(distroDir)) {
+ if (!isXPI(file.leafName, true)) {
+ // Only warn for files, not directories
+ if (!file.isDirectory()) {
+ logger.warn(`Ignoring distribution: not an XPI: ${file.path}`);
+ }
+ continue;
+ }
+
+ let id = getExpectedID(file);
+ if (!id) {
+ logger.warn(
+ `Ignoring distribution: name is not a valid add-on ID: ${file.path}`
+ );
+ continue;
+ }
+
+ /* If this is not an upgrade and we've already handled this extension
+ * just continue */
+ if (
+ !aAppChanged &&
+ Services.prefs.prefHasUserValue(PREF_BRANCH_INSTALLED_ADDON + id)
+ ) {
+ continue;
+ }
+
+ try {
+ let loc = XPIStates.getLocation(KEY_APP_PROFILE);
+ let addon = awaitPromise(
+ lazy.XPIInstall.installDistributionAddon(
+ id,
+ file,
+ loc,
+ aOldAppVersion
+ )
+ );
+
+ if (addon) {
+ // aManifests may contain a copy of a newly installed add-on's manifest
+ // and we'll have overwritten that so instead cache our install manifest
+ // which will later be put into the database in processFileChanges
+ if (!(loc.name in aManifests)) {
+ aManifests[loc.name] = {};
+ }
+ aManifests[loc.name][id] = addon;
+ changed = true;
+ }
+ } catch (e) {
+ logger.error(`Failed to install distribution add-on ${file.path}`, e);
+ }
+ }
+ }
+
+ return changed;
+ },
+
+ /**
+ * Like `installBuiltinAddon`, but only installs the addon at `aBase`
+ * if an existing built-in addon with the ID `aID` and version doesn't
+ * already exist.
+ *
+ * @param {string} aID
+ * The ID of the add-on being registered.
+ * @param {string} aVersion
+ * The version of the add-on being registered.
+ * @param {string} aBase
+ * A string containing the base URL. Must be a resource: URL.
+ * @returns {Promise<Addon>} a Promise that resolves when the addon is installed.
+ */
+ async maybeInstallBuiltinAddon(aID, aVersion, aBase) {
+ let installed;
+ if (lazy.enabledScopes & BuiltInLocation.scope) {
+ let existing = BuiltInLocation.get(aID);
+ if (!existing || existing.version != aVersion) {
+ installed = this.installBuiltinAddon(aBase);
+ this.startupPromises.push(installed);
+ }
+ }
+ return installed;
+ },
+
+ getDependentAddons(aAddon) {
+ return Array.from(lazy.XPIDatabase.getAddons()).filter(addon =>
+ addon.dependencies.includes(aAddon.id)
+ );
+ },
+
+ /**
+ * Checks for any changes that have occurred since the last time the
+ * application was launched.
+ *
+ * @param {boolean?} [aAppChanged]
+ * A tri-state value. Undefined means the current profile was created
+ * for this session, true means the profile already existed but was
+ * last used with an application with a different version number,
+ * false means that the profile was last used by this version of the
+ * application.
+ * @param {string?} [aOldAppVersion]
+ * The version of the application last run with this profile or null
+ * if it is a new profile or the version is unknown
+ * @param {string?} [aOldPlatformVersion]
+ * The version of the platform last run with this profile or null
+ * if it is a new profile or the version is unknown
+ */
+ checkForChanges(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
+ logger.debug("checkForChanges");
+
+ // Keep track of whether and why we need to open and update the database at
+ // startup time.
+ let updateReasons = [];
+ if (aAppChanged) {
+ updateReasons.push("appChanged");
+ }
+
+ let installChanged = XPIStates.scanForChanges(aAppChanged === false);
+ if (installChanged) {
+ updateReasons.push("directoryState");
+ }
+
+ // First install any new add-ons into the locations, if there are any
+ // changes then we must update the database with the information in the
+ // install locations
+ let manifests = {};
+ let updated = this.processPendingFileChanges(manifests);
+ if (updated) {
+ updateReasons.push("pendingFileChanges");
+ }
+
+ // This will be true if the previous session made changes that affect the
+ // active state of add-ons but didn't commit them properly (normally due
+ // to the application crashing)
+ let hasPendingChanges = Services.prefs.getBoolPref(
+ PREF_PENDING_OPERATIONS,
+ false
+ );
+ if (hasPendingChanges) {
+ updateReasons.push("hasPendingChanges");
+ }
+
+ // If the application has changed then check for new distribution add-ons
+ if (Services.prefs.getBoolPref(PREF_INSTALL_DISTRO_ADDONS, true)) {
+ updated = this.installDistributionAddons(
+ manifests,
+ aAppChanged,
+ aOldAppVersion
+ );
+ if (updated) {
+ updateReasons.push("installDistributionAddons");
+ }
+ }
+
+ // If the schema appears to have changed then we should update the database
+ if (DB_SCHEMA != Services.prefs.getIntPref(PREF_DB_SCHEMA, 0)) {
+ // If we don't have any add-ons, just update the pref, since we don't need to
+ // write the database
+ if (!XPIStates.size) {
+ logger.debug(
+ "Empty XPI database, setting schema version preference to " +
+ DB_SCHEMA
+ );
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
+ } else {
+ updateReasons.push("schemaChanged");
+ }
+ }
+
+ // Catch and log any errors during the main startup
+ try {
+ let extensionListChanged = false;
+ // If the database needs to be updated then open it and then update it
+ // from the filesystem
+ if (updateReasons.length) {
+ AddonManagerPrivate.recordSimpleMeasure(
+ "XPIDB_startup_load_reasons",
+ updateReasons
+ );
+ lazy.XPIDatabase.syncLoadDB(false);
+ try {
+ extensionListChanged = lazy.XPIDatabaseReconcile.processFileChanges(
+ manifests,
+ aAppChanged,
+ aOldAppVersion,
+ aOldPlatformVersion,
+ updateReasons.includes("schemaChanged")
+ );
+ } catch (e) {
+ logger.error("Failed to process extension changes at startup", e);
+ }
+ }
+
+ // If the application crashed before completing any pending operations then
+ // we should perform them now.
+ if (extensionListChanged || hasPendingChanges) {
+ lazy.XPIDatabase.updateActiveAddons();
+ return;
+ }
+
+ logger.debug("No changes found");
+ } catch (e) {
+ logger.error("Error during startup file checks", e);
+ }
+ },
+
+ /**
+ * Gets an array of add-ons which were placed in a known install location
+ * prior to startup of the current session, were detected by a directory scan
+ * of those locations, and are currently disabled.
+ *
+ * @returns {Promise<Array<Addon>>}
+ */
+ async getNewSideloads() {
+ if (XPIStates.scanForChanges(false)) {
+ // We detected changes. Update the database to account for them.
+ await lazy.XPIDatabase.asyncLoadDB(false);
+ lazy.XPIDatabaseReconcile.processFileChanges({}, false);
+ lazy.XPIDatabase.updateActiveAddons();
+ }
+
+ let addons = await Promise.all(
+ Array.from(XPIStates.sideLoadedAddons.keys(), id => this.getAddonByID(id))
+ );
+
+ return addons.filter(
+ addon =>
+ addon &&
+ addon.seen === false &&
+ addon.permissions & AddonManager.PERM_CAN_ENABLE
+ );
+ },
+
+ /**
+ * Called to test whether this provider supports installing a particular
+ * mimetype.
+ *
+ * @param {string} aMimetype
+ * The mimetype to check for
+ * @returns {boolean}
+ * True if the mimetype is application/x-xpinstall
+ */
+ supportsMimetype(aMimetype) {
+ return aMimetype == "application/x-xpinstall";
+ },
+
+ // Identify temporary install IDs.
+ isTemporaryInstallID(id) {
+ return id.endsWith(TEMPORARY_ADDON_SUFFIX);
+ },
+
+ /**
+ * Sets startupData for the given addon. The provided data will be stored
+ * in addonsStartup.json so it is available early during browser startup.
+ * Note that this file is read synchronously at startup, so startupData
+ * should be used with care.
+ *
+ * @param {string} aID
+ * The id of the addon to save startup data for.
+ * @param {any} aData
+ * The data to store. Must be JSON serializable.
+ */
+ setStartupData(aID, aData) {
+ let state = XPIStates.findAddon(aID);
+ state.startupData = aData;
+ XPIStates.save();
+ },
+
+ /**
+ * Persists some startupData into an addon if it is available in the current
+ * XPIState for the addon id.
+ *
+ * @param {AddonInternal} addon An addon to receive the startup data, typically an update that is occuring.
+ * @param {XPIState} state optional
+ */
+ persistStartupData(addon, state) {
+ if (!addon.startupData) {
+ state = state || XPIStates.findAddon(addon.id);
+ if (state?.enabled) {
+ // Save persistent listener data if available. It will be
+ // removed later if necessary.
+ let persistentListeners = state.startupData?.persistentListeners;
+ addon.startupData = { persistentListeners };
+ }
+ }
+ },
+
+ getAddonIDByInstanceID(aInstanceID) {
+ if (!aInstanceID || typeof aInstanceID != "symbol") {
+ throw Components.Exception(
+ "aInstanceID must be a Symbol()",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ for (let [id, val] of this.activeAddons) {
+ if (aInstanceID == val.instanceID) {
+ return id;
+ }
+ }
+
+ return null;
+ },
+
+ async getAddonsByTypes(aTypes) {
+ if (aTypes && !aTypes.some(type => ALL_XPI_TYPES.has(type))) {
+ return [];
+ }
+ return lazy.XPIDatabase.getAddonsByTypes(aTypes);
+ },
+
+ /**
+ * Called to get active Addons of a particular type
+ *
+ * @param {Array<string>?} aTypes
+ * An array of types to fetch. Can be null to get all types.
+ * @returns {Promise<Array<Addon>>}
+ */
+ async getActiveAddons(aTypes) {
+ // If we already have the database loaded, returning full info is fast.
+ if (this.isDBLoaded) {
+ let addons = await this.getAddonsByTypes(aTypes);
+ return {
+ addons: addons.filter(addon => addon.isActive),
+ fullData: true,
+ };
+ }
+
+ let result = [];
+ for (let addon of XPIStates.enabledAddons()) {
+ if (aTypes && !aTypes.includes(addon.type)) {
+ continue;
+ }
+ let { scope, isSystem } = addon.location;
+ result.push({
+ id: addon.id,
+ version: addon.version,
+ type: addon.type,
+ updateDate: addon.lastModifiedTime,
+ scope,
+ isSystem,
+ isWebExtension: addon.isWebExtension,
+ });
+ }
+
+ return { addons: result, fullData: false };
+ },
+
+ /*
+ * Notified when a preference we're interested in has changed.
+ *
+ * @see nsIObserver
+ */
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case NOTIFICATION_FLUSH_PERMISSIONS:
+ if (!aData || aData == XPI_PERMISSION) {
+ lazy.XPIDatabase.importPermissions();
+ }
+ break;
+
+ case "nsPref:changed":
+ switch (aData) {
+ case PREF_XPI_SIGNATURES_REQUIRED:
+ case PREF_LANGPACK_SIGNATURES:
+ lazy.XPIDatabase.updateAddonAppDisabledStates();
+ break;
+ }
+ }
+ },
+
+ uninstallSystemProfileAddon(aID) {
+ let location = XPIStates.getLocation(KEY_APP_SYSTEM_PROFILE);
+ return lazy.XPIInstall.uninstallAddonFromLocation(aID, location);
+ },
+};
+
+for (let meth of [
+ "getInstallForFile",
+ "getInstallForURL",
+ "getInstallsByTypes",
+ "installTemporaryAddon",
+ "installBuiltinAddon",
+ "isInstallAllowed",
+ "isInstallEnabled",
+ "updateSystemAddons",
+ "stageLangpacksForAppUpdate",
+]) {
+ XPIProvider[meth] = function () {
+ return lazy.XPIInstall[meth](...arguments);
+ };
+}
+
+for (let meth of [
+ "addonChanged",
+ "getAddonByID",
+ "getAddonBySyncGUID",
+ "updateAddonRepositoryData",
+ "updateAddonAppDisabledStates",
+]) {
+ XPIProvider[meth] = function () {
+ return lazy.XPIDatabase[meth](...arguments);
+ };
+}
+
+var XPIInternal = {
+ BOOTSTRAP_REASONS,
+ BootstrapScope,
+ BuiltInLocation,
+ DB_SCHEMA,
+ DIR_STAGE,
+ DIR_TRASH,
+ KEY_APP_PROFILE,
+ KEY_APP_SYSTEM_PROFILE,
+ KEY_APP_SYSTEM_ADDONS,
+ KEY_APP_SYSTEM_DEFAULTS,
+ PREF_BRANCH_INSTALLED_ADDON,
+ PREF_SYSTEM_ADDON_SET,
+ SystemAddonLocation,
+ TEMPORARY_ADDON_SUFFIX,
+ TemporaryInstallLocation,
+ XPIStates,
+ XPI_PERMISSION,
+ awaitPromise,
+ canRunInSafeMode,
+ getURIForResourceInFile,
+ isXPI,
+ iterDirectory,
+ maybeResolveURI,
+ migrateAddonLoader,
+ resolveDBReady,
+
+ // Used by tests to shut down AddonManager.
+ overrideAsyncShutdown(mockAsyncShutdown) {
+ lazy.AsyncShutdown = mockAsyncShutdown;
+ },
+};
+
+AddonManagerPrivate.registerProvider(XPIProvider, Array.from(ALL_XPI_TYPES));