Source code

Revision control

Copy as Markdown

Other Tools

/* 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 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.sys.mjs or XPIDatabase.sys.mjs 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"}}] */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { XPIExports } from "resource://gre/modules/addons/XPIExports.sys.mjs";
import {
AddonManager,
AddonManagerPrivate,
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
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 = 36;
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;
import { Log } from "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.sys.mjs)
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.sys.mjs: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.sys.mjs.
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 (XPIExports.XPIDatabase.initialized) {
XPIExports.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 XPIExports.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 XPIExports.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 = () => 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 XPIExports.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)) {
XPIExports.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) {
XPIExports.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 = XPIExports.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 XPIExports.XPIDatabase.getAddonByID(
this.addon.id
);
}
if (newAddon instanceof XPIState) {
newAddon = await XPIExports.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;
});
export 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]),
registerProvider() {
AddonManagerPrivate.registerProvider(this, Array.from(ALL_XPI_TYPES));
},
// 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(XPIExports, "XPIDatabase").value &&
XPIExports.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",
);
resolveProviderReady(Promise.all(this.startupPromises));
if (AppConstants.MOZ_CRASHREPORTER) {
// Annotate the crash report with relevant add-on information.
try {
// The `EMCheckCompatibility` annotation represents a boolean, but
// we've historically set it as a string so keep doing it for the
// time being.
Services.appinfo.annotateCrashReport(
"EMCheckCompatibility",
AddonManager.checkCompatibility.toString()
);
} 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 = XPIExports.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) => {
if (
topic == "xul-window-visible" &&
!Services.wm.getMostRecentWindow("devtools:toolbox")
) {
return;
}
for (let event of EVENTS) {
Services.obs.removeObserver(observer, event);
}
XPIExports.XPIDatabase.asyncLoadDB();
};
for (let event of EVENTS) {
Services.obs.addObserver(observer, event);
}
}
AddonManagerPrivate.recordTimestamp("XPI_startup_end");
lazy.timerManager.registerTimer(
"xpi-signature-verification",
() => {
XPIExports.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
XPIExports.XPIInstall.cancelAll();
for (let install of XPIExports.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)) {
XPIExports.XPIDatabase.updateActiveAddons();
Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
}
await XPIExports.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();
XPIExports.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(
XPIExports.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(
XPIExports.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(XPIExports.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
);
XPIExports.XPIDatabase.syncLoadDB(false);
try {
extensionListChanged =
XPIExports.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) {
XPIExports.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 XPIExports.XPIDatabase.asyncLoadDB(false);
XPIExports.XPIDatabaseReconcile.processFileChanges({}, false);
XPIExports.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 XPIExports.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) {
XPIExports.XPIDatabase.importPermissions();
}
break;
case "nsPref:changed":
switch (aData) {
case PREF_XPI_SIGNATURES_REQUIRED:
case PREF_LANGPACK_SIGNATURES:
XPIExports.XPIDatabase.updateAddonAppDisabledStates();
break;
}
}
},
uninstallSystemProfileAddon(aID) {
let location = XPIStates.getLocation(KEY_APP_SYSTEM_PROFILE);
return XPIExports.XPIInstall.uninstallAddonFromLocation(aID, location);
},
};
for (let meth of [
"getInstallForFile",
"getInstallForURL",
"getInstallsByTypes",
"installTemporaryAddon",
"installBuiltinAddon",
"isInstallAllowed",
"isInstallEnabled",
"updateSystemAddons",
"stageLangpacksForAppUpdate",
]) {
XPIProvider[meth] = function () {
return XPIExports.XPIInstall[meth](...arguments);
};
}
for (let meth of [
"addonChanged",
"getAddonByID",
"getAddonBySyncGUID",
"updateAddonRepositoryData",
"updateAddonAppDisabledStates",
]) {
XPIProvider[meth] = function () {
return XPIExports.XPIDatabase[meth](...arguments);
};
}
export 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;
},
};