mirror of https://github.com/roytam1/UXP
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2976 lines
99 KiB
2976 lines
99 KiB
/* 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"; |
|
|
|
const Cc = Components.classes; |
|
const Ci = Components.interfaces; |
|
const Cr = Components.results; |
|
const Cu = Components.utils; |
|
|
|
// 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) { |
|
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 PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion"; |
|
const PREF_DEFAULT_PROVIDERS_ENABLED = "extensions.defaultProviders.enabled"; |
|
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_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url"; |
|
const PREF_APP_UPDATE_ENABLED = "app.update.enabled"; |
|
const PREF_APP_UPDATE_AUTO = "app.update.auto"; |
|
const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; |
|
const PREF_SELECTED_LOCALE = "general.useragent.locale"; |
|
const UNKNOWN_XPCOM_ABI = "unknownABI"; |
|
|
|
const UPDATE_REQUEST_VERSION = 2; |
|
const CATEGORY_UPDATE_PARAMS = "extension-update-params"; |
|
|
|
const XMLURI_BLOCKLIST = "http://www.mozilla.org/2006/addons-blocklist"; |
|
|
|
const KEY_PROFILEDIR = "ProfD"; |
|
const KEY_APPDIR = "XCurProcD"; |
|
const FILE_BLOCKLIST = "blocklist.xml"; |
|
|
|
const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi; |
|
const PREF_EM_CHECK_COMPATIBILITY = "extensions.enableCompatibilityChecking"; |
|
|
|
const TOOLKIT_ID = "toolkit@mozilla.org"; |
|
|
|
const VALID_TYPES_REGEXP = /^[\w\-]+$/; |
|
|
|
Cu.import("resource://gre/modules/Services.jsm"); |
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
Cu.import("resource://gre/modules/AsyncShutdown.jsm"); |
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
"resource://gre/modules/Task.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
"resource://gre/modules/Promise.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", |
|
"resource://gre/modules/addons/AddonRepository.jsm"); |
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
|
"resource://gre/modules/FileUtils.jsm"); |
|
|
|
XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() { |
|
let certUtils = {}; |
|
Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); |
|
return certUtils; |
|
}); |
|
|
|
|
|
this.EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ]; |
|
|
|
const CATEGORY_PROVIDER_MODULE = "addon-provider-module"; |
|
|
|
// A list of providers to load by default |
|
const DEFAULT_PROVIDERS = [ |
|
"resource://gre/modules/addons/XPIProvider.jsm", |
|
"resource://gre/modules/LightweightThemeManager.jsm" |
|
]; |
|
|
|
Cu.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)); |
|
// Set parent logger (and its children) to |
|
// also append to standard out |
|
parentLogger.addAppender(new Log.DumpAppender(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: function() { |
|
Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false); |
|
Services.obs.addObserver(this, "xpcom-shutdown", false); |
|
this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED); |
|
}, |
|
|
|
observe: function(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(). |
|
* The last parameter must be a callback function. |
|
* 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 |
|
*/ |
|
function callProviderAsync(aProvider, aMethod, ...aArgs) { |
|
let callback = aArgs[aArgs.length - 1]; |
|
if (!(aMethod in aProvider)) { |
|
callback(undefined); |
|
return; |
|
} |
|
try { |
|
return aProvider[aMethod].apply(aProvider, aArgs); |
|
} |
|
catch (e) { |
|
reportProviderError(aProvider, aMethod, e); |
|
callback(undefined); |
|
return; |
|
} |
|
} |
|
|
|
/** |
|
* Gets the currently selected locale for display. |
|
* @return the selected locale or "en-US" if none is selected |
|
*/ |
|
function getLocale() { |
|
try { |
|
if (Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE)) |
|
return Services.locale.getLocaleComponentForUserAgent(); |
|
} |
|
catch (e) { } |
|
|
|
try { |
|
let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE, |
|
Ci.nsIPrefLocalizedString); |
|
if (locale) |
|
return locale; |
|
} |
|
catch (e) { } |
|
|
|
try { |
|
return Services.prefs.getCharPref(PREF_SELECTED_LOCALE); |
|
} |
|
catch (e) { } |
|
|
|
return "en-US"; |
|
} |
|
|
|
/** |
|
* Previously the APIs for installing add-ons from webpages accepted nsIURI |
|
* arguments for the installing page. They now take an nsIPrincipal but for now |
|
* maintain backwards compatibility by converting an nsIURI to an nsIPrincipal. |
|
* |
|
* @param aPrincipalOrURI |
|
* The argument passed to the API function. Can be null, an nsIURI or |
|
* an nsIPrincipal. |
|
* @return an nsIPrincipal. |
|
*/ |
|
function ensurePrincipal(principalOrURI) { |
|
if (principalOrURI instanceof Ci.nsIPrincipal) |
|
return principalOrURI; |
|
|
|
logger.warn("Deprecated API call, please pass a non-null nsIPrincipal instead of an nsIURI"); |
|
|
|
// Previously a null installing URI meant allowing the install regardless. |
|
if (!principalOrURI) { |
|
return Services.scriptSecurityManager.getSystemPrincipal(); |
|
} |
|
|
|
if (principalOrURI instanceof Ci.nsIURI) { |
|
return Services.scriptSecurityManager.createCodebasePrincipal(principalOrURI, { |
|
inBrowser: true |
|
}); |
|
} |
|
|
|
// Just return whatever we have, the API method will log an error about it. |
|
return principalOrURI; |
|
} |
|
|
|
/** |
|
* A helper class to repeatedly call a listener with each object in an array |
|
* optionally checking whether the object has a method in it. |
|
* |
|
* @param aObjects |
|
* The array of objects to iterate through |
|
* @param aMethod |
|
* An optional method name, if not null any objects without this method |
|
* will not be passed to the listener |
|
* @param aListener |
|
* A listener implementing nextObject and noMoreObjects methods. The |
|
* former will be called with the AsyncObjectCaller as the first |
|
* parameter and the object as the second. noMoreObjects will be passed |
|
* just the AsyncObjectCaller |
|
*/ |
|
function AsyncObjectCaller(aObjects, aMethod, aListener) { |
|
this.objects = [...aObjects]; |
|
this.method = aMethod; |
|
this.listener = aListener; |
|
|
|
this.callNext(); |
|
} |
|
|
|
AsyncObjectCaller.prototype = { |
|
objects: null, |
|
method: null, |
|
listener: null, |
|
|
|
/** |
|
* Passes the next object to the listener or calls noMoreObjects if there |
|
* are none left. |
|
*/ |
|
callNext: function() { |
|
if (this.objects.length == 0) { |
|
this.listener.noMoreObjects(this); |
|
return; |
|
} |
|
|
|
let object = this.objects.shift(); |
|
if (!this.method || this.method in object) |
|
this.listener.nextObject(this, object); |
|
else |
|
this.callNext(); |
|
} |
|
}; |
|
|
|
/** |
|
* Listens for a browser changing origin and cancels the installs that were |
|
* started by it. |
|
*/ |
|
function BrowserListener(aBrowser, aInstallingPrincipal, aInstalls) { |
|
this.browser = aBrowser; |
|
this.principal = aInstallingPrincipal; |
|
this.installs = aInstalls; |
|
this.installCount = aInstalls.length; |
|
|
|
aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); |
|
Services.obs.addObserver(this, "message-manager-disconnect", true); |
|
|
|
for (let install of this.installs) |
|
install.addListener(this); |
|
|
|
this.registered = true; |
|
} |
|
|
|
BrowserListener.prototype = { |
|
browser: null, |
|
installs: null, |
|
installCount: null, |
|
registered: false, |
|
|
|
unregister: function() { |
|
if (!this.registered) |
|
return; |
|
this.registered = false; |
|
|
|
Services.obs.removeObserver(this, "message-manager-disconnect"); |
|
// The browser may have already been detached |
|
if (this.browser.removeProgressListener) |
|
this.browser.removeProgressListener(this); |
|
|
|
for (let install of this.installs) |
|
install.removeListener(this); |
|
this.installs = null; |
|
}, |
|
|
|
cancelInstalls: function() { |
|
for (let install of this.installs) { |
|
try { |
|
install.cancel(); |
|
} |
|
catch (e) { |
|
// Some installs may have already failed or been cancelled, ignore these |
|
} |
|
} |
|
}, |
|
|
|
observe: function(subject, topic, data) { |
|
if (subject != this.browser.messageManager) |
|
return; |
|
|
|
// The browser's message manager has closed and so the browser is |
|
// going away, cancel all installs |
|
this.cancelInstalls(); |
|
}, |
|
|
|
onLocationChange: function(webProgress, request, location) { |
|
if (this.browser.contentPrincipal && this.principal.subsumes(this.browser.contentPrincipal)) |
|
return; |
|
|
|
// The browser has navigated to a new origin so cancel all installs |
|
this.cancelInstalls(); |
|
}, |
|
|
|
onDownloadCancelled: function(install) { |
|
// Don't need to hear more events from this install |
|
install.removeListener(this); |
|
|
|
// Once all installs have ended unregister everything |
|
if (--this.installCount == 0) |
|
this.unregister(); |
|
}, |
|
|
|
onDownloadFailed: function(install) { |
|
this.onDownloadCancelled(install); |
|
}, |
|
|
|
onInstallFailed: function(install) { |
|
this.onDownloadCancelled(install); |
|
}, |
|
|
|
onInstallEnded: function(install) { |
|
this.onDownloadCancelled(install); |
|
}, |
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, |
|
Ci.nsIWebProgressListener, |
|
Ci.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: function() { |
|
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: function() { |
|
return this.url || ""; |
|
} |
|
} |
|
|
|
|
|
/** |
|
* This represents a compatibility override for an addon. |
|
* |
|
* @param aType |
|
* Overrride type - "compatible" or "incompatible" |
|
* @param aMinVersion |
|
* Minimum version of the addon to match |
|
* @param aMaxVersion |
|
* Maximum version of the addon to match |
|
* @param aAppID |
|
* Application ID used to match appMinVersion and appMaxVersion |
|
* @param aAppMinVersion |
|
* Minimum version of the application to match |
|
* @param aAppMaxVersion |
|
* Maximum version of the application to match |
|
*/ |
|
function AddonCompatibilityOverride(aType, aMinVersion, aMaxVersion, aAppID, |
|
aAppMinVersion, aAppMaxVersion) { |
|
this.type = aType; |
|
this.minVersion = aMinVersion; |
|
this.maxVersion = aMaxVersion; |
|
this.appID = aAppID; |
|
this.appMinVersion = aAppMinVersion; |
|
this.appMaxVersion = aAppMaxVersion; |
|
} |
|
|
|
AddonCompatibilityOverride.prototype = { |
|
/** |
|
* Type of override - "incompatible" or "compatible". |
|
* Only "incompatible" is supported for now. |
|
*/ |
|
type: null, |
|
|
|
/** |
|
* Min version of the addon to match. |
|
*/ |
|
minVersion: null, |
|
|
|
/** |
|
* Max version of the addon to match. |
|
*/ |
|
maxVersion: null, |
|
|
|
/** |
|
* Application ID to match. |
|
*/ |
|
appID: null, |
|
|
|
/** |
|
* Min version of the application to match. |
|
*/ |
|
appMinVersion: null, |
|
|
|
/** |
|
* Max version of the application to match. |
|
*/ |
|
appMaxVersion: null |
|
}; |
|
|
|
|
|
/** |
|
* 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 or the actual display |
|
* name if aLocaleURI is null. Include %ID% to include the type ID in |
|
* the key |
|
* @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. |
|
* @param aFlags |
|
* An option set of flags that customize the display of the add-on in |
|
* the UI. |
|
*/ |
|
function AddonType(aID, aLocaleURI, aLocaleKey, aViewType, aUIPriority, aFlags) { |
|
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; |
|
this.flags = aFlags; |
|
|
|
if (aLocaleURI) { |
|
this.__defineGetter__("name", function nameGetter() { |
|
delete this.name; |
|
let bundle = Services.strings.createBundle(aLocaleURI); |
|
this.name = bundle.GetStringFromName(aLocaleKey.replace("%ID%", aID)); |
|
return this.name; |
|
}); |
|
} |
|
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 gShutdownBarrier = null; |
|
var gRepoShutdownState = ""; |
|
var gShutdownInProgress = false; |
|
|
|
/** |
|
* This is the real manager, kept here rather than in AddonManager to keep its |
|
* contents hidden from API users. |
|
*/ |
|
var AddonManagerInternal = { |
|
managerListeners: [], |
|
installListeners: [], |
|
addonListeners: [], |
|
typeListeners: [], |
|
pendingProviders: new Set(), |
|
providers: new Set(), |
|
providerShutdowns: new Map(), |
|
types: {}, |
|
startupChanges: {}, |
|
// Store telemetry details per addon provider |
|
telemetryDetails: {}, |
|
|
|
recordTimestamp: function(name, value) { |
|
this.TelemetryTimestamps.add(name, value); |
|
}, |
|
|
|
validateBlocklist: function() { |
|
let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]); |
|
|
|
// If there is no application shipped blocklist then there is nothing to do |
|
if (!appBlocklist.exists()) |
|
return; |
|
|
|
let profileBlocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]); |
|
|
|
// If there is no blocklist in the profile then copy the application shipped |
|
// one there |
|
if (!profileBlocklist.exists()) { |
|
try { |
|
appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST); |
|
} |
|
catch (e) { |
|
logger.warn("Failed to copy the application shipped blocklist to the profile", e); |
|
} |
|
return; |
|
} |
|
|
|
let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]. |
|
createInstance(Ci.nsIFileInputStream); |
|
try { |
|
let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]. |
|
createInstance(Ci.nsIConverterInputStream); |
|
fileStream.init(appBlocklist, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); |
|
cstream.init(fileStream, "UTF-8", 0, 0); |
|
|
|
let data = ""; |
|
let str = {}; |
|
let read = 0; |
|
do { |
|
read = cstream.readString(0xffffffff, str); |
|
data += str.value; |
|
} while (read != 0); |
|
|
|
let parser = Cc["@mozilla.org/xmlextras/domparser;1"]. |
|
createInstance(Ci.nsIDOMParser); |
|
var doc = parser.parseFromString(data, "text/xml"); |
|
} |
|
catch (e) { |
|
logger.warn("Application shipped blocklist could not be loaded", e); |
|
return; |
|
} |
|
finally { |
|
try { |
|
fileStream.close(); |
|
} |
|
catch (e) { |
|
logger.warn("Unable to close blocklist file stream", e); |
|
} |
|
} |
|
|
|
// If the namespace is incorrect then ignore the application shipped |
|
// blocklist |
|
if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) { |
|
logger.warn("Application shipped blocklist has an unexpected namespace (" + |
|
doc.documentElement.namespaceURI + ")"); |
|
return; |
|
} |
|
|
|
// If there is no lastupdate information then ignore the application shipped |
|
// blocklist |
|
if (!doc.documentElement.hasAttribute("lastupdate")) |
|
return; |
|
|
|
// If the application shipped blocklist is older than the profile blocklist |
|
// then do nothing |
|
if (doc.documentElement.getAttribute("lastupdate") <= |
|
profileBlocklist.lastModifiedTime) |
|
return; |
|
|
|
// Otherwise copy the application shipped blocklist to the profile |
|
try { |
|
appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST); |
|
} |
|
catch (e) { |
|
logger.warn("Failed to copy the application shipped blocklist to the profile", e); |
|
} |
|
}, |
|
|
|
/** |
|
* Start up a provider, and register its shutdown hook if it has one |
|
*/ |
|
_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 incase 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); |
|
AddonManager.shutdown.addBlocker(name, AMProviderShutdown); |
|
} |
|
|
|
this.pendingProviders.delete(aProvider); |
|
this.providers.add(aProvider); |
|
logger.debug(`Provider finished startup: ${providerName(aProvider)}`); |
|
}, |
|
|
|
/** |
|
* Initializes the AddonManager, loading any known providers and initializing |
|
* them. |
|
*/ |
|
startup: function() { |
|
try { |
|
if (gStarted) |
|
return; |
|
|
|
this.recordTimestamp("AMI_startup_begin"); |
|
|
|
// 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) { } |
|
|
|
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)); |
|
this.validateBlocklist(); |
|
} |
|
|
|
gCheckCompatibility = Services.prefs.getBoolPref(PREF_EM_CHECK_COMPATIBILITY, |
|
gCheckCompatibility); |
|
Services.prefs.addObserver(PREF_EM_CHECK_COMPATIBILITY, this, false); |
|
|
|
gStrictCompatibility = Services.prefs.getBoolPref(PREF_EM_STRICT_COMPATIBILITY, |
|
gStrictCompatibility); |
|
Services.prefs.addObserver(PREF_EM_STRICT_COMPATIBILITY, this, false); |
|
|
|
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, false); |
|
|
|
gUpdateEnabled = Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED, gUpdateEnabled); |
|
Services.prefs.addObserver(PREF_EM_UPDATE_ENABLED, this, false); |
|
|
|
gAutoUpdateDefault = Services.prefs.getBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, |
|
gAutoUpdateDefault); |
|
Services.prefs.addObserver(PREF_EM_AUTOUPDATE_DEFAULT, this, false); |
|
|
|
let defaultProvidersEnabled = Services.prefs.getBoolPref(PREF_DEFAULT_PROVIDERS_ENABLED, true); |
|
AddonManagerPrivate.recordSimpleMeasure("default_providers", defaultProvidersEnabled); |
|
|
|
// Ensure all default providers have had a chance to register themselves |
|
if (defaultProvidersEnabled) { |
|
for (let url of DEFAULT_PROVIDERS) { |
|
try { |
|
let scope = {}; |
|
Components.utils.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 |
|
let catman = Cc["@mozilla.org/categorymanager;1"]. |
|
getService(Ci.nsICategoryManager); |
|
let entries = catman.enumerateCategory(CATEGORY_PROVIDER_MODULE); |
|
while (entries.hasMoreElements()) { |
|
let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; |
|
let url = catman.getCategoryEntry(CATEGORY_PROVIDER_MODULE, entry); |
|
|
|
try { |
|
Components.utils.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 |
|
gShutdownBarrier = 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 aProvider |
|
* The provider to register |
|
* @param aTypes |
|
* An optional array of add-on types |
|
*/ |
|
registerProvider: function(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) { |
|
aTypes.forEach(function(aType) { |
|
if (!(aType.id in this.types)) { |
|
if (!VALID_TYPES_REGEXP.test(aType.id)) { |
|
logger.warn("Ignoring invalid type " + aType.id); |
|
return; |
|
} |
|
|
|
this.types[aType.id] = { |
|
type: aType, |
|
providers: [aProvider] |
|
}; |
|
|
|
let typeListeners = this.typeListeners.slice(0); |
|
for (let listener of typeListeners) { |
|
safeCall(function listenerSafeCall() { |
|
listener.onTypeAdded(aType); |
|
}); |
|
} |
|
} |
|
else { |
|
this.types[aType.id].providers.push(aProvider); |
|
} |
|
}, this); |
|
} |
|
|
|
// 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: function(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(function filterProvider(p) p != aProvider); |
|
if (this.types[type].providers.length == 0) { |
|
let oldType = this.types[type].type; |
|
delete this.types[type]; |
|
|
|
let typeListeners = this.typeListeners.slice(0); |
|
for (let listener of typeListeners) { |
|
safeCall(function listenerSafeCall() { |
|
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 gShutdownBarrier 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); |
|
gShutdownBarrier.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: function(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: function(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(aProvider, aMethod, e); |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Report the current state of asynchronous shutdown |
|
*/ |
|
shutdownState() { |
|
let state = []; |
|
if (gShutdownBarrier) { |
|
state.push({ |
|
name: gShutdownBarrier.client.name, |
|
state: gShutdownBarrier.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 |
|
*/ |
|
shutdownManager: Task.async(function* () { |
|
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); |
|
|
|
let savedError = null; |
|
// Only shut down providers if they've been started. |
|
if (gStarted) { |
|
try { |
|
yield gShutdownBarrier.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"; |
|
yield 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.splice(0, this.managerListeners.length); |
|
this.installListeners.splice(0, this.installListeners.length); |
|
this.addonListeners.splice(0, this.addonListeners.length); |
|
this.typeListeners.splice(0, this.typeListeners.length); |
|
this.providerShutdowns.clear(); |
|
for (let type in this.startupChanges) |
|
delete this.startupChanges[type]; |
|
gStarted = false; |
|
gStartupComplete = false; |
|
gShutdownBarrier = null; |
|
gShutdownInProgress = false; |
|
if (savedError) { |
|
throw savedError; |
|
} |
|
}), |
|
|
|
/** |
|
* Notified when a preference we're interested in has changed. |
|
* |
|
* @see nsIObserver |
|
*/ |
|
observe: function(aSubject, aTopic, aData) { |
|
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: { |
|
let oldValue = gUpdateEnabled; |
|
gUpdateEnabled = Services.prefs.getBoolPref(PREF_EM_UPDATE_ENABLED, true); |
|
|
|
this.callManagerListeners("onUpdateModeChanged"); |
|
break; |
|
} |
|
case PREF_EM_AUTOUPDATE_DEFAULT: { |
|
let oldValue = gAutoUpdateDefault; |
|
gAutoUpdateDefault = Services.prefs.getBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, true); |
|
|
|
this.callManagerListeners("onUpdateModeChanged"); |
|
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: function(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"; |
|
if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) |
|
addonStatus += ",blocklisted"; |
|
if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) |
|
addonStatus += ",softblocked"; |
|
|
|
try { |
|
var xpcomABI = Services.appinfo.XPCOMABI; |
|
} catch (ex) { |
|
xpcomABI = UNKNOWN_XPCOM_ABI; |
|
} |
|
|
|
let uri = aUri.replace(/%ITEM_ID%/g, aAddon.id); |
|
uri = uri.replace(/%ITEM_VERSION%/g, aAddon.version); |
|
uri = uri.replace(/%ITEM_STATUS%/g, addonStatus); |
|
uri = uri.replace(/%APP_ID%/g, Services.appinfo.ID); |
|
uri = uri.replace(/%APP_VERSION%/g, aAppVersion ? aAppVersion : |
|
Services.appinfo.version); |
|
uri = uri.replace(/%REQ_VERSION%/g, UPDATE_REQUEST_VERSION); |
|
uri = uri.replace(/%APP_OS%/g, Services.appinfo.OS); |
|
uri = uri.replace(/%APP_ABI%/g, xpcomABI); |
|
uri = uri.replace(/%APP_LOCALE%/g, getLocale()); |
|
uri = uri.replace(/%CURRENT_APP_VERSION%/g, Services.appinfo.version); |
|
|
|
// Replace custom parameters (names of custom parameters must have at |
|
// least 3 characters to prevent lookups for something like %D0%C8) |
|
var catMan = null; |
|
uri = uri.replace(/%(\w{3,})%/g, function parameterReplace(aMatch, aParam) { |
|
if (!catMan) { |
|
catMan = Cc["@mozilla.org/categorymanager;1"]. |
|
getService(Ci.nsICategoryManager); |
|
} |
|
|
|
try { |
|
var contractID = catMan.getCategoryEntry(CATEGORY_UPDATE_PARAMS, aParam); |
|
var paramHandler = Cc[contractID].getService(Ci.nsIPropertyBag2); |
|
return paramHandler.getPropertyAsAString(aParam); |
|
} |
|
catch(e) { |
|
return aMatch; |
|
} |
|
}); |
|
|
|
// escape() does not properly encode + symbols in any embedded FVF strings. |
|
return uri.replace(/\+/g, "%2B"); |
|
}, |
|
|
|
/** |
|
* 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: function() { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
let buPromise = Task.spawn(function* backgroundUpdateTask() { |
|
logger.debug("Background update check beginning"); |
|
|
|
Services.obs.notifyObservers(null, "addons-background-update-start", null); |
|
|
|
if (this.updateEnabled) { |
|
let scope = {}; |
|
Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", scope); |
|
scope.LightweightThemeManager.updateCurrentTheme(); |
|
|
|
let allAddons = yield new Promise((resolve, reject) => this.getAllAddons(resolve)); |
|
|
|
// Repopulate repository cache first, to ensure compatibility overrides |
|
// are up to date before checking for addon updates. |
|
yield AddonRepository.backgroundUpdateCheck(); |
|
|
|
// Keep track of all the async add-on updates happening in parallel |
|
let updates = []; |
|
|
|
for (let addon of allAddons) { |
|
// Check all add-ons for updates so that any compatibility updates will |
|
// be applied |
|
updates.push(new Promise((resolve, reject) => { |
|
addon.findUpdates({ |
|
onUpdateAvailable: function(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 (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE && |
|
AddonManager.shouldAutoUpdate(aAddon)) { |
|
// XXX we really should resolve when this install is done, |
|
// not when update-available check completes, no? |
|
logger.debug("Starting install of ${id}", aAddon); |
|
aInstall.install(); |
|
} |
|
}, |
|
|
|
onUpdateFinished: aAddon => { logger.debug("onUpdateFinished for ${id}", aAddon); resolve(); } |
|
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE); |
|
})); |
|
} |
|
yield Promise.all(updates); |
|
} |
|
|
|
logger.debug("Background update check complete"); |
|
Services.obs.notifyObservers(null, |
|
"addons-background-update-complete", |
|
null); |
|
}.bind(this)); |
|
// Fork the promise chain so we can log the error and let our caller see it too. |
|
buPromise.then(null, 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: function(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; |
|
|
|
// 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: function(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( |
|
function filterItem(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: function(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 = this.managerListeners.slice(0); |
|
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: function(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 = aExtraListeners.concat(this.installListeners); |
|
else |
|
listeners = this.installListeners.slice(0); |
|
|
|
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: function(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 = this.addonListeners.slice(0); |
|
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 |
|
*/ |
|
notifyAddonChanged: function(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. Furthermore, LightweightThemeManager may |
|
// not be initialized until after XPIProvider is, and therefore would also |
|
// be unaccessible during XPIProvider startup. Thankfully, these are the |
|
// only two uses 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) { |
|
callProvider(provider, "addonChanged", null, aID, aType, aPendingRestart); |
|
} |
|
}, |
|
|
|
/** |
|
* Notifies all providers they need to update the appDisabled property for |
|
* their add-ons in response to an application change such as a blocklist |
|
* update. |
|
*/ |
|
updateAddonAppDisabledStates: function() { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
this.callProviders("updateAddonAppDisabledStates"); |
|
}, |
|
|
|
/** |
|
* Notifies all providers that the repository has updated its data for |
|
* installed add-ons. |
|
* |
|
* @param aCallback |
|
* Function to call when operation is complete. |
|
*/ |
|
updateAddonRepositoryData: function(aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
new AsyncObjectCaller(this.providers, "updateAddonRepositoryData", { |
|
nextObject: function(aCaller, aProvider) { |
|
callProviderAsync(aProvider, "updateAddonRepositoryData", |
|
aCaller.callNext.bind(aCaller)); |
|
}, |
|
noMoreObjects: function(aCaller) { |
|
safeCall(aCallback); |
|
// only tests should care about this |
|
Services.obs.notifyObservers(null, "TEST:addon-repository-data-updated", null); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets an AddonInstall for a URL. |
|
* |
|
* @param aUrl |
|
* The string represenation of the URL the add-on is located at |
|
* @param aCallback |
|
* A callback to pass the AddonInstall to |
|
* @param aMimetype |
|
* The mimetype of the add-on |
|
* @param aHash |
|
* An optional hash of the add-on |
|
* @param aName |
|
* An optional placeholder name while the add-on is being downloaded |
|
* @param aIcons |
|
* Optional placeholder icons while the add-on is being downloaded |
|
* @param aVersion |
|
* An optional placeholder version while the add-on is being downloaded |
|
* @param aLoadGroup |
|
* An optional nsILoadGroup to associate any network requests with |
|
* @throws if the aUrl, aCallback or aMimetype arguments are not specified |
|
*/ |
|
getInstallForURL: function(aUrl, aCallback, aMimetype, |
|
aHash, aName, aIcons, |
|
aVersion, aBrowser) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (!aUrl || typeof aUrl != "string") |
|
throw Components.Exception("aURL must be a non-empty string", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!aMimetype || typeof aMimetype != "string") |
|
throw Components.Exception("aMimetype must be a non-empty string", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (aHash && typeof aHash != "string") |
|
throw Components.Exception("aHash must be a string or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (aName && typeof aName != "string") |
|
throw Components.Exception("aName must be a string or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (aIcons) { |
|
if (typeof aIcons == "string") |
|
aIcons = { "32": aIcons }; |
|
else if (typeof aIcons != "object") |
|
throw Components.Exception("aIcons must be a string, an object or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
} else { |
|
aIcons = {}; |
|
} |
|
|
|
if (aVersion && typeof aVersion != "string") |
|
throw Components.Exception("aVersion must be a string or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (aBrowser && (!(aBrowser instanceof Ci.nsIDOMElement))) |
|
throw Components.Exception("aBrowser must be a nsIDOMElement or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let providers = [...this.providers]; |
|
for (let provider of providers) { |
|
if (callProvider(provider, "supportsMimetype", false, aMimetype)) { |
|
callProviderAsync(provider, "getInstallForURL", |
|
aUrl, aHash, aName, aIcons, aVersion, aBrowser, |
|
function getInstallForURL_safeCall(aInstall) { |
|
safeCall(aCallback, aInstall); |
|
}); |
|
return; |
|
} |
|
} |
|
safeCall(aCallback, null); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets an AddonInstall for an nsIFile. |
|
* |
|
* @param aFile |
|
* The nsIFile where the add-on is located |
|
* @param aCallback |
|
* A callback to pass the AddonInstall to |
|
* @param aMimetype |
|
* An optional mimetype hint for the add-on |
|
* @throws if the aFile or aCallback arguments are not specified |
|
*/ |
|
getInstallForFile: function(aFile, aCallback, aMimetype) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (!(aFile instanceof Ci.nsIFile)) |
|
throw Components.Exception("aFile must be a nsIFile", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (aMimetype && typeof aMimetype != "string") |
|
throw Components.Exception("aMimetype must be a string or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
new AsyncObjectCaller(this.providers, "getInstallForFile", { |
|
nextObject: function(aCaller, aProvider) { |
|
callProviderAsync(aProvider, "getInstallForFile", aFile, |
|
function getInstallForFile_safeCall(aInstall) { |
|
if (aInstall) |
|
safeCall(aCallback, aInstall); |
|
else |
|
aCaller.callNext(); |
|
}); |
|
}, |
|
|
|
noMoreObjects: function(aCaller) { |
|
safeCall(aCallback, null); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets all current AddonInstalls optionally limiting to a list |
|
* of types. |
|
* |
|
* @param aTypes |
|
* An optional array of types to retrieve. Each type is a string name |
|
* @param aCallback |
|
* A callback which will be passed an array of AddonInstalls |
|
* @throws If the aCallback argument is not specified |
|
*/ |
|
getInstallsByTypes: function(aTypes, aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (aTypes && !Array.isArray(aTypes)) |
|
throw Components.Exception("aTypes must be an array or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let installs = []; |
|
|
|
new AsyncObjectCaller(this.providers, "getInstallsByTypes", { |
|
nextObject: function(aCaller, aProvider) { |
|
callProviderAsync(aProvider, "getInstallsByTypes", aTypes, |
|
function getInstallsByTypes_safeCall(aProviderInstalls) { |
|
if (aProviderInstalls) { |
|
installs = installs.concat(aProviderInstalls); |
|
} |
|
aCaller.callNext(); |
|
}); |
|
}, |
|
|
|
noMoreObjects: function(aCaller) { |
|
safeCall(aCallback, installs); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets all current AddonInstalls. |
|
* |
|
* @param aCallback |
|
* A callback which will be passed an array of AddonInstalls |
|
*/ |
|
getAllInstalls: function(aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
this.getInstallsByTypes(null, aCallback); |
|
}, |
|
|
|
/** |
|
* Synchronously map a URI to the corresponding Addon ID. |
|
* |
|
* Mappable URIs are limited to in-application resources belonging to the |
|
* add-on, such as Javascript compartments, XUL windows, XBL bindings, etc. |
|
* but do not include URIs from meta data, such as the add-on homepage. |
|
* |
|
* @param aURI |
|
* nsIURI to map to an addon id |
|
* @return string containing the Addon ID or null |
|
* @see amIAddonManager.mapURIToAddonID |
|
*/ |
|
mapURIToAddonID: function(aURI) { |
|
if (!(aURI instanceof Ci.nsIURI)) { |
|
throw Components.Exception("aURI is not a nsIURI", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
} |
|
|
|
// Try all providers |
|
let providers = [...this.providers]; |
|
for (let provider of providers) { |
|
var id = callProvider(provider, "mapURIToAddonID", null, aURI); |
|
if (id !== null) { |
|
return id; |
|
} |
|
} |
|
|
|
return null; |
|
}, |
|
|
|
/** |
|
* Checks whether installation is enabled for a particular mimetype. |
|
* |
|
* @param aMimetype |
|
* The mimetype to check |
|
* @return true if installation is enabled for the mimetype |
|
*/ |
|
isInstallEnabled: function(aMimetype) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (!aMimetype || typeof aMimetype != "string") |
|
throw Components.Exception("aMimetype must be a non-empty string", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let providers = [...this.providers]; |
|
for (let provider of providers) { |
|
if (callProvider(provider, "supportsMimetype", false, aMimetype) && |
|
callProvider(provider, "isInstallEnabled")) |
|
return true; |
|
} |
|
return false; |
|
}, |
|
|
|
/** |
|
* Checks whether a particular source is allowed to install add-ons of a |
|
* given mimetype. |
|
* |
|
* @param aMimetype |
|
* The mimetype of the add-on |
|
* @param aInstallingPrincipal |
|
* The nsIPrincipal that initiated the install |
|
* @return true if the source is allowed to install this mimetype |
|
*/ |
|
isInstallAllowed: function(aMimetype, aInstallingPrincipal) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (!aMimetype || typeof aMimetype != "string") |
|
throw Components.Exception("aMimetype must be a non-empty string", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal)) |
|
throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let providers = [...this.providers]; |
|
for (let provider of providers) { |
|
if (callProvider(provider, "supportsMimetype", false, aMimetype) && |
|
callProvider(provider, "isInstallAllowed", null, aInstallingPrincipal)) |
|
return true; |
|
} |
|
return false; |
|
}, |
|
|
|
/** |
|
* Starts installation of an array of AddonInstalls notifying the registered |
|
* web install listener of blocked or started installs. |
|
* |
|
* @param aMimetype |
|
* The mimetype of add-ons being installed |
|
* @param aBrowser |
|
* The optional browser element that started the installs |
|
* @param aInstallingPrincipal |
|
* The nsIPrincipal that initiated the install |
|
* @param aInstalls |
|
* The array of AddonInstalls to be installed |
|
*/ |
|
installAddonsFromWebpage: function(aMimetype, aBrowser, aInstallingPrincipal, aInstalls) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (!aMimetype || typeof aMimetype != "string") |
|
throw Components.Exception("aMimetype must be a non-empty string", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (aBrowser && !(aBrowser instanceof Ci.nsIDOMElement)) |
|
throw Components.Exception("aSource must be a nsIDOMElement, or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!aInstallingPrincipal || !(aInstallingPrincipal instanceof Ci.nsIPrincipal)) |
|
throw Components.Exception("aInstallingPrincipal must be a nsIPrincipal", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!Array.isArray(aInstalls)) |
|
throw Components.Exception("aInstalls must be an array", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!("@mozilla.org/addons/web-install-listener;1" in Cc)) { |
|
logger.warn("No web installer available, cancelling all installs"); |
|
aInstalls.forEach(function(aInstall) { |
|
aInstall.cancel(); |
|
}); |
|
return; |
|
} |
|
|
|
// When a chrome in-content UI has loaded a <browser> inside to host a |
|
// website we want to do our security checks on the inner-browser but |
|
// notify front-end that install events came from the outer-browser (the |
|
// main tab's browser). Check this by seeing if the browser we've been |
|
// passed is in a content type docshell and if so get the outer-browser. |
|
let topBrowser = aBrowser; |
|
let docShell = aBrowser.ownerDocument.defaultView |
|
.QueryInterface(Ci.nsIInterfaceRequestor) |
|
.getInterface(Ci.nsIDocShell) |
|
.QueryInterface(Ci.nsIDocShellTreeItem); |
|
if (docShell.itemType == Ci.nsIDocShellTreeItem.typeContent) |
|
topBrowser = docShell.chromeEventHandler; |
|
|
|
try { |
|
let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"]. |
|
getService(Ci.amIWebInstallListener); |
|
|
|
if (!this.isInstallEnabled(aMimetype)) { |
|
for (let install of aInstalls) |
|
install.cancel(); |
|
|
|
weblistener.onWebInstallDisabled(topBrowser, aInstallingPrincipal.URI, |
|
aInstalls, aInstalls.length); |
|
return; |
|
} |
|
else if (!aBrowser.contentPrincipal || !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)) { |
|
for (let install of aInstalls) |
|
install.cancel(); |
|
|
|
if (weblistener instanceof Ci.amIWebInstallListener2) { |
|
weblistener.onWebInstallOriginBlocked(topBrowser, aInstallingPrincipal.URI, |
|
aInstalls, aInstalls.length); |
|
} |
|
return; |
|
} |
|
|
|
// The installs may start now depending on the web install listener, |
|
// listen for the browser navigating to a new origin and cancel the |
|
// installs in that case. |
|
new BrowserListener(aBrowser, aInstallingPrincipal, aInstalls); |
|
|
|
if (!this.isInstallAllowed(aMimetype, aInstallingPrincipal)) { |
|
if (weblistener.onWebInstallBlocked(topBrowser, aInstallingPrincipal.URI, |
|
aInstalls, aInstalls.length)) { |
|
aInstalls.forEach(function(aInstall) { |
|
aInstall.install(); |
|
}); |
|
} |
|
} |
|
else if (weblistener.onWebInstallRequested(topBrowser, aInstallingPrincipal.URI, |
|
aInstalls, aInstalls.length)) { |
|
aInstalls.forEach(function(aInstall) { |
|
aInstall.install(); |
|
}); |
|
} |
|
} |
|
catch (e) { |
|
// In the event that the weblistener throws during instantiation or when |
|
// calling onWebInstallBlocked or onWebInstallRequested all of the |
|
// installs should get cancelled. |
|
logger.warn("Failure calling web installer", e); |
|
aInstalls.forEach(function(aInstall) { |
|
aInstall.cancel(); |
|
}); |
|
} |
|
}, |
|
|
|
/** |
|
* Adds a new InstallListener if the listener is not already registered. |
|
* |
|
* @param aListener |
|
* The InstallListener to add |
|
*/ |
|
addInstallListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be a InstallListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!this.installListeners.some(function addInstallListener_matchListener(i) { |
|
return i == aListener; })) |
|
this.installListeners.push(aListener); |
|
}, |
|
|
|
/** |
|
* Removes an InstallListener if the listener is registered. |
|
* |
|
* @param aListener |
|
* The InstallListener to remove |
|
*/ |
|
removeInstallListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be a InstallListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let pos = 0; |
|
while (pos < this.installListeners.length) { |
|
if (this.installListeners[pos] == aListener) |
|
this.installListeners.splice(pos, 1); |
|
else |
|
pos++; |
|
} |
|
}, |
|
|
|
/** |
|
* Asynchronously gets an add-on with a specific ID. |
|
* |
|
* @param aID |
|
* The ID of the add-on to retrieve |
|
* @param aCallback |
|
* The callback to pass the retrieved add-on to |
|
* @throws if the aID or aCallback arguments are not specified |
|
*/ |
|
getAddonByID: function(aID, aCallback) { |
|
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 non-empty string", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
new AsyncObjectCaller(this.providers, "getAddonByID", { |
|
nextObject: function(aCaller, aProvider) { |
|
callProviderAsync(aProvider, "getAddonByID", aID, |
|
function getAddonByID_safeCall(aAddon) { |
|
if (aAddon) |
|
safeCall(aCallback, aAddon); |
|
else |
|
aCaller.callNext(); |
|
}); |
|
}, |
|
|
|
noMoreObjects: function(aCaller) { |
|
safeCall(aCallback, null); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Asynchronously get an add-on with a specific Sync GUID. |
|
* |
|
* @param aGUID |
|
* String GUID of add-on to retrieve |
|
* @param aCallback |
|
* The callback to pass the retrieved add-on to. |
|
* @throws if the aGUID or aCallback arguments are not specified |
|
*/ |
|
getAddonBySyncGUID: function(aGUID, aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (!aGUID || typeof aGUID != "string") |
|
throw Components.Exception("aGUID must be a non-empty string", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
new AsyncObjectCaller(this.providers, "getAddonBySyncGUID", { |
|
nextObject: function(aCaller, aProvider) { |
|
callProviderAsync(aProvider, "getAddonBySyncGUID", aGUID, |
|
function getAddonBySyncGUID_safeCall(aAddon) { |
|
if (aAddon) { |
|
safeCall(aCallback, aAddon); |
|
} else { |
|
aCaller.callNext(); |
|
} |
|
}); |
|
}, |
|
|
|
noMoreObjects: function(aCaller) { |
|
safeCall(aCallback, null); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets an array of add-ons. |
|
* |
|
* @param aIDs |
|
* The array of IDs to retrieve |
|
* @param aCallback |
|
* The callback to pass an array of Addons to |
|
* @throws if the aID or aCallback arguments are not specified |
|
*/ |
|
getAddonsByIDs: function(aIDs, aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (!Array.isArray(aIDs)) |
|
throw Components.Exception("aIDs must be an array", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let addons = []; |
|
|
|
new AsyncObjectCaller(aIDs, null, { |
|
nextObject: function(aCaller, aID) { |
|
AddonManagerInternal.getAddonByID(aID, |
|
function getAddonsByIDs_getAddonByID(aAddon) { |
|
addons.push(aAddon); |
|
aCaller.callNext(); |
|
}); |
|
}, |
|
|
|
noMoreObjects: function(aCaller) { |
|
safeCall(aCallback, addons); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets add-ons of specific types. |
|
* |
|
* @param aTypes |
|
* An optional array of types to retrieve. Each type is a string name |
|
* @param aCallback |
|
* The callback to pass an array of Addons to. |
|
* @throws if the aCallback argument is not specified |
|
*/ |
|
getAddonsByTypes: function(aTypes, aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (aTypes && !Array.isArray(aTypes)) |
|
throw Components.Exception("aTypes must be an array or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let addons = []; |
|
|
|
new AsyncObjectCaller(this.providers, "getAddonsByTypes", { |
|
nextObject: function(aCaller, aProvider) { |
|
callProviderAsync(aProvider, "getAddonsByTypes", aTypes, |
|
function getAddonsByTypes_concatAddons(aProviderAddons) { |
|
if (aProviderAddons) { |
|
addons = addons.concat(aProviderAddons); |
|
} |
|
aCaller.callNext(); |
|
}); |
|
}, |
|
|
|
noMoreObjects: function(aCaller) { |
|
safeCall(aCallback, addons); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets all installed add-ons. |
|
* |
|
* @param aCallback |
|
* A callback which will be passed an array of Addons |
|
*/ |
|
getAllAddons: function(aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
this.getAddonsByTypes(null, aCallback); |
|
}, |
|
|
|
/** |
|
* Asynchronously gets add-ons that have operations waiting for an application |
|
* restart to complete. |
|
* |
|
* @param aTypes |
|
* An optional array of types to retrieve. Each type is a string name |
|
* @param aCallback |
|
* The callback to pass the array of Addons to |
|
* @throws if the aCallback argument is not specified |
|
*/ |
|
getAddonsWithOperationsByTypes: function(aTypes, aCallback) { |
|
if (!gStarted) |
|
throw Components.Exception("AddonManager is not initialized", |
|
Cr.NS_ERROR_NOT_INITIALIZED); |
|
|
|
if (aTypes && !Array.isArray(aTypes)) |
|
throw Components.Exception("aTypes must be an array or null", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (typeof aCallback != "function") |
|
throw Components.Exception("aCallback must be a function", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let addons = []; |
|
|
|
new AsyncObjectCaller(this.providers, "getAddonsWithOperationsByTypes", { |
|
nextObject: function(aCaller, aProvider) { |
|
callProviderAsync(aProvider, "getAddonsWithOperationsByTypes", aTypes, |
|
function getAddonsWithOperationsByTypes_concatAddons |
|
(aProviderAddons) { |
|
if (aProviderAddons) { |
|
addons = addons.concat(aProviderAddons); |
|
} |
|
aCaller.callNext(); |
|
}); |
|
}, |
|
|
|
noMoreObjects: function(caller) { |
|
safeCall(aCallback, addons); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
* Adds a new AddonManagerListener if the listener is not already registered. |
|
* |
|
* @param aListener |
|
* The listener to add |
|
*/ |
|
addManagerListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be an AddonManagerListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!this.managerListeners.some(function addManagerListener_matchListener(i) { |
|
return i == aListener; })) |
|
this.managerListeners.push(aListener); |
|
}, |
|
|
|
/** |
|
* Removes an AddonManagerListener if the listener is registered. |
|
* |
|
* @param aListener |
|
* The listener to remove |
|
*/ |
|
removeManagerListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be an AddonManagerListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let pos = 0; |
|
while (pos < this.managerListeners.length) { |
|
if (this.managerListeners[pos] == aListener) |
|
this.managerListeners.splice(pos, 1); |
|
else |
|
pos++; |
|
} |
|
}, |
|
|
|
/** |
|
* Adds a new AddonListener if the listener is not already registered. |
|
* |
|
* @param aListener |
|
* The AddonListener to add |
|
*/ |
|
addAddonListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be an AddonListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!this.addonListeners.some(function addAddonListener_matchListener(i) { |
|
return i == aListener; })) |
|
this.addonListeners.push(aListener); |
|
}, |
|
|
|
/** |
|
* Removes an AddonListener if the listener is registered. |
|
* |
|
* @param aListener |
|
* The AddonListener to remove |
|
*/ |
|
removeAddonListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be an AddonListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let pos = 0; |
|
while (pos < this.addonListeners.length) { |
|
if (this.addonListeners[pos] == aListener) |
|
this.addonListeners.splice(pos, 1); |
|
else |
|
pos++; |
|
} |
|
}, |
|
|
|
/** |
|
* Adds a new TypeListener if the listener is not already registered. |
|
* |
|
* @param aListener |
|
* The TypeListener to add |
|
*/ |
|
addTypeListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be a TypeListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
if (!this.typeListeners.some(function addTypeListener_matchListener(i) { |
|
return i == aListener; })) |
|
this.typeListeners.push(aListener); |
|
}, |
|
|
|
/** |
|
* Removes an TypeListener if the listener is registered. |
|
* |
|
* @param aListener |
|
* The TypeListener to remove |
|
*/ |
|
removeTypeListener: function(aListener) { |
|
if (!aListener || typeof aListener != "object") |
|
throw Components.Exception("aListener must be a TypeListener object", |
|
Cr.NS_ERROR_INVALID_ARG); |
|
|
|
let pos = 0; |
|
while (pos < this.typeListeners.length) { |
|
if (this.typeListeners[pos] == aListener) |
|
this.typeListeners.splice(pos, 1); |
|
else |
|
pos++; |
|
} |
|
}, |
|
|
|
get addonTypes() { |
|
// A read-only wrapper around the types dictionary |
|
return new Proxy(this.types, { |
|
defineProperty(target, property, descriptor) { |
|
// Not allowed to define properties |
|
return false; |
|
}, |
|
|
|
deleteProperty(target, property) { |
|
// Not allowed to delete properties |
|
return false; |
|
}, |
|
|
|
get(target, property, receiver) { |
|
if (!target.hasOwnProperty(property)) |
|
return undefined; |
|
|
|
return target[property].type; |
|
}, |
|
|
|
getOwnPropertyDescriptor(target, property) { |
|
if (!target.hasOwnProperty(property)) |
|
return undefined; |
|
|
|
return { |
|
value: target[property].type, |
|
writable: false, |
|
// Claim configurability to maintain the proxy invariants. |
|
configurable: true, |
|
enumerable: true |
|
} |
|
}, |
|
|
|
preventExtensions(target) { |
|
// Not allowed to prevent adding new properties |
|
return false; |
|
}, |
|
|
|
set(target, property, value, receiver) { |
|
// Not allowed to set properties |
|
return false; |
|
}, |
|
|
|
setPrototypeOf(target, prototype) { |
|
// Not allowed to change prototype |
|
return false; |
|
} |
|
}); |
|
}, |
|
|
|
get autoUpdateDefault() { |
|
return gAutoUpdateDefault; |
|
}, |
|
|
|
set autoUpdateDefault(aValue) { |
|
aValue = !!aValue; |
|
if (aValue != gAutoUpdateDefault) |
|
Services.prefs.setBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, aValue); |
|
return aValue; |
|
}, |
|
|
|
get checkCompatibility() { |
|
return gCheckCompatibility; |
|
}, |
|
|
|
set checkCompatibility(aValue) { |
|
aValue = !!aValue; |
|
if (aValue != gCheckCompatibility) { |
|
if (!aValue) |
|
Services.prefs.setBoolPref(PREF_EM_CHECK_COMPATIBILITY, false); |
|
else |
|
Services.prefs.clearUserPref(PREF_EM_CHECK_COMPATIBILITY); |
|
} |
|
return aValue; |
|
}, |
|
|
|
get strictCompatibility() { |
|
return gStrictCompatibility; |
|
}, |
|
|
|
set strictCompatibility(aValue) { |
|
aValue = !!aValue; |
|
if (aValue != gStrictCompatibility) |
|
Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, aValue); |
|
return aValue; |
|
}, |
|
|
|
get checkUpdateSecurityDefault() { |
|
return gCheckUpdateSecurityDefault; |
|
}, |
|
|
|
get checkUpdateSecurity() { |
|
return gCheckUpdateSecurity; |
|
}, |
|
|
|
set checkUpdateSecurity(aValue) { |
|
aValue = !!aValue; |
|
if (aValue != gCheckUpdateSecurity) { |
|
if (aValue != gCheckUpdateSecurityDefault) |
|
Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, aValue); |
|
else |
|
Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY); |
|
} |
|
return aValue; |
|
}, |
|
|
|
get updateEnabled() { |
|
return gUpdateEnabled; |
|
}, |
|
|
|
set updateEnabled(aValue) { |
|
aValue = !!aValue; |
|
if (aValue != gUpdateEnabled) |
|
Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue); |
|
return aValue; |
|
}, |
|
}; |
|
|
|
/** |
|
* Should not be used outside of core Mozilla code. This is a private API for |
|
* the startup and platform integration code to use. Refer to the methods on |
|
* AddonManagerInternal for documentation however note that these methods are |
|
* subject to change at any time. |
|
*/ |
|
this.AddonManagerPrivate = { |
|
startup: function() { |
|
AddonManagerInternal.startup(); |
|
}, |
|
|
|
registerProvider: function(aProvider, aTypes) { |
|
AddonManagerInternal.registerProvider(aProvider, aTypes); |
|
}, |
|
|
|
unregisterProvider: function(aProvider) { |
|
AddonManagerInternal.unregisterProvider(aProvider); |
|
}, |
|
|
|
markProviderSafe: function(aProvider) { |
|
AddonManagerInternal.markProviderSafe(aProvider); |
|
}, |
|
|
|
backgroundUpdateCheck: function() { |
|
return AddonManagerInternal.backgroundUpdateCheck(); |
|
}, |
|
|
|
backgroundUpdateTimerHandler() { |
|
// Don't call through to the real update check if no checks are enabled. |
|
if (!AddonManagerInternal.updateEnabled) { |
|
logger.info("Skipping background update check"); |
|
return; |
|
} |
|
// Don't return the promise here, since the caller doesn't care. |
|
AddonManagerInternal.backgroundUpdateCheck(); |
|
}, |
|
|
|
addStartupChange: function(aType, aID) { |
|
AddonManagerInternal.addStartupChange(aType, aID); |
|
}, |
|
|
|
removeStartupChange: function(aType, aID) { |
|
AddonManagerInternal.removeStartupChange(aType, aID); |
|
}, |
|
|
|
notifyAddonChanged: function(aID, aType, aPendingRestart) { |
|
AddonManagerInternal.notifyAddonChanged(aID, aType, aPendingRestart); |
|
}, |
|
|
|
updateAddonAppDisabledStates: function() { |
|
AddonManagerInternal.updateAddonAppDisabledStates(); |
|
}, |
|
|
|
updateAddonRepositoryData: function(aCallback) { |
|
AddonManagerInternal.updateAddonRepositoryData(aCallback); |
|
}, |
|
|
|
callInstallListeners: function(...aArgs) { |
|
return AddonManagerInternal.callInstallListeners.apply(AddonManagerInternal, |
|
aArgs); |
|
}, |
|
|
|
callAddonListeners: function(...aArgs) { |
|
AddonManagerInternal.callAddonListeners.apply(AddonManagerInternal, aArgs); |
|
}, |
|
|
|
AddonAuthor: AddonAuthor, |
|
|
|
AddonScreenshot: AddonScreenshot, |
|
|
|
AddonCompatibilityOverride: AddonCompatibilityOverride, |
|
|
|
AddonType: AddonType, |
|
|
|
recordTimestamp: function(name, value) { |
|
|