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/. */
"use strict";
/**
* This file contains most of the logic required to maintain the
* extensions database, including querying and modifying extension
* metadata. In general, we try to avoid loading it during startup when
* at all possible. Please keep that in mind when deciding whether to
* add code here or elsewhere.
*/
/* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */
var EXPORTED_SYMBOLS = [
"AddonInternal",
"BuiltInThemesHelpers",
"XPIDatabase",
"XPIDatabaseReconcile",
];
const { XPCOMUtils } = ChromeUtils.importESModule(
);
const lazy = {};
XPCOMUtils.defineLazyServiceGetters(lazy, {
ThirdPartyUtil: ["@mozilla.org/thirdpartyutil;1", "mozIThirdPartyUtil"],
});
ChromeUtils.defineESModuleGetters(lazy, {
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
verifyBundleSignedState: "resource://gre/modules/addons/XPIInstall.jsm",
});
// WARNING: BuiltInThemes.sys.mjs may be provided by the host application (e.g.
// Firefox), or it might not exist at all. Use with caution, as we don't
// want things to completely fail if that module can't be loaded.
XPCOMUtils.defineLazyGetter(lazy, "BuiltInThemes", () => {
try {
let { BuiltInThemes } = ChromeUtils.importESModule(
);
return BuiltInThemes;
} catch (e) {
Cu.reportError(`Unable to load BuiltInThemes.sys.mjs: ${e}`);
}
return undefined;
});
// A set of helpers to account from a single place that in some builds
// (e.g. GeckoView and Thunderbird) the BuiltInThemes module may either
// not be bundled at all or not be exposing the same methods provided
// by the module as defined in Firefox Desktop.
const BuiltInThemesHelpers = {
getLocalizedColorwayGroupName(addonId) {
return lazy.BuiltInThemes?.getLocalizedColorwayGroupName?.(addonId);
},
getLocalizedColorwayDescription(addonId) {
return lazy.BuiltInThemes?.getLocalizedColorwayGroupDescription?.(addonId);
},
isActiveTheme(addonId) {
return lazy.BuiltInThemes?.isActiveTheme?.(addonId);
},
isRetainedExpiredTheme(addonId) {
return lazy.BuiltInThemes?.isRetainedExpiredTheme?.(addonId);
},
themeIsExpired(addonId) {
return lazy.BuiltInThemes?.themeIsExpired?.(addonId);
},
// Helper function called form XPInstall.jsm to remove from the retained themes
// list the built-in colorways theme that have been migrated to a non built-in.
unretainMigratedColorwayTheme(addonId) {
lazy.BuiltInThemes?.unretainMigratedColorwayTheme?.(addonId);
},
};
XPCOMUtils.defineLazyPreferenceGetter(
BuiltInThemesHelpers,
"isColorwayMigrationEnabled",
"browser.theme.colorway-migration",
false
);
// A temporary hidden pref just meant to be used as a last resort, in case
// we need to force-disable the "per-addon quarantined domains user controls"
// feature during the beta cycle, e.g. if unexpected issues are caught late and
// it shouldn't ride the train.
//
// TODO(Bug 1839616): remove this pref after the user controls features have been
// released.
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"isQuarantineUIDisabled",
"extensions.quarantinedDomains.uiDisabled",
false
);
const { nsIBlocklistService } = Ci;
const { Log } = ChromeUtils.importESModule(
);
const LOGGER_ID = "addons.xpi-utils";
const nsIFile = Components.Constructor(
"@mozilla.org/file/local;1",
"nsIFile",
"initWithPath"
);
// Create a new logger for use by the Addons XPI Provider Utils
// (Requires AddonManager.jsm)
var logger = Log.repository.getLogger(LOGGER_ID);
const FILE_JSON_DB = "extensions.json";
const PREF_DB_SCHEMA = "extensions.databaseSchema";
const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes";
const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall.";
const PREF_XPI_SIGNATURES_DEV_ROOT = "xpinstall.signatures.dev-root";
const TOOLKIT_ID = "toolkit@mozilla.org";
const KEY_APP_SYSTEM_ADDONS = "app-system-addons";
const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults";
const KEY_APP_SYSTEM_PROFILE = "app-system-profile";
const KEY_APP_BUILTINS = "app-builtin";
const KEY_APP_SYSTEM_LOCAL = "app-system-local";
const KEY_APP_SYSTEM_SHARE = "app-system-share";
const KEY_APP_GLOBAL = "app-global";
const KEY_APP_PROFILE = "app-profile";
const KEY_APP_TEMPORARY = "app-temporary";
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
// Properties to cache and reload when an addon installation is pending
const PENDING_INSTALL_METADATA = [
"syncGUID",
"targetApplications",
"userDisabled",
"softDisabled",
"embedderDisabled",
"sourceURI",
"releaseNotesURI",
"installDate",
"updateDate",
"applyBackgroundUpdates",
"installTelemetryInfo",
];
// Properties to save in JSON file
const PROP_JSON_FIELDS = [
"id",
"syncGUID",
"version",
"type",
"loader",
"updateURL",
"installOrigins",
"manifestVersion",
"optionsURL",
"optionsType",
"optionsBrowserStyle",
"aboutURL",
"defaultLocale",
"visible",
"active",
"userDisabled",
"appDisabled",
"embedderDisabled",
"pendingUninstall",
"installDate",
"updateDate",
"applyBackgroundUpdates",
"path",
"skinnable",
"sourceURI",
"releaseNotesURI",
"softDisabled",
"foreignInstall",
"strictCompatibility",
"locales",
"targetApplications",
"targetPlatforms",
"signedState",
"signedDate",
"seen",
"dependencies",
"incognito",
"userPermissions",
"optionalPermissions",
"sitePermissions",
"siteOrigin",
"icons",
"iconURL",
"blocklistState",
"blocklistURL",
"startupData",
"previewImage",
"hidden",
"installTelemetryInfo",
"recommendationState",
"rootURI",
];
const SIGNED_TYPES = new Set([
"extension",
"locale",
"theme",
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
"sitepermission-deprecated",
]);
// Time to wait before async save of XPI JSON database, in milliseconds
const ASYNC_SAVE_DELAY_MS = 20;
const l10n = new Localization(["browser/appExtensionFields.ftl"], true);
/**
* Schedules an idle task, and returns a promise which resolves to an
* IdleDeadline when an idle slice is available. The caller should
* perform all of its idle work in the same micro-task, before the
* deadline is reached.
*
* @returns {Promise<IdleDeadline>}
*/
function promiseIdleSlice() {
return new Promise(resolve => {
ChromeUtils.idleDispatch(resolve);
});
}
let arrayForEach = Function.call.bind(Array.prototype.forEach);
/**
* Loops over the given array, in the same way as Array forEach, but
* splitting the work among idle tasks.
*
* @param {Array} array
* The array to loop over.
* @param {function} func
* The function to call on each array element.
* @param {integer} [taskTimeMS = 5]
* The minimum time to allocate to each task. If less time than
* this is available in a given idle slice, and there are more
* elements to loop over, they will be deferred until the next
* idle slice.
*/
async function idleForEach(array, func, taskTimeMS = 5) {
let deadline;
for (let i = 0; i < array.length; i++) {
if (!deadline || deadline.timeRemaining() < taskTimeMS) {
deadline = await promiseIdleSlice();
}
func(array[i], i);
}
}
/**
* Asynchronously fill in the _repositoryAddon field for one addon
*
* @param {AddonInternal} aAddon
* The add-on to annotate.
* @returns {AddonInternal}
* The annotated add-on.
*/
async function getRepositoryAddon(aAddon) {
if (aAddon) {
aAddon._repositoryAddon = await lazy.AddonRepository.getCachedAddonByID(
aAddon.id
);
}
return aAddon;
}
/**
* Copies properties from one object to another. If no target object is passed
* a new object will be created and returned.
*
* @param {object} aObject
* An object to copy from
* @param {string[]} aProperties
* An array of properties to be copied
* @param {object?} [aTarget]
* An optional target object to copy the properties to
* @returns {Object}
* The object that the properties were copied onto
*/
function copyProperties(aObject, aProperties, aTarget) {
if (!aTarget) {
aTarget = {};
}
aProperties.forEach(function (aProp) {
if (aProp in aObject) {
aTarget[aProp] = aObject[aProp];
}
});
return aTarget;
}
// Maps instances of AddonInternal to AddonWrapper
const wrapperMap = new WeakMap();
let addonFor = wrapper => wrapperMap.get(wrapper);
const EMPTY_ARRAY = Object.freeze([]);
let AddonWrapper;
/**
* The AddonInternal is an internal only representation of add-ons. It
* may have come from the database or an extension manifest.
*/
class AddonInternal {
constructor(addonData) {
this._wrapper = null;
this._selectedLocale = null;
this.active = false;
this.visible = false;
this.userDisabled = false;
this.appDisabled = false;
this.softDisabled = false;
this.embedderDisabled = false;
this.blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
this.blocklistURL = null;
this.sourceURI = null;
this.releaseNotesURI = null;
this.foreignInstall = false;
this.seen = true;
this.skinnable = false;
this.startupData = null;
this._hidden = false;
this.installTelemetryInfo = null;
this.rootURI = null;
this._updateInstall = null;
this.recommendationState = null;
this.inDatabase = false;
/**
* @property {Array<string>} dependencies
* An array of bootstrapped add-on IDs on which this add-on depends.
* The add-on will remain appDisabled if any of the dependent
* add-ons is not installed and enabled.
*/
this.dependencies = EMPTY_ARRAY;
if (addonData) {
copyProperties(addonData, PROP_JSON_FIELDS, this);
this.location = addonData.location;
if (!this.dependencies) {
this.dependencies = [];
}
Object.freeze(this.dependencies);
if (this.location) {
this.addedToDatabase();
}
this.sourceBundle = addonData._sourceBundle;
}
}
get sourceBundle() {
return this._sourceBundle;
}
set sourceBundle(file) {
this._sourceBundle = file;
if (file) {
this.rootURI = lazy.XPIInternal.getURIForResourceInFile(file, "").spec;
}
}
get wrapper() {
if (!this._wrapper) {
this._wrapper = new AddonWrapper(this);
}
return this._wrapper;
}
get resolvedRootURI() {
return lazy.XPIInternal.maybeResolveURI(Services.io.newURI(this.rootURI));
}
get isBuiltinColorwayTheme() {
return (
this.type === "theme" &&
this.location.isBuiltin &&
this.id.endsWith("-colorway@mozilla.org")
);
}
/**
* Validate a list of origins are contained in the installOrigins array (defined in manifest.json).
*
* SitePermission addons are a special case, where the triggering install site may be a subdomain
* of a valid xpi origin.
*
* @param {Object} origins Object containing URIs related to install.
* @params {nsIURI} origins.installFrom The nsIURI of the website that has triggered the install flow.
* @params {nsIURI} origins.source The nsIURI where the xpi is hosted.
* @returns {boolean}
*/
validInstallOrigins({ installFrom, source }) {
if (
!Services.prefs.getBoolPref("extensions.install_origins.enabled", true)
) {
return true;
}
let { installOrigins, manifestVersion } = this;
if (!installOrigins) {
// Install origins are mandatory in MV3 and optional
// in MV2. Old addons need to keep installing per the
// old install flow.
return manifestVersion < 3;
}
// An empty install_origins prevents any install from 3rd party websites.
if (!installOrigins.length) {
return false;
}
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
if (this.type == "sitepermission-deprecated") {
// NOTE: This may move into a check for all addons later.
for (let origin of installOrigins) {
let host = new URL(origin).host;
// install_origin cannot be on a known etld (e.g. github.io).
if (Services.eTLD.getKnownPublicSuffixFromHost(host) == host) {
logger.warn(
`Addon ${this.id} Installation not allowed from the install_origin ${host} that is an eTLD`
);
return false;
}
}
if (!installOrigins.includes(new URL(source.spec).origin)) {
logger.warn(
`Addon ${this.id} Installation not allowed, "${source.spec}" is not included in the Addon install_origins`
);
return false;
}
if (lazy.ThirdPartyUtil.isThirdPartyURI(source, installFrom)) {
logger.warn(
`Addon ${this.id} Installation not allowed, installFrom "${installFrom.spec}" is third party to the Addon install_origins`
);
return false;
}
return true;
}
for (const [name, uri] of Object.entries({ installFrom, source })) {
if (!installOrigins.includes(new URL(uri.spec).origin)) {
logger.warn(
`Addon ${this.id} Installation not allowed, ${name} "${uri.spec}" is not included in the Addon install_origins`
);
return false;
}
}
return true;
}
addedToDatabase() {
this._key = `${this.location.name}:${this.id}`;
this.inDatabase = true;
}
get isWebExtension() {
return this.loader == null;
}
get selectedLocale() {
if (this._selectedLocale) {
return this._selectedLocale;
}
/**
* this.locales is a list of objects that have property `locales`.
* It's value is an array of locale codes.
*
* First, we reduce this nested structure to a flat list of locale codes.
*/
const locales = [].concat(...this.locales.map(loc => loc.locales));
let requestedLocales = Services.locale.requestedLocales;
/**
* If en-US is not in the list, add it as the last fallback.
*/
if (!requestedLocales.includes("en-US")) {
requestedLocales.push("en-US");
}
/**
* Then we negotiate best locale code matching the app locales.
*/
let bestLocale = Services.locale.negotiateLanguages(
requestedLocales,
locales,
"und",
Services.locale.langNegStrategyLookup
)[0];
/**
* If no match has been found, we'll assign the default locale as
* the selected one.
*/
if (bestLocale === "und") {
this._selectedLocale = this.defaultLocale;
} else {
/**
* Otherwise, we'll go through all locale entries looking for the one
* that has the best match in it's locales list.
*/
this._selectedLocale = this.locales.find(loc =>
loc.locales.includes(bestLocale)
);
}
return this._selectedLocale;
}
get providesUpdatesSecurely() {
return !this.updateURL || this.updateURL.startsWith("https:");
}
get isCorrectlySigned() {
switch (this.location.name) {
case KEY_APP_SYSTEM_PROFILE:
// Add-ons installed via Normandy must be signed by the system
// key or the "Mozilla Extensions" key.
return [
lazy.AddonManager.SIGNEDSTATE_SYSTEM,
lazy.AddonManager.SIGNEDSTATE_PRIVILEGED,
].includes(this.signedState);
case KEY_APP_SYSTEM_ADDONS:
// System add-ons must be signed by the system key.
return this.signedState == lazy.AddonManager.SIGNEDSTATE_SYSTEM;
case KEY_APP_SYSTEM_DEFAULTS:
case KEY_APP_BUILTINS:
case KEY_APP_TEMPORARY:
// Temporary and built-in add-ons do not require signing.
return true;
case KEY_APP_SYSTEM_SHARE:
case KEY_APP_SYSTEM_LOCAL:
// On UNIX platforms except OSX, an additional location for system
// add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons
// installed there do not require signing.
if (Services.appinfo.OS != "Darwin") {
return true;
}
break;
}
if (this.signedState === lazy.AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
return true;
}
return this.signedState > lazy.AddonManager.SIGNEDSTATE_MISSING;
}
get isCompatible() {
return this.isCompatibleWith();
}
get isPrivileged() {
return lazy.ExtensionData.getIsPrivileged({
signedState: this.signedState,
builtIn: this.location.isBuiltin,
temporarilyInstalled: this.location.isTemporary,
});
}
get hidden() {
return (
this.location.hidden ||
// The hidden flag is intended to only be used for features that are part
// of the application. Temporary add-ons should not be hidden.
(this._hidden && this.isPrivileged && !this.location.isTemporary) ||
false
);
}
set hidden(val) {
this._hidden = val;
}
get disabled() {
return (
this.userDisabled ||
this.appDisabled ||
this.softDisabled ||
this.embedderDisabled
);
}
get isPlatformCompatible() {
if (!this.targetPlatforms.length) {
return true;
}
let matchedOS = false;
// If any targetPlatform matches the OS and contains an ABI then we will
// only match a targetPlatform that contains both the current OS and ABI
let needsABI = false;
// Some platforms do not specify an ABI, test against null in that case.
let abi = null;
try {
abi = Services.appinfo.XPCOMABI;
} catch (e) {}
// Something is causing errors in here
try {
for (let platform of this.targetPlatforms) {
if (platform.os == Services.appinfo.OS) {
if (platform.abi) {
needsABI = true;
if (platform.abi === abi) {
return true;
}
} else {
matchedOS = true;
}
}
}
} catch (e) {
let message =
"Problem with addon " +
this.id +
" targetPlatforms " +
JSON.stringify(this.targetPlatforms);
logger.error(message, e);
lazy.AddonManagerPrivate.recordException("XPI", message, e);
// don't trust this add-on
return false;
}
return matchedOS && !needsABI;
}
isCompatibleWith(aAppVersion, aPlatformVersion) {
let app = this.matchingTargetApplication;
if (!app) {
return false;
}
// set reasonable defaults for minVersion and maxVersion
let minVersion = app.minVersion || "0";
let maxVersion = app.maxVersion || "*";
if (!aAppVersion) {
aAppVersion = Services.appinfo.version;
}
if (!aPlatformVersion) {
aPlatformVersion = Services.appinfo.platformVersion;
}
let version;
if (app.id == Services.appinfo.ID) {
version = aAppVersion;
} else if (app.id == TOOLKIT_ID) {
version = aPlatformVersion;
}
// Only extensions and dictionaries can be compatible by default; themes
// and language packs always use strict compatibility checking.
// Dictionaries are compatible by default unless requested by the dictinary.
if (
!this.strictCompatibility &&
(!lazy.AddonManager.strictCompatibility || this.type == "dictionary")
) {
return Services.vc.compare(version, minVersion) >= 0;
}
return (
Services.vc.compare(version, minVersion) >= 0 &&
Services.vc.compare(version, maxVersion) <= 0
);
}
get matchingTargetApplication() {
let app = null;
for (let targetApp of this.targetApplications) {
if (targetApp.id == Services.appinfo.ID) {
return targetApp;
}
if (targetApp.id == TOOLKIT_ID) {
app = targetApp;
}
}
return app;
}
async findBlocklistEntry() {
return lazy.Blocklist.getAddonBlocklistEntry(this.wrapper);
}
async updateBlocklistState(options = {}) {
if (this.location.isSystem || this.location.isBuiltin) {
return;
}
let { applySoftBlock = true, updateDatabase = true } = options;
let oldState = this.blocklistState;
let entry = await this.findBlocklistEntry();
let newState = entry ? entry.state : Services.blocklist.STATE_NOT_BLOCKED;
this.blocklistState = newState;
this.blocklistURL = entry && entry.url;
let userDisabled, softDisabled;
// After a blocklist update, the blocklist service manually applies
// new soft blocks after displaying a UI, in which cases we need to
// skip updating it here.
if (applySoftBlock && oldState != newState) {
if (newState == Services.blocklist.STATE_SOFTBLOCKED) {
if (this.type == "theme") {
userDisabled = true;
} else {
softDisabled = !this.userDisabled;
}
} else {
softDisabled = false;
}
}
if (this.inDatabase && updateDatabase) {
await XPIDatabase.updateAddonDisabledState(this, {
userDisabled,
softDisabled,
});
XPIDatabase.saveChanges();
} else {
this.appDisabled = !XPIDatabase.isUsableAddon(this);
if (userDisabled !== undefined) {
this.userDisabled = userDisabled;
}
if (softDisabled !== undefined) {
this.softDisabled = softDisabled;
}
}
}
recordAddonBlockChangeTelemetry(reason) {
lazy.Blocklist.recordAddonBlockChangeTelemetry(this.wrapper, reason);
}
async setUserDisabled(val, allowSystemAddons = false) {
if (val == (this.userDisabled || this.softDisabled)) {
return;
}
if (this.inDatabase) {
// System add-ons should not be user disabled, as there is no UI to
// re-enable them.
if (this.location.isSystem && !allowSystemAddons) {
throw new Error(`Cannot disable system add-on ${this.id}`);
}
await XPIDatabase.updateAddonDisabledState(this, { userDisabled: val });
} else {
this.userDisabled = val;
// When enabling remove the softDisabled flag
if (!val) {
this.softDisabled = false;
}
}
}
applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
let wasCompatible = this.isCompatible;
for (let targetApp of this.targetApplications) {
for (let updateTarget of aUpdate.targetApplications) {
if (
targetApp.id == updateTarget.id &&
(aSyncCompatibility ||
Services.vc.compare(targetApp.maxVersion, updateTarget.maxVersion) <
0)
) {
targetApp.minVersion = updateTarget.minVersion;
targetApp.maxVersion = updateTarget.maxVersion;
if (this.inDatabase) {
XPIDatabase.saveChanges();
}
}
}
}
if (wasCompatible != this.isCompatible) {
if (this.inDatabase) {
XPIDatabase.updateAddonDisabledState(this);
} else {
this.appDisabled = !XPIDatabase.isUsableAddon(this);
}
}
}
toJSON() {
let obj = copyProperties(this, PROP_JSON_FIELDS);
obj.location = this.location.name;
return obj;
}
/**
* When an add-on install is pending its metadata will be cached in a file.
* This method reads particular properties of that metadata that may be newer
* than that in the extension manifest, like compatibility information.
*
* @param {Object} aObj
* A JS object containing the cached metadata
*/
importMetadata(aObj) {
for (let prop of PENDING_INSTALL_METADATA) {
if (!(prop in aObj)) {
continue;
}
this[prop] = aObj[prop];
}
// Compatibility info may have changed so update appDisabled
this.appDisabled = !XPIDatabase.isUsableAddon(this);
}
permissions() {
let permissions = 0;
// Add-ons that aren't installed cannot be modified in any way
if (!this.inDatabase) {
return permissions;
}
if (!this.appDisabled) {
if (this.userDisabled || this.softDisabled) {
permissions |= lazy.AddonManager.PERM_CAN_ENABLE;
} else if (this.type != "theme" || this.id != DEFAULT_THEME_ID) {
// We do not expose disabling the default theme.
permissions |= lazy.AddonManager.PERM_CAN_DISABLE;
}
}
// Add-ons that are in locked install locations, or are pending uninstall
// cannot be uninstalled or upgraded. One caveat is extensions sideloaded
// from non-profile locations. Since Firefox 73(?), new sideloaded extensions
// from outside the profile have not been installed so any such extensions
// must be from an older profile. Users may uninstall such an extension which
// removes the related state from this profile but leaves the actual file alone
// (since it is outside this profile and may be in use in other profiles)
let changesAllowed = !this.location.locked && !this.pendingUninstall;
if (changesAllowed) {
// System add-on upgrades are triggered through a different mechanism (see updateSystemAddons())
// Builtin addons are only upgraded with Firefox (or app) updates.
let isSystem = this.location.isSystem || this.location.isBuiltin;
// Add-ons that are installed by a file link cannot be upgraded.
if (!isSystem && !this.location.isLinkedAddon(this.id)) {
permissions |= lazy.AddonManager.PERM_CAN_UPGRADE;
}
// Allow active and retained colorways builtin themes to be updated to the same theme hosted on AMO
// (the PERM_CAN_UPGRADE permission will ensure we will be asking AMO for an update,
// then the AMO addon xpi will be installed in the profile location, overridden in
// the `createUpdate` defined in `XPIInstall.jsm` and called from `UpdateChecker`
// `onUpdateCheckComplete` method).
if (
this.isBuiltinColorwayTheme &&
BuiltInThemesHelpers.isColorwayMigrationEnabled &&
BuiltInThemesHelpers.themeIsExpired(this.id) &&
(BuiltInThemesHelpers.isActiveTheme(this.id) ||
BuiltInThemesHelpers.isRetainedExpiredTheme(this.id))
) {
permissions |= lazy.AddonManager.PERM_CAN_UPGRADE;
}
}
// We allow uninstall of legacy sideloaded extensions, even when in locked locations,
// but we do not remove the addon file in that case.
let isLegacySideload =
this.foreignInstall &&
!(this.location.scope & lazy.AddonSettings.SCOPES_SIDELOAD);
if (changesAllowed || isLegacySideload) {
permissions |= lazy.AddonManager.PERM_API_CAN_UNINSTALL;
if (!this.location.isBuiltin) {
permissions |= lazy.AddonManager.PERM_CAN_UNINSTALL;
}
}
// The permission to "toggle the private browsing access" is locked down
// when the extension has opted out or it gets the permission automatically
// on every extension startup (as system, privileged and builtin addons).
if (
(this.type === "extension" ||
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
this.type == "sitepermission-deprecated") &&
this.incognito !== "not_allowed" &&
this.signedState !== lazy.AddonManager.SIGNEDSTATE_PRIVILEGED &&
this.signedState !== lazy.AddonManager.SIGNEDSTATE_SYSTEM &&
!this.location.isBuiltin
) {
permissions |= lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS;
}
if (Services.policies) {
if (!Services.policies.isAllowed(`uninstall-extension:${this.id}`)) {
permissions &= ~lazy.AddonManager.PERM_CAN_UNINSTALL;
}
if (!Services.policies.isAllowed(`disable-extension:${this.id}`)) {
permissions &= ~lazy.AddonManager.PERM_CAN_DISABLE;
}
if (Services.policies.getExtensionSettings(this.id)?.updates_disabled) {
permissions &= ~lazy.AddonManager.PERM_CAN_UPGRADE;
}
}
return permissions;
}
propagateDisabledState(oldAddon) {
if (oldAddon) {
this.userDisabled = oldAddon.userDisabled;
this.embedderDisabled = oldAddon.embedderDisabled;
this.softDisabled = oldAddon.softDisabled;
this.blocklistState = oldAddon.blocklistState;
}
}
}
/**
* The AddonWrapper wraps an Addon to provide the data visible to consumers of
* the public API.
*
* NOTE: Do not add any new logic here. Add it to AddonInternal and expose
* through defineAddonWrapperProperty after this class definition.
*
* @param {AddonInternal} aAddon
* The add-on object to wrap.
*/
AddonWrapper = class {
constructor(aAddon) {
wrapperMap.set(this, aAddon);
}
get __AddonInternal__() {
return addonFor(this);
}
get quarantineIgnoredByApp() {
return this.isPrivileged || !!this.recommendationStates?.length;
}
get quarantineIgnoredByUser() {
// NOTE: confirm if this getter could be replaced by a
// lazy preference getter and the addon wrapper to not be
// kept around longer by the pref observer registered
// internally by the lazy getter.
return lazy.QuarantinedDomains.isUserAllowedAddonId(this.id);
}
set quarantineIgnoredByUser(val) {
lazy.QuarantinedDomains.setUserAllowedAddonIdPref(this.id, !!val);
}
get canChangeQuarantineIgnored() {
// Never show the quarantined domains user controls UI if the
// quarantined domains feature is disabled.
return (
WebExtensionPolicy.quarantinedDomainsEnabled &&
!lazy.isQuarantineUIDisabled &&
this.type === "extension" &&
!this.quarantineIgnoredByApp
);
}
get seen() {
return addonFor(this).seen;
}
markAsSeen() {
addonFor(this).seen = true;
XPIDatabase.saveChanges();
}
get installTelemetryInfo() {
const addon = addonFor(this);
if (!addon.installTelemetryInfo && addon.location) {
if (addon.location.isSystem) {
return { source: "system-addon" };
}
if (addon.location.isTemporary) {
return { source: "temporary-addon" };
}
}
return addon.installTelemetryInfo;
}
get temporarilyInstalled() {
return addonFor(this).location.isTemporary;
}
get aboutURL() {
return this.isActive ? addonFor(this).aboutURL : null;
}
get optionsURL() {
if (!this.isActive) {
return null;
}
let addon = addonFor(this);
if (addon.optionsURL) {
if (this.isWebExtension) {
// The internal object's optionsURL property comes from the addons
// DB and should be a relative URL. However, extensions with
// options pages installed before bug 1293721 was fixed got absolute
// URLs in the addons db. This code handles both cases.
let policy = WebExtensionPolicy.getByID(addon.id);
if (!policy) {
return null;
}
let base = policy.getURL();
return new URL(addon.optionsURL, base).href;
}
return addon.optionsURL;
}
return null;
}
get optionsType() {
if (!this.isActive) {
return null;
}
let addon = addonFor(this);
let hasOptionsURL = !!this.optionsURL;
if (addon.optionsType) {
switch (parseInt(addon.optionsType, 10)) {
case lazy.AddonManager.OPTIONS_TYPE_TAB:
case lazy.AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
return hasOptionsURL ? addon.optionsType : null;
}
return null;
}
return null;
}
get optionsBrowserStyle() {
let addon = addonFor(this);
return addon.optionsBrowserStyle;
}
get incognito() {
return addonFor(this).incognito;
}
async getBlocklistURL() {
return addonFor(this).blocklistURL;
}
get iconURL() {
return lazy.AddonManager.getPreferredIconURL(this, 48);
}
get icons() {
let addon = addonFor(this);
let icons = {};
if (addon._repositoryAddon) {
for (let size in addon._repositoryAddon.icons) {
icons[size] = addon._repositoryAddon.icons[size];
}
}
if (addon.icons) {
for (let size in addon.icons) {
let path = addon.icons[size].replace(/^\//, "");
icons[size] = this.getResourceURI(path).spec;
}
}
let canUseIconURLs = this.isActive;
if (canUseIconURLs && addon.iconURL) {
icons[32] = addon.iconURL;
icons[48] = addon.iconURL;
}
Object.freeze(icons);
return icons;
}
get screenshots() {
let addon = addonFor(this);
let repositoryAddon = addon._repositoryAddon;
if (repositoryAddon && "screenshots" in repositoryAddon) {
let repositoryScreenshots = repositoryAddon.screenshots;
if (repositoryScreenshots && repositoryScreenshots.length) {
return repositoryScreenshots;
}
}
if (addon.previewImage) {
let url = this.getResourceURI(addon.previewImage).spec;
return [new lazy.AddonManagerPrivate.AddonScreenshot(url)];
}
return null;
}
get recommendationStates() {
let addon = addonFor(this);
let state = addon.recommendationState;
if (
state &&
state.validNotBefore < addon.updateDate &&
state.validNotAfter > addon.updateDate &&
addon.isCorrectlySigned &&
!this.temporarilyInstalled
) {
return state.states;
}
return [];
}
// NOTE: this boolean getter doesn't return true for all recommendation
// states at the moment. For the states actually supported on the autograph
// side see:
get isRecommended() {
return this.recommendationStates.includes("recommended");
}
get canBypassThirdParyInstallPrompt() {
// We only bypass if the extension is signed (to support distributions
// that turn off the signing requirement) and has recommendation states,
// or the extension is signed as privileged.
return (
this.signedState == lazy.AddonManager.SIGNEDSTATE_PRIVILEGED ||
(this.signedState >= lazy.AddonManager.SIGNEDSTATE_SIGNED &&
this.recommendationStates.length)
);
}
get applyBackgroundUpdates() {
return addonFor(this).applyBackgroundUpdates;
}
set applyBackgroundUpdates(val) {
let addon = addonFor(this);
if (
val != lazy.AddonManager.AUTOUPDATE_DEFAULT &&
val != lazy.AddonManager.AUTOUPDATE_DISABLE &&
val != lazy.AddonManager.AUTOUPDATE_ENABLE
) {
val = val
? lazy.AddonManager.AUTOUPDATE_DEFAULT
: lazy.AddonManager.AUTOUPDATE_DISABLE;
}
if (val == addon.applyBackgroundUpdates) {
return;
}
XPIDatabase.setAddonProperties(addon, {
applyBackgroundUpdates: val,
});
lazy.AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [
"applyBackgroundUpdates",
]);
}
set syncGUID(val) {
let addon = addonFor(this);
if (addon.syncGUID == val) {
return;
}
if (addon.inDatabase) {
XPIDatabase.setAddonSyncGUID(addon, val);
}
addon.syncGUID = val;
}
get install() {
let addon = addonFor(this);
if (!("_install" in addon) || !addon._install) {
return null;
}
return addon._install.wrapper;
}
get updateInstall() {
let addon = addonFor(this);
return addon._updateInstall ? addon._updateInstall.wrapper : null;
}
get pendingUpgrade() {
let addon = addonFor(this);
return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null;
}
get scope() {
let addon = addonFor(this);
if (addon.location) {
return addon.location.scope;
}
return lazy.AddonManager.SCOPE_PROFILE;
}
get pendingOperations() {
let addon = addonFor(this);
let pending = 0;
if (!addon.inDatabase) {
// Add-on is pending install if there is no associated install (shouldn't
// happen here) or if the install is in the process of or has successfully
// completed the install. If an add-on is pending install then we ignore
// any other pending operations.
if (
!addon._install ||
addon._install.state == lazy.AddonManager.STATE_INSTALLING ||
addon._install.state == lazy.AddonManager.STATE_INSTALLED
) {
return lazy.AddonManager.PENDING_INSTALL;
}
} else if (addon.pendingUninstall) {
// If an add-on is pending uninstall then we ignore any other pending
// operations
return lazy.AddonManager.PENDING_UNINSTALL;
}
if (addon.active && addon.disabled) {
pending |= lazy.AddonManager.PENDING_DISABLE;
} else if (!addon.active && !addon.disabled) {
pending |= lazy.AddonManager.PENDING_ENABLE;
}
if (addon.pendingUpgrade) {
pending |= lazy.AddonManager.PENDING_UPGRADE;
}
return pending;
}
get operationsRequiringRestart() {
return 0;
}
get isDebuggable() {
return this.isActive;
}
get permissions() {
return addonFor(this).permissions();
}
get isActive() {
let addon = addonFor(this);
if (!addon.active) {
return false;
}
if (!Services.appinfo.inSafeMode) {
return true;
}
return lazy.XPIInternal.canRunInSafeMode(addon);
}
get startupPromise() {
let addon = addonFor(this);
if (!this.isActive) {
return null;
}
let activeAddon = lazy.XPIProvider.activeAddons.get(addon.id);
if (activeAddon) {
return activeAddon.startupPromise || null;
}
return null;
}
updateBlocklistState(applySoftBlock = true) {
return addonFor(this).updateBlocklistState({ applySoftBlock });
}
get userDisabled() {
let addon = addonFor(this);
return addon.softDisabled || addon.userDisabled;
}
/**
* Get the embedderDisabled property for this addon.
*
* This is intended for embedders of Gecko like GeckoView apps to control
* which addons are usable on their app.
*
* @returns {boolean}
*/
get embedderDisabled() {
if (!lazy.AddonSettings.IS_EMBEDDED) {
return undefined;
}
return addonFor(this).embedderDisabled;
}
/**
* Set the embedderDisabled property for this addon.
*
* This is intended for embedders of Gecko like GeckoView apps to control
* which addons are usable on their app.
*
* Embedders can disable addons for various reasons, e.g. the addon is not
* compatible with their implementation of the WebExtension API.
*
* When an addon is embedderDisabled it will behave like it was appDisabled.
*
* @param {boolean} val
* whether this addon should be embedder disabled or not.
*/
async setEmbedderDisabled(val) {
if (!lazy.AddonSettings.IS_EMBEDDED) {
throw new Error("Setting embedder disabled while not embedding.");
}
let addon = addonFor(this);
if (addon.embedderDisabled == val) {
return val;
}
if (addon.inDatabase) {
await XPIDatabase.updateAddonDisabledState(addon, {
embedderDisabled: val,
});
} else {
addon.embedderDisabled = val;
}
return val;
}
enable(options = {}) {
const { allowSystemAddons = false } = options;
return addonFor(this).setUserDisabled(false, allowSystemAddons);
}
disable(options = {}) {
const { allowSystemAddons = false } = options;
return addonFor(this).setUserDisabled(true, allowSystemAddons);
}
async setSoftDisabled(val) {
let addon = addonFor(this);
if (val == addon.softDisabled) {
return val;
}
if (addon.inDatabase) {
// When softDisabling a theme just enable the active theme
if (addon.type === "theme" && val && !addon.userDisabled) {
if (addon.isWebExtension) {
await XPIDatabase.updateAddonDisabledState(addon, {
softDisabled: val,
});
}
} else {
await XPIDatabase.updateAddonDisabledState(addon, {
softDisabled: val,
});
}
} else if (!addon.userDisabled) {
// Only set softDisabled if not already disabled
addon.softDisabled = val;
}
return val;
}
get isPrivileged() {
return addonFor(this).isPrivileged;
}
get hidden() {
return addonFor(this).hidden;
}
get isSystem() {
let addon = addonFor(this);
return addon.location.isSystem;
}
get isBuiltin() {
return addonFor(this).location.isBuiltin;
}
// Returns true if Firefox Sync should sync this addon. Only addons
// in the profile install location are considered syncable.
get isSyncable() {
let addon = addonFor(this);
return addon.location.name == KEY_APP_PROFILE;
}
get userPermissions() {
return addonFor(this).userPermissions;
}
get optionalPermissions() {
return addonFor(this).optionalPermissions;
}
isCompatibleWith(aAppVersion, aPlatformVersion) {
return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
}
async uninstall(alwaysAllowUndo) {
let addon = addonFor(this);
return lazy.XPIInstall.uninstallAddon(addon, alwaysAllowUndo);
}
cancelUninstall() {
let addon = addonFor(this);
lazy.XPIInstall.cancelUninstallAddon(addon);
}
findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
new lazy.UpdateChecker(
addonFor(this),
aListener,
aReason,
aAppVersion,
aPlatformVersion
);
}
// Returns true if there was an update in progress, false if there was no update to cancel
cancelUpdate() {
let addon = addonFor(this);
if (addon._updateCheck) {
addon._updateCheck.cancel();
return true;
}
return false;
}
/**
* Reloads the add-on.
*
* For temporarily installed add-ons, this uninstalls and re-installs the
* add-on. Otherwise, the addon is disabled and then re-enabled, and the cache
* is flushed.
*/
async reload() {
const addon = addonFor(this);
logger.debug(`reloading add-on ${addon.id}`);
if (!this.temporarilyInstalled) {
await XPIDatabase.updateAddonDisabledState(addon, { userDisabled: true });
await XPIDatabase.updateAddonDisabledState(addon, {
userDisabled: false,
});
} else {
// This function supports re-installing an existing add-on.
await lazy.AddonManager.installTemporaryAddon(addon._sourceBundle);
}
}
/**
* Returns a URI to the selected resource or to the add-on bundle if aPath
* is null. URIs to the bundle will always be file: URIs. URIs to resources
* will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
* still an XPI file.
*
* @param {string?} aPath
* The path in the add-on to get the URI for or null to get a URI to
* the file or directory the add-on is installed as.
* @returns {nsIURI}
*/
getResourceURI(aPath) {
let addon = addonFor(this);
let url = Services.io.newURI(addon.rootURI);
if (aPath) {
if (aPath.startsWith("/")) {
throw new Error("getResourceURI() must receive a relative path");
}
url = Services.io.newURI(aPath, null, url);
}
return url;
}
};
function chooseValue(aAddon, aObj, aProp) {
let repositoryAddon = aAddon._repositoryAddon;
let objValue = aObj[aProp];
if (
repositoryAddon &&
aProp in repositoryAddon &&
(aProp === "creator" || objValue == null)
) {
return [repositoryAddon[aProp], true];
}
return [objValue, false];
}
function defineAddonWrapperProperty(name, getter) {
Object.defineProperty(AddonWrapper.prototype, name, {
get: getter,
enumerable: true,
});
}
[
"id",
"syncGUID",
"version",
"type",
"isWebExtension",
"isCompatible",
"isPlatformCompatible",
"providesUpdatesSecurely",
"blocklistState",
"appDisabled",
"softDisabled",
"skinnable",
"foreignInstall",
"strictCompatibility",
"updateURL",
"installOrigins",
"manifestVersion",
"validInstallOrigins",
"dependencies",
"signedState",
"sitePermissions",
"siteOrigin",
"isCorrectlySigned",
"isBuiltinColorwayTheme",
].forEach(function (aProp) {
defineAddonWrapperProperty(aProp, function () {
let addon = addonFor(this);
return aProp in addon ? addon[aProp] : undefined;
});
});
[
"fullDescription",
"developerComments",
"supportURL",
"contributionURL",
"averageRating",
"reviewCount",
"reviewURL",
"weeklyDownloads",
].forEach(function (aProp) {
defineAddonWrapperProperty(aProp, function () {
let addon = addonFor(this);
if (addon._repositoryAddon) {
return addon._repositoryAddon[aProp];
}
return null;
});
});
["installDate", "updateDate"].forEach(function (aProp) {
defineAddonWrapperProperty(aProp, function () {
let addon = addonFor(this);
// installDate is always set, updateDate is sometimes missing.
return new Date(addon[aProp] ?? addon.installDate);
});
});
defineAddonWrapperProperty("signedDate", function () {
let addon = addonFor(this);
let { signedDate } = addon;
if (signedDate != null) {
return new Date(signedDate);
}
return null;
});
["sourceURI", "releaseNotesURI"].forEach(function (aProp) {
defineAddonWrapperProperty(aProp, function () {
let addon = addonFor(this);
// Temporary Installed Addons do not have a "sourceURI",
// But we can use the "_sourceBundle" as an alternative,
// which points to the path of the addon xpi installed
// or its source dir (if it has been installed from a
// directory).
if (aProp == "sourceURI" && this.temporarilyInstalled) {
return Services.io.newFileURI(addon._sourceBundle);
}
let [target, fromRepo] = chooseValue(addon, addon, aProp);
if (!target) {
return null;
}
if (fromRepo) {
return target;
}
return Services.io.newURI(target);
});
});
// Add to this Map if you need to change an addon's Fluent ID. Keep it in sync
// with the list in browser_verify_l10n_strings.js
const updatedAddonFluentIds = new Map([
["extension-default-theme-name", "extension-default-theme-name-auto"],
]);
["name", "description", "creator", "homepageURL"].forEach(function (aProp) {
defineAddonWrapperProperty(aProp, function () {
let addon = addonFor(this);
let formattedMessage;
// We want to make sure that all built-in themes that are localizable can
// actually localized, particularly those for thunderbird and desktop.
if (
(aProp === "name" || aProp === "description") &&
addon.location.name === KEY_APP_BUILTINS &&
addon.type === "theme"
) {
// Built-in themes are localized with Fluent instead of the WebExtension API.
let addonIdPrefix = addon.id.replace("@mozilla.org", "");
const colorwaySuffix = "colorway";
if (addonIdPrefix.endsWith(colorwaySuffix)) {
// FIXME: Depending on BuiltInThemes here is sort of a hack. Bug 1733466
// would provide a more generalized way of doing this.
if (aProp == "description") {
return BuiltInThemesHelpers.getLocalizedColorwayDescription(addon.id);
}
// Colorway collections are usually divided into and presented as
// "groups". A group either contains closely related colorways, e.g.
// stemming from the same base color but with different intensities, or
// if the current collection doesn't have intensities, each colorway is
// their own group. Colorway names combine the group name with an
// intensity. Their ids have the format
// {colorwayGroup}-{intensity}-colorway@mozilla.org or
// {colorwayGroupName}-colorway@mozilla.org). L10n for colorway group
// names is optional and falls back on the unlocalized name from the
// theme's manifest. The intensity part, if present, must be localized.
let localizedColorwayGroupName =
BuiltInThemesHelpers.getLocalizedColorwayGroupName(addon.id);
let [colorwayGroupName, intensity] = addonIdPrefix.split("-", 2);
if (intensity == colorwaySuffix) {
// This theme doesn't have an intensity.
return localizedColorwayGroupName || addon.defaultLocale.name;
}
// We're not using toLocaleUpperCase because these color names are
// always in English.
colorwayGroupName =
localizedColorwayGroupName ||
colorwayGroupName[0].toUpperCase() + colorwayGroupName.slice(1);
let defaultFluentId = `extension-colorways-${intensity}-name`;
let fluentId =
updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId;
[formattedMessage] = l10n.formatMessagesSync([
{
id: fluentId,
args: {
"colorway-name": colorwayGroupName,
},
},
]);
} else {
let defaultFluentId = `extension-${addonIdPrefix}-${aProp}`;
let fluentId =
updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId;
[formattedMessage] = l10n.formatMessagesSync([{ id: fluentId }]);
}