Source code

Revision control

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";
// Cannot use Services.appinfo here, or else xpcshell-tests will blow up, as
// most tests later register different nsIAppInfo implementations, which
// wouldn't be reflected in Services.appinfo anymore, as the lazy getter
// underlying it would have been initialized if we used it here.
if ("@mozilla.org/xre/app-info;1" in Cc) {
// eslint-disable-next-line mozilla/use-services
let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
if (runtime.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
// Refuse to run in child processes.
throw new Error("You cannot use the AddonManager in child processes!");
}
}
const { AppConstants } = ChromeUtils.import(
);
const MOZ_COMPATIBILITY_NIGHTLY = ![
"aurora",
"beta",
"release",
"esr",
].includes(AppConstants.MOZ_UPDATE_CHANNEL);
const INTL_LOCALES_CHANGED = "intl:app-locales-changed";
const PREF_AMO_ABUSEREPORT = "extensions.abuseReport.amWebAPI.enabled";
const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled";
const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
const PREF_EM_LAST_PLATFORM_VERSION = "extensions.lastPlatformVersion";
const PREF_EM_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
const PREF_SYS_ADDON_UPDATE_ENABLED = "extensions.systemAddon.update.enabled";
const PREF_MIN_WEBEXT_PLATFORM_VERSION =
"extensions.webExtensionsMinPlatformVersion";
const PREF_WEBAPI_TESTING = "extensions.webapi.testing";
const PREF_EM_POSTDOWNLOAD_THIRD_PARTY =
"extensions.postDownloadThirdPartyPrompt";
const UPDATE_REQUEST_VERSION = 2;
const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
const PREF_EM_CHECK_COMPATIBILITY_BASE = "extensions.checkCompatibility";
var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY
? PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly"
: undefined;
const VALID_TYPES_REGEXP = /^[\w\-]+$/;
const WEBAPI_INSTALL_HOSTS = ["addons.mozilla.org"];
const WEBAPI_TEST_INSTALL_HOSTS = [
"addons.allizom.org",
"addons-dev.allizom.org",
"example.com",
];
const AMO_ATTRIBUTION_ALLOWED_SOURCES = ["amo", "disco"];
const AMO_ATTRIBUTION_DATA_KEYS = [
"utm_campaign",
"utm_content",
"utm_medium",
"utm_source",
];
const AMO_ATTRIBUTION_DATA_MAX_LENGTH = 40;
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
);
// This global is overridden by xpcshell tests, and therefore cannot be
// a const.
var { AsyncShutdown } = ChromeUtils.import(
);
XPCOMUtils.defineLazyGlobalGetters(this, ["Element"]);
XPCOMUtils.defineLazyModuleGetters(this, {
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"WEBEXT_POSTDOWNLOAD_THIRD_PARTY",
PREF_EM_POSTDOWNLOAD_THIRD_PARTY,
false
);
// Initialize the WebExtension process script service as early as possible,
// since it needs to be able to track things like new frameLoader globals that
// are created before other framework code has been initialized.
Services.ppmm.loadProcessScript(
true
);
const INTEGER = /^[1-9]\d*$/;
var EXPORTED_SYMBOLS = ["AddonManager", "AddonManagerPrivate", "AMTelemetry"];
const CATEGORY_PROVIDER_MODULE = "addon-provider-module";
// A list of providers to load by default
const DEFAULT_PROVIDERS = ["resource://gre/modules/addons/XPIProvider.jsm"];
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
// Configure a logger at the parent 'addons' level to format
// messages for all the modules under addons.*
const PARENT_LOGGER_ID = "addons";
var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
parentLogger.level = Log.Level.Warn;
var formatter = new Log.BasicFormatter();
// Set parent logger (and its children) to append to
// the Javascript section of the Browser Console
parentLogger.addAppender(new Log.ConsoleAppender(formatter));
// Create a new logger (child of 'addons' logger)
// for use by the Addons Manager
const LOGGER_ID = "addons.manager";
var logger = Log.repository.getLogger(LOGGER_ID);
// Provide the ability to enable/disable logging
// messages at runtime.
// If the "extensions.logging.enabled" preference is
// missing or 'false', messages at the WARNING and higher
// severity should be logged to the JS console and standard error.
// If "extensions.logging.enabled" is set to 'true', messages
// at DEBUG and higher should go to JS console and standard error.
const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
const UNNAMED_PROVIDER = "<unnamed-provider>";
function providerName(aProvider) {
return aProvider.name || UNNAMED_PROVIDER;
}
/**
* Preference listener which listens for a change in the
* "extensions.logging.enabled" preference and changes the logging level of the
* parent 'addons' level logger accordingly.
*/
var PrefObserver = {
init() {
Services.prefs.addObserver(PREF_LOGGING_ENABLED, this);
Services.obs.addObserver(this, "xpcom-shutdown");
this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
},
observe(aSubject, aTopic, aData) {
if (aTopic == "xpcom-shutdown") {
Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
Services.obs.removeObserver(this, "xpcom-shutdown");
} else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
let debugLogEnabled = Services.prefs.getBoolPref(
PREF_LOGGING_ENABLED,
false
);
if (debugLogEnabled) {
parentLogger.level = Log.Level.Debug;
} else {
parentLogger.level = Log.Level.Warn;
}
}
},
};
PrefObserver.init();
/**
* Calls a callback method consuming any thrown exception. Any parameters after
* the callback parameter will be passed to the callback.
*
* @param aCallback
* The callback method to call
*/
function safeCall(aCallback, ...aArgs) {
try {
aCallback.apply(null, aArgs);
} catch (e) {
logger.warn("Exception calling callback", e);
}
}
/**
* Report an exception thrown by a provider API method.
*/
function reportProviderError(aProvider, aMethod, aError) {
let method = `provider ${providerName(aProvider)}.${aMethod}`;
AddonManagerPrivate.recordException("AMI", method, aError);
logger.error("Exception calling " + method, aError);
}
/**
* Calls a method on a provider if it exists and consumes any thrown exception.
* Any parameters after the aDefault parameter are passed to the provider's method.
*
* @param aProvider
* The provider to call
* @param aMethod
* The method name to call
* @param aDefault
* A default return value if the provider does not implement the named
* method or throws an error.
* @return the return value from the provider, or aDefault if the provider does not
* implement method or throws an error
*/
function callProvider(aProvider, aMethod, aDefault, ...aArgs) {
if (!(aMethod in aProvider)) {
return aDefault;
}
try {
return aProvider[aMethod].apply(aProvider, aArgs);
} catch (e) {
reportProviderError(aProvider, aMethod, e);
return aDefault;
}
}
/**
* Calls a method on a provider if it exists and consumes any thrown exception.
* Parameters after aMethod are passed to aProvider.aMethod().
* If the provider does not implement the method, or the method throws, calls
* the callback with 'undefined'.
*
* @param aProvider
* The provider to call
* @param aMethod
* The method name to call
*/
async function promiseCallProvider(aProvider, aMethod, ...aArgs) {
if (!(aMethod in aProvider)) {
return undefined;
}
try {
return aProvider[aMethod].apply(aProvider, aArgs);
} catch (e) {
reportProviderError(aProvider, aMethod, e);
return undefined;
}
}
/**
* Gets the currently selected locale for display.
* @return the selected locale or "en-US" if none is selected
*/
function getLocale() {
return Services.locale.requestedLocale || "en-US";
}
const WEB_EXPOSED_ADDON_PROPERTIES = [
"id",
"version",
"type",
"name",
"description",
"isActive",
];
function webAPIForAddon(addon) {
if (!addon) {
return null;
}
// These web-exposed Addon properties (see AddonManager.webidl)
// just come directly from an Addon object.
let result = {};
for (let prop of WEB_EXPOSED_ADDON_PROPERTIES) {
result[prop] = addon[prop];
}
// These properties are computed.
result.isEnabled = !addon.userDisabled;
result.canUninstall = Boolean(
addon.permissions & AddonManager.PERM_CAN_UNINSTALL
);
return result;
}
/**
* Listens for a browser changing origin and cancels the installs that were
* started by it.
*/
function BrowserListener(aBrowser, aInstallingPrincipal, aInstall) {
this.browser = aBrowser;
this.messageManager = this.browser.messageManager;
this.principal = aInstallingPrincipal;
this.install = aInstall;
aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
Services.obs.addObserver(this, "message-manager-close", true);
aInstall.addListener(this);
this.registered = true;
}
BrowserListener.prototype = {
browser: null,
install: null,
registered: false,
unregister() {
if (!this.registered) {
return;
}
this.registered = false;
Services.obs.removeObserver(this, "message-manager-close");
// The browser may have already been detached
if (this.browser.removeProgressListener) {
this.browser.removeProgressListener(this);
}
this.install.removeListener(this);
this.install = null;
},
cancelInstall() {
try {
this.install.cancel();
} catch (e) {
// install may have already failed or been cancelled, ignore these
}
},
observe(subject, topic, data) {
if (subject != this.messageManager) {
return;
}
// The browser's message manager has closed and so the browser is
// going away, cancel the install
this.cancelInstall();
},
onLocationChange(webProgress, request, location) {
if (
this.browser.contentPrincipal &&
this.principal.subsumes(this.browser.contentPrincipal)
) {
return;
}
// The browser has navigated to a new origin so cancel the install
this.cancelInstall();
},
onDownloadCancelled(install) {
this.unregister();
},
onDownloadFailed(install) {
this.unregister();
},
onInstallFailed(install) {
this.unregister();
},
onInstallEnded(install) {
this.unregister();
},
QueryInterface: ChromeUtils.generateQI([
"nsISupportsWeakReference",
"nsIWebProgressListener",
"nsIObserver",
]),
};
/**
* This represents an author of an add-on (e.g. creator or developer)
*
* @param aName
* The name of the author
* @param aURL
* The URL of the author's profile page
*/
function AddonAuthor(aName, aURL) {
this.name = aName;
this.url = aURL;
}
AddonAuthor.prototype = {
name: null,
url: null,
// Returns the author's name, defaulting to the empty string
toString() {
return this.name || "";
},
};
/**
* This represents an screenshot for an add-on
*
* @param aURL
* The URL to the full version of the screenshot
* @param aWidth
* The width in pixels of the screenshot
* @param aHeight
* The height in pixels of the screenshot
* @param aThumbnailURL
* The URL to the thumbnail version of the screenshot
* @param aThumbnailWidth
* The width in pixels of the thumbnail version of the screenshot
* @param aThumbnailHeight
* The height in pixels of the thumbnail version of the screenshot
* @param aCaption
* The caption of the screenshot
*/
function AddonScreenshot(
aURL,
aWidth,
aHeight,
aThumbnailURL,
aThumbnailWidth,
aThumbnailHeight,
aCaption
) {
this.url = aURL;
if (aWidth) {
this.width = aWidth;
}
if (aHeight) {
this.height = aHeight;
}
if (aThumbnailURL) {
this.thumbnailURL = aThumbnailURL;
}
if (aThumbnailWidth) {
this.thumbnailWidth = aThumbnailWidth;
}
if (aThumbnailHeight) {
this.thumbnailHeight = aThumbnailHeight;
}
if (aCaption) {
this.caption = aCaption;
}
}
AddonScreenshot.prototype = {
url: null,
width: null,
height: null,
thumbnailURL: null,
thumbnailWidth: null,
thumbnailHeight: null,
caption: null,
// Returns the screenshot URL, defaulting to the empty string
toString() {
return this.url || "";
},
};
/**
* A type of add-on, used by the UI to determine how to display different types
* of add-ons.
*
* @param aID
* The add-on type ID
* @param aLocaleURI
* The URI of a localized properties file to get the displayable name
* for the type from
* @param aLocaleKey
* The key for the string in the properties file.
* @param aViewType
* The optional type of view to use in the UI
* @param aUIPriority
* The priority is used by the UI to list the types in order. Lower
* values push the type higher in the list.
*/
function AddonType(aID, aLocaleURI, aLocaleKey, aViewType, aUIPriority) {
if (!aID) {
throw Components.Exception(
"An AddonType must have an ID",
Cr.NS_ERROR_INVALID_ARG
);
}
if (aViewType && aUIPriority === undefined) {
throw Components.Exception(
"An AddonType with a defined view must have a set UI priority",
Cr.NS_ERROR_INVALID_ARG
);
}
if (!aLocaleKey) {
throw Components.Exception(
"An AddonType must have a displayable name",
Cr.NS_ERROR_INVALID_ARG
);
}
this.id = aID;
this.uiPriority = aUIPriority;
this.viewType = aViewType;
if (aLocaleURI) {
XPCOMUtils.defineLazyGetter(this, "name", () => {
let bundle = Services.strings.createBundle(aLocaleURI);
return bundle.GetStringFromName(aLocaleKey);
});
} else {
this.name = aLocaleKey;
}
}
var gStarted = false;
var gStartupComplete = false;
var gCheckCompatibility = true;
var gStrictCompatibility = true;
var gCheckUpdateSecurityDefault = true;
var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
var gUpdateEnabled = true;
var gAutoUpdateDefault = true;
var gWebExtensionsMinPlatformVersion = "";
var gFinalShutdownBarrier = null;
var gBeforeShutdownBarrier = null;
var gRepoShutdownState = "";
var gShutdownInProgress = false;
var gBrowserUpdated = null;
var AMTelemetry;
/**
* This is the real manager, kept here rather than in AddonManager to keep its
* contents hidden from API users.
* @class
* @lends AddonManager
*/
var AddonManagerInternal = {
managerListeners: new Set(),
installListeners: new Set(),
addonListeners: new Set(),
typeListeners: new Set(),
pendingProviders: new Set(),
providers: new Set(),
providerShutdowns: new Map(),
types: {},
startupChanges: {},
// Store telemetry details per addon provider
telemetryDetails: {},
upgradeListeners: new Map(),
externalExtensionLoaders: new Map(),
recordTimestamp(name, value) {
this.TelemetryTimestamps.add(name, value);
},
/**
* Start up a provider, and register its shutdown hook if it has one
*
* @param {string} aProvider - An add-on provider.
* @param {boolean} aAppChanged - Whether or not the app version has changed since last session.
* @param {string} aOldAppVersion - Previous application version, if changed.
* @param {string} aOldPlatformVersion - Previous platform version, if changed.
*
* @private
*/
_startProvider(aProvider, aAppChanged, aOldAppVersion, aOldPlatformVersion) {
if (!gStarted) {
throw Components.Exception(
"AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED
);
}
logger.debug(`Starting provider: ${providerName(aProvider)}`);
callProvider(
aProvider,
"startup",
null,
aAppChanged,
aOldAppVersion,
aOldPlatformVersion
);
if ("shutdown" in aProvider) {
let name = providerName(aProvider);
let AMProviderShutdown = () => {
// If the provider has been unregistered, it will have been removed from
// this.providers. If it hasn't been unregistered, then this is a normal
// shutdown - and we move it to this.pendingProviders in case we're
// running in a test that will start AddonManager again.
if (this.providers.has(aProvider)) {
this.providers.delete(aProvider);
this.pendingProviders.add(aProvider);
}
return new Promise((resolve, reject) => {
logger.debug("Calling shutdown blocker for " + name);
resolve(aProvider.shutdown());
}).catch(err => {
logger.warn("Failure during shutdown of " + name, err);
AddonManagerPrivate.recordException(
"AMI",
"Async shutdown of " + name,
err
);
});
};
logger.debug("Registering shutdown blocker for " + name);
this.providerShutdowns.set(aProvider, AMProviderShutdown);
AddonManagerPrivate.finalShutdown.addBlocker(name, AMProviderShutdown);
}
this.pendingProviders.delete(aProvider);
this.providers.add(aProvider);
logger.debug(`Provider finished startup: ${providerName(aProvider)}`);
},
_getProviderByName(aName) {
for (let provider of this.providers) {
if (providerName(provider) == aName) {
return provider;
}
}
return undefined;
},
/**
* Initializes the AddonManager, loading any known providers and initializing
* them.
*/
startup() {
try {
if (gStarted) {
return;
}
this.recordTimestamp("AMI_startup_begin");
// Enable the addonsManager telemetry event category.
AMTelemetry.init();
// clear this for xpcshell test restarts
for (let provider in this.telemetryDetails) {
delete this.telemetryDetails[provider];
}
let appChanged = undefined;
let oldAppVersion = null;
try {
oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
appChanged = Services.appinfo.version != oldAppVersion;
} catch (e) {}
gBrowserUpdated = appChanged;
let oldPlatformVersion = Services.prefs.getCharPref(
PREF_EM_LAST_PLATFORM_VERSION,
""
);
if (appChanged !== false) {
logger.debug("Application has been upgraded");
Services.prefs.setCharPref(
PREF_EM_LAST_APP_VERSION,
Services.appinfo.version
);
Services.prefs.setCharPref(
PREF_EM_LAST_PLATFORM_VERSION,
Services.appinfo.platformVersion
);
Services.prefs.setIntPref(
PREF_BLOCKLIST_PINGCOUNTVERSION,
appChanged === undefined ? 0 : -1
);
}
if (!MOZ_COMPATIBILITY_NIGHTLY) {
PREF_EM_CHECK_COMPATIBILITY =
PREF_EM_CHECK_COMPATIBILITY_BASE +
"." +
Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
}
gCheckCompatibility = Services.prefs.getBoolPref(
PREF_EM_CHECK_COMPATIBILITY,
gCheckCompatibility
);
Services.prefs.addObserver(PREF_EM_CHECK_COMPATIBILITY, this);
gStrictCompatibility = Services.prefs.getBoolPref(
PREF_EM_STRICT_COMPATIBILITY,
gStrictCompatibility
);
Services.prefs.addObserver(PREF_EM_STRICT_COMPATIBILITY, this);
let defaultBranch = Services.prefs.getDefaultBranch("");
gCheckUpdateSecurityDefault = defaultBranch.getBoolPref(
PREF_EM_CHECK_UPDATE_SECURITY,
gCheckUpdateSecurityDefault
);
gCheckUpdateSecurity = Services.prefs.getBoolPref(
PREF_EM_CHECK_UPDATE_SECURITY,
gCheckUpdateSecurity
);
Services.prefs.addObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
gUpdateEnabled = Services.prefs.getBoolPref(
PREF_EM_UPDATE_ENABLED,
gUpdateEnabled
);
Services.prefs.addObserver(PREF_EM_UPDATE_ENABLED, this);
gAutoUpdateDefault = Services.prefs.getBoolPref(
PREF_EM_AUTOUPDATE_DEFAULT,
gAutoUpdateDefault
);
Services.prefs.addObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
PREF_MIN_WEBEXT_PLATFORM_VERSION,
gWebExtensionsMinPlatformVersion
);
Services.prefs.addObserver(PREF_MIN_WEBEXT_PLATFORM_VERSION, this);
// Watch for language changes, refresh the addon cache when it changes.
Services.obs.addObserver(this, INTL_LOCALES_CHANGED);
// Ensure all default providers have had a chance to register themselves
for (let url of DEFAULT_PROVIDERS) {
try {
let scope = {};
ChromeUtils.import(url, scope);
// Sanity check - make sure the provider exports a symbol that
// has a 'startup' method
let syms = Object.keys(scope);
if (syms.length < 1 || typeof scope[syms[0]].startup != "function") {
logger.warn("Provider " + url + " has no startup()");
AddonManagerPrivate.recordException(
"AMI",
"provider " + url,
"no startup()"
);
}
logger.debug(
"Loaded provider scope for " +
url +
": " +
Object.keys(scope).toSource()
);
} catch (e) {
AddonManagerPrivate.recordException(
"AMI",
"provider " + url + " load failed",
e
);
logger.error('Exception loading default provider "' + url + '"', e);
}
}
// Load any providers registered in the category manager
for (let { entry, value: url } of Services.catMan.enumerateCategory(
CATEGORY_PROVIDER_MODULE
)) {
try {
ChromeUtils.import(url, {});
logger.debug(`Loaded provider scope for ${url}`);
} catch (e) {
AddonManagerPrivate.recordException(
"AMI",
"provider " + url + " load failed",
e
);
logger.error(
"Exception loading provider " +
entry +
' from category "' +
url +
'"',
e
);
}
}
// Register our shutdown handler with the AsyncShutdown manager
gBeforeShutdownBarrier = new AsyncShutdown.Barrier(
"AddonManager: Waiting to start provider shutdown."
);
gFinalShutdownBarrier = new AsyncShutdown.Barrier(
"AddonManager: Waiting for providers to shut down."
);
AsyncShutdown.profileBeforeChange.addBlocker(
"AddonManager: shutting down.",
this.shutdownManager.bind(this),
{ fetchState: this.shutdownState.bind(this) }
);
// Once we start calling providers we must allow all normal methods to work.
gStarted = true;
for (let provider of this.pendingProviders) {
this._startProvider(
provider,
appChanged,
oldAppVersion,
oldPlatformVersion
);
}
// If this is a new profile just pretend that there were no changes
if (appChanged === undefined) {
for (let type in this.startupChanges) {
delete this.startupChanges[type];
}
}
gStartupComplete = true;
this.recordTimestamp("AMI_startup_end");
} catch (e) {
logger.error("startup failed", e);
AddonManagerPrivate.recordException("AMI", "startup failed", e);
}
logger.debug("Completed startup sequence");
this.callManagerListeners("onStartup");
},
/**
* Registers a new AddonProvider.
*
* @param {string} aProvider -The provider to register
* @param {string[]} [aTypes] - An optional array of add-on types
*/
registerProvider(aProvider, aTypes) {
if (!aProvider || typeof aProvider != "object") {
throw Components.Exception(
"aProvider must be specified",
Cr.NS_ERROR_INVALID_ARG
);
}
if (aTypes && !Array.isArray(aTypes)) {
throw Components.Exception(
"aTypes must be an array or null",
Cr.NS_ERROR_INVALID_ARG
);
}
this.pendingProviders.add(aProvider);
if (aTypes) {
for (let type of aTypes) {
if (!(type.id in this.types)) {
if (!VALID_TYPES_REGEXP.test(type.id)) {
logger.warn("Ignoring invalid type " + type.id);
return;
}
this.types[type.id] = {
type,
providers: [aProvider],
};
let typeListeners = new Set(this.typeListeners);
for (let listener of typeListeners) {
safeCall(() => listener.onTypeAdded(type));
}
} else {
this.types[type.id].providers.push(aProvider);
}
}
}
// If we're registering after startup call this provider's startup.
if (gStarted) {
this._startProvider(aProvider);
}
},
/**
* Unregisters an AddonProvider.
*
* @param aProvider
* The provider to unregister
* @return Whatever the provider's 'shutdown' method returns (if anything).
* For providers that have async shutdown methods returning Promises,
* the caller should wait for that Promise to resolve.
*/
unregisterProvider(aProvider) {
if (!aProvider || typeof aProvider != "object") {
throw Components.Exception(
"aProvider must be specified",
Cr.NS_ERROR_INVALID_ARG
);
}
this.providers.delete(aProvider);
// The test harness will unregister XPIProvider *after* shutdown, which is
// after the provider will have been moved from providers to
// pendingProviders.
this.pendingProviders.delete(aProvider);
for (let type in this.types) {
this.types[type].providers = this.types[type].providers.filter(
p => p != aProvider
);
if (!this.types[type].providers.length) {
let oldType = this.types[type].type;
delete this.types[type];
let typeListeners = new Set(this.typeListeners);
for (let listener of typeListeners) {
safeCall(() => listener.onTypeRemoved(oldType));
}
}
}
// If we're unregistering after startup but before shutting down,
// remove the blocker for this provider's shutdown and call it.
// If we're already shutting down, just let gFinalShutdownBarrier
// call it to avoid races.
if (gStarted && !gShutdownInProgress) {
logger.debug(
"Unregistering shutdown blocker for " + providerName(aProvider)
);
let shutter = this.providerShutdowns.get(aProvider);
if (shutter) {
this.providerShutdowns.delete(aProvider);
gFinalShutdownBarrier.client.removeBlocker(shutter);
return shutter();
}
}
return undefined;
},
/**
* Mark a provider as safe to access via AddonManager APIs, before its
* startup has completed.
*
* Normally a provider isn't marked as safe until after its (synchronous)
* startup() method has returned. Until a provider has been marked safe,
* it won't be used by any of the AddonManager APIs. markProviderSafe()
* allows a provider to mark itself as safe during its startup; this can be
* useful if the provider wants to perform tasks that block startup, which
* happen after its required initialization tasks and therefore when the
* provider is in a safe state.
*
* @param aProvider Provider object to mark safe
*/
markProviderSafe(aProvider) {
if (!gStarted) {
throw Components.Exception(
"AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED
);
}
if (!aProvider || typeof aProvider != "object") {
throw Components.Exception(
"aProvider must be specified",
Cr.NS_ERROR_INVALID_ARG
);
}
if (!this.pendingProviders.has(aProvider)) {
return;
}
this.pendingProviders.delete(aProvider);
this.providers.add(aProvider);
},
/**
* Calls a method on all registered providers if it exists and consumes any
* thrown exception. Return values are ignored. Any parameters after the
* method parameter are passed to the provider's method.
* WARNING: Do not use for asynchronous calls; callProviders() does not
* invoke callbacks if provider methods throw synchronous exceptions.
*
* @param aMethod
* The method name to call
* @see callProvider
*/
callProviders(aMethod, ...aArgs) {
if (!aMethod || typeof aMethod != "string") {
throw Components.Exception(
"aMethod must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
let providers = [...this.providers];
for (let provider of providers) {
try {
if (aMethod in provider) {
provider[aMethod].apply(provider, aArgs);
}
} catch (e) {
reportProviderError(provider, aMethod, e);
}
}
},
/**
* Report the current state of asynchronous shutdown
*/
shutdownState() {
let state = [];
for (let barrier of [gBeforeShutdownBarrier, gFinalShutdownBarrier]) {
if (barrier) {
state.push({ name: barrier.client.name, state: barrier.state });
}
}
state.push({
name: "AddonRepository: async shutdown",
state: gRepoShutdownState,
});
return state;
},
/**
* Shuts down the addon manager and all registered providers, this must clean
* up everything in order for automated tests to fake restarts.
* @return Promise{null} that resolves when all providers and dependent modules
* have finished shutting down
*/
async shutdownManager() {
logger.debug("before shutdown");
try {
await gBeforeShutdownBarrier.wait();
} catch (e) {
Cu.reportError(e);
}
logger.debug("shutdown");
this.callManagerListeners("onShutdown");
gRepoShutdownState = "pending";
gShutdownInProgress = true;
// Clean up listeners
Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this);
Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this);
Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
Services.prefs.removeObserver(PREF_EM_UPDATE_ENABLED, this);
Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
Services.obs.removeObserver(this, INTL_LOCALES_CHANGED);
let savedError = null;
// Only shut down providers if they've been started.
if (gStarted) {
try {
await gFinalShutdownBarrier.wait();
} catch (err) {
savedError = err;
logger.error("Failure during wait for shutdown barrier", err);
AddonManagerPrivate.recordException(
"AMI",
"Async shutdown of AddonManager providers",
err
);
}
}
// Shut down AddonRepository after providers (if any).
try {
gRepoShutdownState = "in progress";
await AddonRepository.shutdown();
gRepoShutdownState = "done";
} catch (err) {
savedError = err;
logger.error("Failure during AddonRepository shutdown", err);
AddonManagerPrivate.recordException(
"AMI",
"Async shutdown of AddonRepository",
err
);
}
logger.debug("Async provider shutdown done");
this.managerListeners.clear();
this.installListeners.clear();
this.addonListeners.clear();
this.typeListeners.clear();
this.providerShutdowns.clear();
for (let type in this.startupChanges) {
delete this.startupChanges[type];
}
gStarted = false;
gStartupComplete = false;
gFinalShutdownBarrier = null;
gBeforeShutdownBarrier = null;
gShutdownInProgress = false;
if (savedError) {
throw savedError;
}
},
/**
* Notified when a preference we're interested in has changed.
*
* @see nsIObserver
*/
observe(aSubject, aTopic, aData) {
switch (aTopic) {
case INTL_LOCALES_CHANGED: {
// Asynchronously fetch and update the addons cache.
AddonRepository.backgroundUpdateCheck();
return;
}
}
switch (aData) {
case PREF_EM_CHECK_COMPATIBILITY: {
let oldValue = gCheckCompatibility;
gCheckCompatibility = Services.prefs.getBoolPref(
PREF_EM_CHECK_COMPATIBILITY,
true
);
this.callManagerListeners("onCompatibilityModeChanged");
if (gCheckCompatibility != oldValue) {
this.updateAddonAppDisabledStates();
}
break;
}
case PREF_EM_STRICT_COMPATIBILITY: {
let oldValue = gStrictCompatibility;
gStrictCompatibility = Services.prefs.getBoolPref(
PREF_EM_STRICT_COMPATIBILITY,
true
);
this.callManagerListeners("onCompatibilityModeChanged");
if (gStrictCompatibility != oldValue) {
this.updateAddonAppDisabledStates();
}
break;
}
case PREF_EM_CHECK_UPDATE_SECURITY: {
let oldValue = gCheckUpdateSecurity;
gCheckUpdateSecurity = Services.prefs.getBoolPref(
PREF_EM_CHECK_UPDATE_SECURITY,
true
);
this.callManagerListeners("onCheckUpdateSecurityChanged");
if (gCheckUpdateSecurity != oldValue) {
this.updateAddonAppDisabledStates();
}
break;
}
case PREF_EM_UPDATE_ENABLED: {
gUpdateEnabled = Services.prefs.getBoolPref(
PREF_EM_UPDATE_ENABLED,
true
);
this.callManagerListeners("onUpdateModeChanged");
break;
}
case PREF_EM_AUTOUPDATE_DEFAULT: {
gAutoUpdateDefault = Services.prefs.getBoolPref(
PREF_EM_AUTOUPDATE_DEFAULT,
true
);
this.callManagerListeners("onUpdateModeChanged");
break;
}
case PREF_MIN_WEBEXT_PLATFORM_VERSION: {
gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
PREF_MIN_WEBEXT_PLATFORM_VERSION
);
break;
}
}
},
/**
* Replaces %...% strings in an addon url (update and updateInfo) with
* appropriate values.
*
* @param aAddon
* The Addon representing the add-on
* @param aUri
* The string representation of the URI to escape
* @param aAppVersion
* The optional application version to use for %APP_VERSION%
* @return The appropriately escaped URI.
*/
escapeAddonURI(aAddon, aUri, aAppVersion) {
if (!aAddon || typeof aAddon != "object") {
throw Components.Exception(
"aAddon must be an Addon object",
Cr.NS_ERROR_INVALID_ARG
);
}
if (!aUri || typeof aUri != "string") {
throw Components.Exception(
"aUri must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
if (aAppVersion && typeof aAppVersion != "string") {
throw Components.Exception(
"aAppVersion must be a string or null",
Cr.NS_ERROR_INVALID_ARG
);
}
var addonStatus =
aAddon.userDisabled || aAddon.softDisabled
? "userDisabled"
: "userEnabled";
if (!aAddon.isCompatible) {
addonStatus += ",incompatible";
}
let { blocklistState } = aAddon;
if (blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
addonStatus += ",blocklisted";
}
if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
addonStatus += ",softblocked";
}
let params = new Map(
Object.entries({
ITEM_ID: aAddon.id,
ITEM_VERSION: aAddon.version,
ITEM_STATUS: addonStatus,
APP_ID: Services.appinfo.ID,
APP_VERSION: aAppVersion ? aAppVersion : Services.appinfo.version,
REQ_VERSION: UPDATE_REQUEST_VERSION,
APP_OS: Services.appinfo.OS,
APP_ABI: Services.appinfo.XPCOMABI,
APP_LOCALE: getLocale(),
CURRENT_APP_VERSION: Services.appinfo.version,
})
);
let uri = aUri.replace(/%([A-Z_]+)%/g, (m0, m1) => params.get(m1) || m0);
// escape() does not properly encode + symbols in any embedded FVF strings.
return uri.replace(/\+/g, "%2B");
},
_updatePromptHandler(info) {
let oldPerms = info.existingAddon.userPermissions;
if (!oldPerms) {
// Updating from a legacy add-on, just let it proceed
return Promise.resolve();
}
let newPerms = info.addon.userPermissions;
let difference = Extension.comparePermissions(oldPerms, newPerms);
// If there are no new permissions, just go ahead with the update
if (!difference.origins.length && !difference.permissions.length) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
let subject = {
wrappedJSObject: {
addon: info.addon,
permissions: difference,
resolve,
reject,
// Reference to the related AddonInstall object (used in AMTelemetry to
// link the recorded event to the other events from the same install flow).
install: info.install,
},
};
Services.obs.notifyObservers(subject, "webextension-update-permissions");
});
},
// Returns true if System Addons should be updated
systemUpdateEnabled() {
if (!Services.prefs.getBoolPref(PREF_SYS_ADDON_UPDATE_ENABLED)) {
return false;
}
if (Services.policies && !Services.policies.isAllowed("SysAddonUpdate")) {
return false;
}
return true;
},
/**
* Performs a background update check by starting an update for all add-ons
* that can be updated.
* @return Promise{null} Resolves when the background update check is complete
* (the resulting addon installations may still be in progress).
*/
backgroundUpdateCheck() {
if (!gStarted) {
throw Components.Exception(
"AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED
);
}
let buPromise = (async () => {
logger.debug("Background update check beginning");
Services.obs.notifyObservers(null, "addons-background-update-start");
if (this.updateEnabled) {
// Keep track of all the async add-on updates happening in parallel
let updates = [];
let allAddons = await this.getAllAddons();
// Repopulate repository cache first, to ensure compatibility overrides
// are up to date before checking for addon updates.
await AddonRepository.backgroundUpdateCheck();
for (let addon of allAddons) {
// Check all add-ons for updates so that any compatibility updates will
// be applied
if (!(addon.permissions & AddonManager.PERM_CAN_UPGRADE)) {
continue;
}
updates.push(
new Promise((resolve, reject) => {
addon.findUpdates(
{
onUpdateAvailable(aAddon, aInstall) {
// Start installing updates when the add-on can be updated and
// background updates should be applied.
logger.debug("Found update for add-on ${id}", aAddon);
if (AddonManager.shouldAutoUpdate(aAddon)) {
// XXX we really should resolve when this install is done,
// not when update-available check completes, no?
logger.debug(`Starting upgrade install of ${aAddon.id}`);
aInstall.promptHandler = (...args) =>
AddonManagerInternal._updatePromptHandler(...args);
aInstall.install();
}
},
onUpdateFinished: aAddon => {
logger.debug("onUpdateFinished for ${id}", aAddon);
resolve();
},
},
AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
);
})
);
}
await Promise.all(updates);
}
if (AddonManagerInternal.systemUpdateEnabled()) {
try {
await AddonManagerInternal._getProviderByName(
"XPIProvider"
).updateSystemAddons();
} catch (e) {
logger.warn("Failed to update system addons", e);
}
}
logger.debug("Background update check complete");
Services.obs.notifyObservers(null, "addons-background-update-complete");
})();
// Fork the promise chain so we can log the error and let our caller see it too.
buPromise.catch(e => logger.warn("Error in background update", e));
return buPromise;
},
/**
* Adds a add-on to the list of detected changes for this startup. If
* addStartupChange is called multiple times for the same add-on in the same
* startup then only the most recent change will be remembered.
*
* @param aType
* The type of change as a string. Providers can define their own
* types of changes or use the existing defined STARTUP_CHANGE_*
* constants
* @param aID
* The ID of the add-on
*/
addStartupChange(aType, aID) {
if (!aType || typeof aType != "string") {
throw Components.Exception(
"aType must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
if (!aID || typeof aID != "string") {
throw Components.Exception(
"aID must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
if (gStartupComplete) {
return;
}
logger.debug("Registering startup change '" + aType + "' for " + aID);
// Ensure that an ID is only listed in one type of change
for (let type in this.startupChanges) {
this.removeStartupChange(type, aID);
}
if (!(aType in this.startupChanges)) {
this.startupChanges[aType] = [];
}
this.startupChanges[aType].push(aID);
},
/**
* Removes a startup change for an add-on.
*
* @param aType
* The type of change
* @param aID
* The ID of the add-on
*/
removeStartupChange(aType, aID) {
if (!aType || typeof aType != "string") {
throw Components.Exception(
"aType must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
if (!aID || typeof aID != "string") {
throw Components.Exception(
"aID must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
if (gStartupComplete) {
return;
}
if (!(aType in this.startupChanges)) {
return;
}
this.startupChanges[aType] = this.startupChanges[aType].filter(
aItem => aItem != aID
);
},
/**
* Calls all registered AddonManagerListeners with an event. Any parameters
* after the method parameter are passed to the listener.
*
* @param aMethod
* The method on the listeners to call
*/
callManagerListeners(aMethod, ...aArgs) {
if (!gStarted) {
throw Components.Exception(
"AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED
);
}
if (!aMethod || typeof aMethod != "string") {
throw Components.Exception(
"aMethod must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
let managerListeners = new Set(this.managerListeners);
for (let listener of managerListeners) {
try {
if (aMethod in listener) {
listener[aMethod].apply(listener, aArgs);
}
} catch (e) {
logger.warn(
"AddonManagerListener threw exception when calling " + aMethod,
e
);
}
}
},
/**
* Calls all registered InstallListeners with an event. Any parameters after
* the extraListeners parameter are passed to the listener.
*
* @param aMethod
* The method on the listeners to call
* @param aExtraListeners
* An optional array of extra InstallListeners to also call
* @return false if any of the listeners returned false, true otherwise
*/
callInstallListeners(aMethod, aExtraListeners, ...aArgs) {
if (!gStarted) {
throw Components.Exception(
"AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED
);
}
if (!aMethod || typeof aMethod != "string") {
throw Components.Exception(
"aMethod must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
if (aExtraListeners && !Array.isArray(aExtraListeners)) {
throw Components.Exception(
"aExtraListeners must be an array or null",
Cr.NS_ERROR_INVALID_ARG
);
}
let result = true;
let listeners;
if (aExtraListeners) {
listeners = new Set(
aExtraListeners.concat(Array.from(this.installListeners))
);
} else {
listeners = new Set(this.installListeners);
}
for (let listener of listeners) {
try {
if (aMethod in listener) {
if (listener[aMethod].apply(listener, aArgs) === false) {
result = false;
}
}
} catch (e) {
logger.warn(
"InstallListener threw exception when calling " + aMethod,
e
);
}
}
return result;
},
/**
* Calls all registered AddonListeners with an event. Any parameters after
* the method parameter are passed to the listener.
*
* @param aMethod
* The method on the listeners to call
*/
callAddonListeners(aMethod, ...aArgs) {
if (!gStarted) {
throw Components.Exception(
"AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED
);
}
if (!aMethod || typeof aMethod != "string") {
throw Components.Exception(
"aMethod must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
let addonListeners = new Set(this.addonListeners);
for (let listener of addonListeners) {
try {
if (aMethod in listener) {
listener[aMethod].apply(listener, aArgs);
}
} catch (e) {
logger.warn("AddonListener threw exception when calling " + aMethod, e);
}
}
},
/**
* Notifies all providers that an add-on has been enabled when that type of
* add-on only supports a single add-on being enabled at a time. This allows
* the providers to disable theirs if necessary.
*
* @param aID
* The ID of the enabled add-on
* @param aType
* The type of the enabled add-on
* @param aPendingRestart
* A boolean indicating if the change will only take place the next
* time the application is restarted
*/
async notifyAddonChanged(aID, aType, aPendingRestart) {
if (!gStarted) {
throw Components.Exception(
"AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED
);
}
if (aID && typeof aID != "string") {
throw Components.Exception(
"aID must be a string or null",
Cr.NS_ERROR_INVALID_ARG
);
}
if (!aType || typeof aType != "string") {
throw Components.Exception(
"aType must be a non-empty string",
Cr.NS_ERROR_INVALID_ARG
);
}
// Temporary hack until bug 520124 lands.
// We can get here during synchronous startup, at which point it's
// considered unsafe (and therefore disallowed by AddonManager.jsm) to
// access providers that haven't been initialized yet. Since this is when
// XPIProvider is starting up, XPIProvider can't access itself via APIs
// going through AddonManager.jsm. Thankfully, this is the only use
// of this API, and we know it's safe to use this API with both
// providers; so we have this hack to allow bypassing the normal
// safetey guard.
// The notifyAddonChanged/addonChanged API will be unneeded and therefore
// removed by bug 520124, so this is a temporary quick'n'dirty hack.
let providers = [...this.providers, ...this.pendingProviders];
for (let provider of providers) {
let result = callProvider(
provider,
"addonChanged",
null,
aID,
aType,
aPendingRestart
);
if (result) {
await result;
}
}