--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -2087,35 +2087,16 @@ function getDirectoryEntries(aDir, aSort
finally {
if (dirEnum) {
dirEnum.close();
}
}
}
/**
- * Wraps a function in an exception handler to protect against exceptions inside callbacks
- * @param aFunction function(args...)
- * @return function(args...), a function that takes the same arguments as aFunction
- * and returns the same result unless aFunction throws, in which case it logs
- * a warning and returns undefined.
- */
-function makeSafe(aFunction) {
- return function(...aArgs) {
- try {
- return aFunction(...aArgs);
- }
- catch (ex) {
- logger.warn("XPIProvider callback failed", ex);
- }
- return undefined;
- }
-}
-
-/**
* Record a bit of per-addon telemetry
* @param aAddon the addon to record
*/
function recordAddonTelemetry(aAddon) {
let locale = aAddon.defaultLocale;
if (locale) {
if (locale.name)
XPIProvider.setTelemetry(aAddon.id, "name", locale.name);
@@ -3537,18 +3518,17 @@ this.XPIProvider = {
source: stageDirEntry,
existingAddonID
});
}
catch (e) {
logger.error("Failed to install staged add-on " + id + " in " + location.name,
e);
// Re-create the staged install
- AddonInstall.createStagedInstall(location, stageDirEntry,
- addon);
+ new StagedAddonInstall(location, stageDirEntry, addon);
// Make sure not to delete the cached manifest json file
seenFiles.pop();
delete aManifests[location.name][id];
if (oldBootstrap) {
// Re-install the old add-on
this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
@@ -3998,36 +3978,33 @@ this.XPIProvider = {
* A version for the install
* @param aBrowser
* The browser performing the install
* @param aCallback
* A callback to pass the AddonInstall to
*/
getInstallForURL: function(aUrl, aHash, aName, aIcons, aVersion, aBrowser,
aCallback) {
- AddonInstall.createDownload(function(aInstall) {
+ createDownloadInstall(function(aInstall) {
aCallback(aInstall.wrapper);
}, aUrl, aHash, aName, aIcons, aVersion, aBrowser);
},
/**
* Called to get an AddonInstall to install an add-on from a local file.
*
* @param aFile
* The file to be installed
* @param aCallback
* A callback to pass the AddonInstall to
*/
getInstallForFile: function(aFile, aCallback) {
- AddonInstall.createInstall(function(aInstall) {
- if (aInstall)
- aCallback(aInstall.wrapper);
- else
- aCallback(null);
- }, aFile);
+ createLocalInstall(aFile).then(install => {
+ aCallback(install ? install.wrapper : null);
+ });
},
/**
* Temporarily installs add-on from a local XPI file or directory.
* As this is intended for development, the signature is not checked and
* the add-on does not persist on application restart.
*
* @param aFile
@@ -5354,298 +5331,112 @@ function getHashStringForCrypto(aCrypto)
// convert the binary hash data to a hex string.
let binary = aCrypto.finish(false);
let hash = Array.from(binary, c => toHexString(c.charCodeAt(0)))
return hash.join("").toLowerCase();
}
/**
- * Instantiates an AddonInstall.
- *
- * @param aInstallLocation
- * The install location the add-on will be installed into
- * @param aUrl
- * The nsIURL to get the add-on from. If this is an nsIFileURL then
- * the add-on will not need to be downloaded
- * @param aHash
- * An optional hash for the add-on
- * @param aReleaseNotesURI
- * An optional nsIURI of release notes for the add-on
- * @param aExistingAddon
- * The add-on this install will update if known
- * @param aBrowser
- * The browser performing the install
- * @throws if the url is the url of a local file and the hash does not match
- * or the add-on does not contain an valid install manifest
+ * Base class for objects that manage the installation of an addon.
+ * This class isn't instantiated directly, see the derived classes below.
*/
-function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI,
- aExistingAddon, aBrowser) {
- this.wrapper = new AddonInstallWrapper(this);
- this.installLocation = aInstallLocation;
- this.sourceURI = aUrl;
- this.releaseNotesURI = aReleaseNotesURI;
- if (aHash) {
- let hashSplit = aHash.toLowerCase().split(":");
- this.originalHash = {
- algorithm: hashSplit[0],
- data: hashSplit[1]
- };
- }
- this.hash = this.originalHash;
- this.browser = aBrowser;
- this.listeners = [];
- this.icons = {};
- this.existingAddon = aExistingAddon;
- this.error = 0;
- this.window = aBrowser ? aBrowser.contentWindow : null;
-
- // Giving each instance of AddonInstall a reference to the logger.
- this.logger = logger;
-
- XPIProvider.installs.add(this);
-}
-
-AddonInstall.prototype = {
- installLocation: null,
- wrapper: null,
- stream: null,
- crypto: null,
- originalHash: null,
- hash: null,
- browser: null,
- badCertHandler: null,
- listeners: null,
- restartDownload: false,
-
- name: null,
- type: null,
- version: null,
- icons: null,
- releaseNotesURI: null,
- sourceURI: null,
- file: null,
- ownsTempFile: false,
- certificate: null,
- certName: null,
-
- linkedInstalls: null,
- existingAddon: null,
- addon: null,
-
- state: null,
- error: null,
- progress: null,
- maxProgress: null,
-
- /**
- * Initialises this install to be a staged install waiting to be applied
+class AddonInstall {
+ /**
+ * Instantiates an AddonInstall.
*
- * @param aManifest
- * The cached manifest for the staged install
- */
- initStagedInstall: function(aManifest) {
- this.name = aManifest.name;
- this.type = aManifest.type;
- this.version = aManifest.version;
- this.icons = aManifest.icons;
- this.releaseNotesURI = aManifest.releaseNotesURI ?
- NetUtil.newURI(aManifest.releaseNotesURI) :
- null;
- this.sourceURI = aManifest.sourceURI ?
- NetUtil.newURI(aManifest.sourceURI) :
- null;
- this.file = this.sourceURI;
- this._sourceBundle = this.sourceURI;
- this.addon = aManifest;
- this.addon.sourceURI = this.sourceURI;
-
- this.state = AddonManager.STATE_INSTALLED;
+ * @param aInstallLocation
+ * The install location the add-on will be installed into
+ * @param aUrl
+ * The nsIURL to get the add-on from. If this is an nsIFileURL then
+ * the add-on will not need to be downloaded
+ * @param aHash
+ * An optional hash for the add-on
+ * @param aExistingAddon
+ * The add-on this install will update if known
+ */
+ constructor(aInstallLocation, aUrl, aHash, aExistingAddon) {
+ this.wrapper = new AddonInstallWrapper(this);
+ this.installLocation = aInstallLocation;
+ this.sourceURI = aUrl;
+
+ if (aHash) {
+ let hashSplit = aHash.toLowerCase().split(":");
+ this.originalHash = {
+ algorithm: hashSplit[0],
+ data: hashSplit[1]
+ };
+ }
+ this.hash = this.originalHash;
+ this.existingAddon = aExistingAddon;
+ this.releaseNotesURI = null;
+
+ this.listeners = [];
+ this.icons = {};
+ this.error = 0;
+
+ this.progress = 0;
+ this.maxProgress = -1;
+
+ // Giving each instance of AddonInstall a reference to the logger.
+ this.logger = logger;
+
+ this.name = null;
+ this.type = null;
+ this.version = null;
+
+ this.file = null;
+ this.ownsTempFile = null;
+ this.certificate = null;
+ this.certName = null;
+
+ this.linkedInstalls = null;
+ this.addon = null;
+ this.state = null;
XPIProvider.installs.add(this);
- return this;
- },
-
- /**
- * Initialises this install to be an install from a local file.
- *
- * @param aCallback
- * The callback to pass the initialised AddonInstall to
- */
- initLocalInstall: function(aCallback) {
- aCallback = makeSafe(aCallback);
- this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
-
- if (!this.file.exists()) {
- logger.warn("XPI file " + this.file.path + " does not exist");
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = AddonManager.ERROR_NETWORK_FAILURE;
- XPIProvider.removeActiveInstall(this);
- aCallback(this);
- return;
- }
-
- this.state = AddonManager.STATE_DOWNLOADED;
- this.progress = this.file.fileSize;
- this.maxProgress = this.file.fileSize;
-
- if (this.hash) {
- let crypto = Cc["@mozilla.org/security/hash;1"].
- createInstance(Ci.nsICryptoHash);
- try {
- crypto.initWithString(this.hash.algorithm);
- }
- catch (e) {
- logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = AddonManager.ERROR_INCORRECT_HASH;
- XPIProvider.removeActiveInstall(this);
- aCallback(this);
- return;
- }
-
- let fis = Cc["@mozilla.org/network/file-input-stream;1"].
- createInstance(Ci.nsIFileInputStream);
- fis.init(this.file, -1, -1, false);
- crypto.updateFromStream(fis, this.file.fileSize);
- let calculatedHash = getHashStringForCrypto(crypto);
- if (calculatedHash != this.hash.data) {
- logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
- this.hash.data + ")");
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = AddonManager.ERROR_INCORRECT_HASH;
- XPIProvider.removeActiveInstall(this);
- aCallback(this);
- return;
- }
- }
-
- this.loadManifest(this.file).then(() => {
- XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => {
- this.existingAddon = aAddon;
- if (aAddon)
- applyBlocklistChanges(aAddon, this.addon);
- this.addon.updateDate = Date.now();
- this.addon.installDate = aAddon ? aAddon.installDate : this.addon.updateDate;
-
- if (!this.addon.isCompatible) {
- // TODO Should we send some event here?
- this.state = AddonManager.STATE_CHECKING;
- new UpdateChecker(this.addon, {
- onUpdateFinished: aAddon => {
- this.state = AddonManager.STATE_DOWNLOADED;
- AddonManagerPrivate.callInstallListeners("onNewInstall",
- this.listeners,
- this.wrapper);
-
- aCallback(this);
- }
- }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
- }
- else {
- AddonManagerPrivate.callInstallListeners("onNewInstall",
- this.listeners,
- this.wrapper);
-
- aCallback(this);
- }
- });
- }, ([error, message]) => {
- logger.warn("Invalid XPI", message);
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = error;
- XPIProvider.removeActiveInstall(this);
- AddonManagerPrivate.callInstallListeners("onNewInstall",
- this.listeners,
- this.wrapper);
-
- aCallback(this);
- });
- },
-
- /**
- * Initialises this install to be a download from a remote url.
- *
- * @param aCallback
- * The callback to pass the initialised AddonInstall to
- * @param aName
- * An optional name for the add-on
- * @param aType
- * An optional type for the add-on
- * @param aIcons
- * Optional icons for the add-on
- * @param aVersion
- * An optional version for the add-on
- */
- initAvailableDownload: function(aName, aType, aIcons, aVersion, aCallback) {
- this.state = AddonManager.STATE_AVAILABLE;
- this.name = aName;
- this.type = aType;
- this.version = aVersion;
- this.icons = aIcons;
- this.progress = 0;
- this.maxProgress = -1;
-
- AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
- this.wrapper);
-
- makeSafe(aCallback)(this);
- },
+ }
/**
* Starts installation of this add-on from whatever state it is currently at
* if possible.
*
+ * Note this method is overridden to handle additional state in
+ * the subclassses below.
+ *
* @throws if installation cannot proceed from the current state
*/
- install: function() {
+ install() {
switch (this.state) {
- case AddonManager.STATE_AVAILABLE:
- this.startDownload();
- break;
case AddonManager.STATE_DOWNLOADED:
this.startInstall();
break;
- case AddonManager.STATE_DOWNLOAD_FAILED:
- case AddonManager.STATE_INSTALL_FAILED:
- case AddonManager.STATE_CANCELLED:
- this.removeTemporaryFile();
- this.state = AddonManager.STATE_AVAILABLE;
- this.error = 0;
- this.progress = 0;
- this.maxProgress = -1;
- this.hash = this.originalHash;
- this.startDownload();
- break;
case AddonManager.STATE_POSTPONED:
logger.debug(`Postponing install of ${this.addon.id}`);
break;
case AddonManager.STATE_DOWNLOADING:
case AddonManager.STATE_CHECKING:
case AddonManager.STATE_INSTALLING:
// Installation is already running
return;
default:
throw new Error("Cannot start installing from this state");
}
- },
+ }
/**
* Cancels installation of this add-on.
*
+ * Note this method is overridden to handle additional state in
+ * the subclass DownloadAddonInstall.
+ *
* @throws if installation cannot be cancelled from the current state
*/
- cancel: function() {
+ cancel() {
switch (this.state) {
- case AddonManager.STATE_DOWNLOADING:
- if (this.channel) {
- logger.debug("Cancelling download of " + this.sourceURI.spec);
- this.channel.cancel(Cr.NS_BINDING_ABORTED);
- }
- break;
case AddonManager.STATE_AVAILABLE:
case AddonManager.STATE_DOWNLOADED:
logger.debug("Cancelling download of " + this.sourceURI.spec);
this.state = AddonManager.STATE_CANCELLED;
XPIProvider.removeActiveInstall(this);
AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
this.listeners, this.wrapper);
this.removeTemporaryFile();
@@ -5681,46 +5472,46 @@ AddonInstall.prototype = {
let stagingDir = this.installLocation.getStagingDir();
let stagedAddon = stagingDir.clone();
this.unstageInstall(stagedAddon);
default:
throw new Error("Cannot cancel install of " + this.sourceURI.spec +
" from this state (" + this.state + ")");
}
- },
+ }
/**
* Adds an InstallListener for this instance if the listener is not already
* registered.
*
* @param aListener
* The InstallListener to add
*/
- addListener: function(aListener) {
+ addListener(aListener) {
if (!this.listeners.some(function(i) { return i == aListener; }))
this.listeners.push(aListener);
- },
+ }
/**
* Removes an InstallListener for this instance if it is registered.
*
* @param aListener
* The InstallListener to remove
*/
- removeListener: function(aListener) {
+ removeListener(aListener) {
this.listeners = this.listeners.filter(function(i) {
return i != aListener;
});
- },
+ }
/**
* Removes the temporary file owned by this AddonInstall if there is one.
*/
- removeTemporaryFile: function() {
+ removeTemporaryFile() {
// Only proceed if this AddonInstall owns its XPI file
if (!this.ownsTempFile) {
this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
return;
}
try {
this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
@@ -5728,610 +5519,276 @@ AddonInstall.prototype = {
this.file.remove(true);
this.ownsTempFile = false;
}
catch (e) {
this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
this.sourceURI.spec,
e);
}
- },
+ }
/**
* Updates the sourceURI and releaseNotesURI values on the Addon being
* installed by this AddonInstall instance.
*/
- updateAddonURIs: function() {
+ updateAddonURIs() {
this.addon.sourceURI = this.sourceURI.spec;
if (this.releaseNotesURI)
this.addon.releaseNotesURI = this.releaseNotesURI.spec;
- },
+ }
/**
* Fills out linkedInstalls with AddonInstall instances for the other files
* in a multi-package XPI.
*
* @param aFiles
* An array of { entryName, file } for each remaining file from the
* multi-package XPI.
*/
- _createLinkedInstalls: Task.async(function*(aFiles) {
- if (aFiles.length == 0)
- return;
-
- // Create new AddonInstall instances for every remaining file
- if (!this.linkedInstalls)
- this.linkedInstalls = [];
-
- for (let { entryName, file } of aFiles) {
- logger.debug("Creating linked install from " + entryName);
- let install = yield new Promise(
- resolve => AddonInstall.createInstall(resolve, file)
- );
-
- // Make the new install own its temporary file
- install.ownsTempFile = true;
-
- this.linkedInstalls.push(install);
-
- // If one of the internal XPIs was multipackage then move its linked
- // installs to the outer install
- if (install.linkedInstalls) {
- this.linkedInstalls.push(...install.linkedInstalls);
- install.linkedInstalls = null;
- }
-
- install.sourceURI = this.sourceURI;
- install.releaseNotesURI = this.releaseNotesURI;
- if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
- install.updateAddonURIs();
- }
- }),
+ _createLinkedInstalls(aFiles) {
+ return Task.spawn((function*() {
+ if (aFiles.length == 0)
+ return;
+
+ // Create new AddonInstall instances for every remaining file
+ if (!this.linkedInstalls)
+ this.linkedInstalls = [];
+
+ for (let { entryName, file } of aFiles) {
+ logger.debug("Creating linked install from " + entryName);
+ let install = yield createLocalInstall(file);
+
+ // Make the new install own its temporary file
+ install.ownsTempFile = true;
+
+ this.linkedInstalls.push(install);
+
+ // If one of the internal XPIs was multipackage then move its linked
+ // installs to the outer install
+ if (install.linkedInstalls) {
+ this.linkedInstalls.push(...install.linkedInstalls);
+ install.linkedInstalls = null;
+ }
+
+ install.sourceURI = this.sourceURI;
+ install.releaseNotesURI = this.releaseNotesURI;
+ if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
+ install.updateAddonURIs();
+ }
+ }).bind(this));
+ }
/**
* Loads add-on manifests from a multi-package XPI file. Each of the
* XPI and JAR files contained in the XPI will be extracted. Any that
* do not contain valid add-ons will be ignored. The first valid add-on will
* be installed by this AddonInstall instance, the rest will have new
* AddonInstall instances created for them.
*
* @param aZipReader
* An open nsIZipReader for the multi-package XPI's files. This will
* be closed before this method returns.
*/
- _loadMultipackageManifests: Task.async(function*(aZipReader) {
- let files = [];
- let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
- while (entries.hasMore()) {
- let entryName = entries.getNext();
- let file = getTemporaryFile();
- try {
- aZipReader.extract(entryName, file);
- files.push({ entryName, file });
- }
- catch (e) {
- logger.warn("Failed to extract " + entryName + " from multi-package " +
- "XPI", e);
- file.remove(false);
- }
- }
-
- aZipReader.close();
-
- if (files.length == 0) {
+ _loadMultipackageManifests(aZipReader) {
+ return Task.spawn((function*() {
+ let files = [];
+ let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
+ while (entries.hasMore()) {
+ let entryName = entries.getNext();
+ let file = getTemporaryFile();
+ try {
+ aZipReader.extract(entryName, file);
+ files.push({ entryName, file });
+ }
+ catch (e) {
+ logger.warn("Failed to extract " + entryName + " from multi-package " +
+ "XPI", e);
+ file.remove(false);
+ }
+ }
+
+ aZipReader.close();
+
+ if (files.length == 0) {
+ return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+ "Multi-package XPI does not contain any packages to install"]);
+ }
+
+ let addon = null;
+
+ // Find the first file that is a valid install and use it for
+ // the add-on that this AddonInstall instance will install.
+ for (let { entryName, file } of files) {
+ this.removeTemporaryFile();
+ try {
+ yield this.loadManifest(file);
+ logger.debug("Base multi-package XPI install came from " + entryName);
+ this.file = file;
+ this.ownsTempFile = true;
+
+ yield this._createLinkedInstalls(files.filter(f => f.file != file));
+ return undefined;
+ }
+ catch (e) {
+ // _createLinkedInstalls will log errors when it tries to process this
+ // file
+ }
+ }
+
+ // No valid add-on was found, delete all the temporary files
+ for (let { file } of files) {
+ try {
+ file.remove(true);
+ } catch (e) {
+ this.logger.warn("Could not remove temp file " + file.path);
+ }
+ }
+
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
- "Multi-package XPI does not contain any packages to install"]);
- }
-
- let addon = null;
-
- // Find the first file that is a valid install and use it for
- // the add-on that this AddonInstall instance will install.
- for (let { entryName, file } of files) {
- this.removeTemporaryFile();
- try {
- yield this.loadManifest(file);
- logger.debug("Base multi-package XPI install came from " + entryName);
- this.file = file;
- this.ownsTempFile = true;
-
- yield this._createLinkedInstalls(files.filter(f => f.file != file));
- return undefined;
- }
- catch (e) {
- // _createLinkedInstalls will log errors when it tries to process this
- // file
- }
- }
-
- // No valid add-on was found, delete all the temporary files
- for (let { file } of files) {
- try {
- file.remove(true);
- } catch (e) {
- this.logger.warn("Could not remove temp file " + file.path);
- }
- }
-
- return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
- "Multi-package XPI does not contain any valid packages to install"]);
- }),
+ "Multi-package XPI does not contain any valid packages to install"]);
+ }).bind(this));
+ }
/**
* Called after the add-on is a local file and the signature and install
* manifest can be read.
*
* @param aCallback
* A function to call when the manifest has been loaded
* @throws if the add-on does not contain a valid install manifest or the
* XPI is incorrectly signed
*/
- loadManifest: Task.async(function*(file) {
- let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
- createInstance(Ci.nsIZipReader);
- try {
- zipreader.open(file);
- }
- catch (e) {
- zipreader.close();
- return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
- }
-
- try {
- // loadManifestFromZipReader performs the certificate verification for us
- this.addon = yield loadManifestFromZipReader(zipreader, this.installLocation);
- }
- catch (e) {
- zipreader.close();
- return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
- }
-
- // A multi-package XPI is a container, the add-ons it holds each
- // have their own id. Everything else had better have an id here.
- if (!this.addon.id && this.addon.type != "multipackage") {
- let err = new Error(`Cannot find id for addon ${file.path}`);
- return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
- }
-
- if (this.existingAddon) {
- // Check various conditions related to upgrades
- if (this.addon.id != this.existingAddon.id) {
- zipreader.close();
- return Promise.reject([AddonManager.ERROR_INCORRECT_ID,
- `Refusing to upgrade addon ${this.existingAddon.id} to different ID {this.addon.id}`]);
- }
-
- if (this.addon.type == "multipackage") {
- zipreader.close();
- return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
- `Refusing to upgrade addon ${this.existingAddon.id} to a multi-package xpi`]);
- }
-
- if (this.existingAddon.type == "webextension" && this.addon.type != "webextension") {
- zipreader.close();
- return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
- "WebExtensions may not be upated to other extension types"]);
- }
- }
-
- if (mustSign(this.addon.type)) {
- if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
- // This add-on isn't properly signed by a signature that chains to the
- // trusted root.
- let state = this.addon.signedState;
- this.addon = null;
+ loadManifest(file) {
+ return Task.spawn((function*() {
+ let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipreader.open(file);
+ }
+ catch (e) {
zipreader.close();
-
- if (state == AddonManager.SIGNEDSTATE_MISSING)
- return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
- "signature is required but missing"])
-
- return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
- "signature verification failed"])
- }
- }
- else if (this.addon.signedState == AddonManager.SIGNEDSTATE_UNKNOWN ||
- this.addon.signedState == AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
- // Check object signing certificate, if any
- let x509 = zipreader.getSigningCert(null);
- if (x509) {
- logger.debug("Verifying XPI signature");
- if (verifyZipSigning(zipreader, x509)) {
- this.certificate = x509;
- if (this.certificate.commonName.length > 0) {
- this.certName = this.certificate.commonName;
- } else {
- this.certName = this.certificate.organization;
- }
- } else {
- zipreader.close();
- return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
- "XPI is incorrectly signed"]);
- }
- }
- }
-
- if (this.addon.type == "multipackage")
- return this._loadMultipackageManifests(zipreader);
-
- zipreader.close();
-
- this.updateAddonURIs();
-
- this.addon._install = this;
- this.name = this.addon.selectedLocale.name;
- this.type = this.addon.type;
- this.version = this.addon.version;
-
- // Setting the iconURL to something inside the XPI locks the XPI and
- // makes it impossible to delete on Windows.
-
- // Try to load from the existing cache first
- let repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
-
- // It wasn't there so try to re-download it
- if (!repoAddon) {
- yield new Promise(resolve => AddonRepository.cacheAddons([this.addon.id], resolve));
- repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
- }
-
- this.addon._repositoryAddon = repoAddon;
- this.name = this.name || this.addon._repositoryAddon.name;
- this.addon.compatibilityOverrides = repoAddon ?
- repoAddon.compatibilityOverrides :
- null;
- this.addon.appDisabled = !isUsableAddon(this.addon);
- return undefined;
- }),
-
- observe: function(aSubject, aTopic, aData) {
- // Network is going offline
- this.cancel();
- },
-
- /**
- * Starts downloading the add-on's XPI file.
- */
- startDownload: function() {
- this.state = AddonManager.STATE_DOWNLOADING;
- if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
- this.listeners, this.wrapper)) {
- logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
- this.state = AddonManager.STATE_CANCELLED;
- XPIProvider.removeActiveInstall(this);
- AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
- this.listeners, this.wrapper)
- return;
- }
-
- // If a listener changed our state then do not proceed with the download
- if (this.state != AddonManager.STATE_DOWNLOADING)
- return;
-
- if (this.channel) {
- // A previous download attempt hasn't finished cleaning up yet, signal
- // that it should restart when complete
- logger.debug("Waiting for previous download to complete");
- this.restartDownload = true;
- return;
- }
-
- this.openChannel();
- },
-
- openChannel: function() {
- this.restartDownload = false;
-
- try {
- this.file = getTemporaryFile();
- this.ownsTempFile = true;
- this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
- createInstance(Ci.nsIFileOutputStream);
- this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
- FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
- }
- catch (e) {
- logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = AddonManager.ERROR_FILE_ACCESS;
- XPIProvider.removeActiveInstall(this);
- AddonManagerPrivate.callInstallListeners("onDownloadFailed",
- this.listeners, this.wrapper);
- return;
- }
-
- let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
- createInstance(Ci.nsIStreamListenerTee);
- listener.init(this, this.stream);
- try {
- let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true);
- this.badCertHandler = new CertUtils.BadCertHandler(!requireBuiltIn);
-
- this.channel = NetUtil.newChannel({
- uri: this.sourceURI,
- loadUsingSystemPrincipal: true
- });
- this.channel.notificationCallbacks = this;
- if (this.channel instanceof Ci.nsIHttpChannel) {
- this.channel.setRequestHeader("Moz-XPI-Update", "1", true);
- if (this.channel instanceof Ci.nsIHttpChannelInternal)
- this.channel.forceAllowThirdPartyCookie = true;
- }
- this.channel.asyncOpen2(listener);
-
- Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
- }
- catch (e) {
- logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = AddonManager.ERROR_NETWORK_FAILURE;
- XPIProvider.removeActiveInstall(this);
- AddonManagerPrivate.callInstallListeners("onDownloadFailed",
- this.listeners, this.wrapper);
- }
- },
-
- /**
- * Update the crypto hasher with the new data and call the progress listeners.
- *
- * @see nsIStreamListener
- */
- onDataAvailable: function(aRequest, aContext, aInputstream,
- aOffset, aCount) {
- this.crypto.updateFromStream(aInputstream, aCount);
- this.progress += aCount;
- if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
- this.listeners, this.wrapper)) {
- // TODO cancel the download and make it available again (bug 553024)
- }
- },
-
- /**
- * Check the redirect response for a hash of the target XPI and verify that
- * we don't end up on an insecure channel.
- *
- * @see nsIChannelEventSink
- */
- asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) {
- if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
- aOldChannel instanceof Ci.nsIHttpChannel) {
+ return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
+ }
+
try {
- let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
- let hashSplit = hashStr.toLowerCase().split(":");
- this.hash = {
- algorithm: hashSplit[0],
- data: hashSplit[1]
- };
+ // loadManifestFromZipReader performs the certificate verification for us
+ this.addon = yield loadManifestFromZipReader(zipreader, this.installLocation);
}
catch (e) {
- }
- }
-
- // Verify that we don't end up on an insecure channel if we haven't got a
- // hash to verify with (see bug 537761 for discussion)
- if (!this.hash)
- this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
- else
- aCallback.onRedirectVerifyCallback(Cr.NS_OK);
-
- this.channel = aNewChannel;
- },
-
- /**
- * This is the first chance to get at real headers on the channel.
- *
- * @see nsIStreamListener
- */
- onStartRequest: function(aRequest, aContext) {
- this.crypto = Cc["@mozilla.org/security/hash;1"].
- createInstance(Ci.nsICryptoHash);
- if (this.hash) {
- try {
- this.crypto.initWithString(this.hash.algorithm);
- }
- catch (e) {
- logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = AddonManager.ERROR_INCORRECT_HASH;
- XPIProvider.removeActiveInstall(this);
- AddonManagerPrivate.callInstallListeners("onDownloadFailed",
- this.listeners, this.wrapper);
- aRequest.cancel(Cr.NS_BINDING_ABORTED);
- return;
- }
- }
- else {
- // We always need something to consume data from the inputstream passed
- // to onDataAvailable so just create a dummy cryptohasher to do that.
- this.crypto.initWithString("sha1");
- }
-
- this.progress = 0;
- if (aRequest instanceof Ci.nsIChannel) {
- try {
- this.maxProgress = aRequest.contentLength;
- }
- catch (e) {
- }
- logger.debug("Download started for " + this.sourceURI.spec + " to file " +
- this.file.path);
- }
- },
-
- /**
- * The download is complete.
- *
- * @see nsIStreamListener
- */
- onStopRequest: function(aRequest, aContext, aStatus) {
- this.stream.close();
- this.channel = null;
- this.badCerthandler = null;
- Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
-
- // If the download was cancelled then update the state and send events
- if (aStatus == Cr.NS_BINDING_ABORTED) {
- if (this.state == AddonManager.STATE_DOWNLOADING) {
- logger.debug("Cancelled download of " + this.sourceURI.spec);
- this.state = AddonManager.STATE_CANCELLED;
- XPIProvider.removeActiveInstall(this);
- AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
- this.listeners, this.wrapper);
- // If a listener restarted the download then there is no need to
- // remove the temporary file
- if (this.state != AddonManager.STATE_CANCELLED)
- return;
- }
-
- this.removeTemporaryFile();
- if (this.restartDownload)
- this.openChannel();
- return;
- }
-
- logger.debug("Download of " + this.sourceURI.spec + " completed.");
-
- if (Components.isSuccessCode(aStatus)) {
- if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
- if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
- try {
- CertUtils.checkCert(aRequest,
- !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true));
- }
- catch (e) {
- this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
- return;
+ zipreader.close();
+ return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
+ }
+
+ // A multi-package XPI is a container, the add-ons it holds each
+ // have their own id. Everything else had better have an id here.
+ if (!this.addon.id && this.addon.type != "multipackage") {
+ let err = new Error(`Cannot find id for addon ${file.path}`);
+ return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
+ }
+
+ if (this.existingAddon) {
+ // Check various conditions related to upgrades
+ if (this.addon.id != this.existingAddon.id) {
+ zipreader.close();
+ return Promise.reject([AddonManager.ERROR_INCORRECT_ID,
+ `Refusing to upgrade addon ${this.existingAddon.id} to different ID {this.addon.id}`]);
+ }
+
+ if (this.addon.type == "multipackage") {
+ zipreader.close();
+ return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
+ `Refusing to upgrade addon ${this.existingAddon.id} to a multi-package xpi`]);
+ }
+
+ if (this.existingAddon.type == "webextension" && this.addon.type != "webextension") {
+ zipreader.close();
+ return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
+ "WebExtensions may not be upated to other extension types"]);
+ }
+ }
+
+ if (mustSign(this.addon.type)) {
+ if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ // This add-on isn't properly signed by a signature that chains to the
+ // trusted root.
+ let state = this.addon.signedState;
+ this.addon = null;
+ zipreader.close();
+
+ if (state == AddonManager.SIGNEDSTATE_MISSING)
+ return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
+ "signature is required but missing"])
+
+ return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+ "signature verification failed"])
+ }
+ }
+ else if (this.addon.signedState == AddonManager.SIGNEDSTATE_UNKNOWN ||
+ this.addon.signedState == AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
+ // Check object signing certificate, if any
+ let x509 = zipreader.getSigningCert(null);
+ if (x509) {
+ logger.debug("Verifying XPI signature");
+ if (verifyZipSigning(zipreader, x509)) {
+ this.certificate = x509;
+ if (this.certificate.commonName.length > 0) {
+ this.certName = this.certificate.commonName;
+ } else {
+ this.certName = this.certificate.organization;
+ }
+ } else {
+ zipreader.close();
+ return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+ "XPI is incorrectly signed"]);
}
}
-
- // convert the binary hash data to a hex string.
- let calculatedHash = getHashStringForCrypto(this.crypto);
- this.crypto = null;
- if (this.hash && calculatedHash != this.hash.data) {
- this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
- "Downloaded file hash (" + calculatedHash +
- ") did not match provided hash (" + this.hash.data + ")");
- return;
- }
-
- this.loadManifest(this.file).then(() => {
- if (this.addon.isCompatible) {
- this.downloadCompleted();
- }
- else {
- // TODO Should we send some event here (bug 557716)?
- this.state = AddonManager.STATE_CHECKING;
- new UpdateChecker(this.addon, {
- onUpdateFinished: aAddon => this.downloadCompleted(),
- }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
- }
- }, ([error, message]) => {
- this.removeTemporaryFile();
- this.downloadFailed(error, message);
- });
- }
- else if (aRequest instanceof Ci.nsIHttpChannel) {
- this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
- aRequest.responseStatus + " " +
- aRequest.responseStatusText);
- }
- else {
- this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
- }
- }
- else {
- this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
- }
- },
-
- /**
- * Notify listeners that the download failed.
- *
- * @param aReason
- * Something to log about the failure
- * @param error
- * The error code to pass to the listeners
- */
- downloadFailed: function(aReason, aError) {
- logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
- this.state = AddonManager.STATE_DOWNLOAD_FAILED;
- this.error = aReason;
- XPIProvider.removeActiveInstall(this);
- AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
- this.wrapper);
-
- // If the listener hasn't restarted the download then remove any temporary
- // file
- if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
- logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
- this.removeTemporaryFile();
- }
- else
- logger.debug("downloadFailed: listener changed AddonInstall state for " +
- this.sourceURI.spec + " to " + this.state);
- },
-
- /**
- * Notify listeners that the download completed.
- */
- downloadCompleted: function() {
- XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => {
- if (aAddon)
- this.existingAddon = aAddon;
-
- this.state = AddonManager.STATE_DOWNLOADED;
- this.addon.updateDate = Date.now();
-
- if (this.existingAddon) {
- this.addon.existingAddonID = this.existingAddon.id;
- this.addon.installDate = this.existingAddon.installDate;
- applyBlocklistChanges(this.existingAddon, this.addon);
- }
- else {
- this.addon.installDate = this.addon.updateDate;
- }
-
- if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
- this.listeners,
- this.wrapper)) {
- // If a listener changed our state then do not proceed with the install
- if (this.state != AddonManager.STATE_DOWNLOADED)
- return;
-
- // If an upgrade listener is registered for this add-on, pass control
- // over the upgrade to the add-on.
- if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
- logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`);
- let resumeFn = () => {
- logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
- this.state = AddonManager.STATE_DOWNLOADED;
- this.install();
- }
- this.postpone(resumeFn);
- } else {
- // no upgrade listener present, so proceed with normal install
- this.install();
- if (this.linkedInstalls) {
- for (let install of this.linkedInstalls) {
- if (install.state == AddonManager.STATE_DOWNLOADED)
- install.install();
- }
- }
- }
- }
- });
- },
+ }
+
+ if (this.addon.type == "multipackage")
+ return this._loadMultipackageManifests(zipreader);
+
+ zipreader.close();
+
+ this.updateAddonURIs();
+
+ this.addon._install = this;
+ this.name = this.addon.selectedLocale.name;
+ this.type = this.addon.type;
+ this.version = this.addon.version;
+
+ // Setting the iconURL to something inside the XPI locks the XPI and
+ // makes it impossible to delete on Windows.
+
+ // Try to load from the existing cache first
+ let repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
+
+ // It wasn't there so try to re-download it
+ if (!repoAddon) {
+ yield new Promise(resolve => AddonRepository.cacheAddons([this.addon.id], resolve));
+ repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
+ }
+
+ this.addon._repositoryAddon = repoAddon;
+ this.name = this.name || this.addon._repositoryAddon.name;
+ this.addon.compatibilityOverrides = repoAddon ?
+ repoAddon.compatibilityOverrides :
+ null;
+ this.addon.appDisabled = !isUsableAddon(this.addon);
+ return undefined;
+ }).bind(this));
+ }
// TODO This relies on the assumption that we are always installing into the
// highest priority install location so the resulting add-on will be visible
// overriding any existing copy in another install location (bug 557710).
/**
* Installs the add-on into the install location.
*/
- startInstall: function() {
+ startInstall() {
this.state = AddonManager.STATE_INSTALLING;
if (!AddonManagerPrivate.callInstallListeners("onInstallStarted",
this.listeners, this.wrapper)) {
this.state = AddonManager.STATE_DOWNLOADED;
XPIProvider.removeActiveInstall(this);
AddonManagerPrivate.callInstallListeners("onInstallCancelled",
this.listeners, this.wrapper)
return;
@@ -6377,20 +5834,16 @@ AddonInstall.prototype = {
AddonManagerPrivate.callInstallListeners("onInstallEnded",
this.listeners, this.wrapper,
this.addon.wrapper);
}
else {
// The install is completed so it should be removed from the active list
XPIProvider.removeActiveInstall(this);
- // TODO We can probably reduce the number of DB operations going on here
- // We probably also want to support rolling back failed upgrades etc.
- // See bug 553015.
-
// Deactivate and remove the old add-on as necessary
let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
if (this.existingAddon) {
if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
else
reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
@@ -6490,195 +5943,789 @@ AddonInstall.prototype = {
this.addon.wrapper);
AddonManagerPrivate.callInstallListeners("onInstallFailed",
this.listeners,
this.wrapper);
}).then(() => {
this.removeTemporaryFile();
return this.installLocation.releaseStagingDir();
});
- },
+ }
/**
* Stages an upgrade for next application restart.
*/
- stageInstall: function*(restartRequired, stagedAddon, isUpgrade) {
- let stagedJSON = stagedAddon.clone();
- stagedJSON.leafName = this.addon.id + ".json";
-
- let installedUnpacked = 0;
-
- // First stage the file regardless of whether restarting is necessary
- if (this.addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
- logger.debug("Addon " + this.addon.id + " will be installed as " +
- "an unpacked directory");
- stagedAddon.leafName = this.addon.id;
- yield OS.File.makeDir(stagedAddon.path);
- yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
- installedUnpacked = 1;
- }
- else {
- logger.debug(`Addon ${this.addon.id} will be installed as a packed xpi`);
- stagedAddon.leafName = this.addon.id + ".xpi";
-
- yield OS.File.copy(this.file.path, stagedAddon.path);
- }
-
- if (restartRequired) {
- // Point the add-on to its extracted files as the xpi may get deleted
- this.addon._sourceBundle = stagedAddon;
-
- // Cache the AddonInternal as it may have updated compatibility info
- writeStringToFile(stagedJSON, JSON.stringify(this.addon));
-
- logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
- if (isUpgrade) {
- delete this.existingAddon.pendingUpgrade;
- this.existingAddon.pendingUpgrade = this.addon;
- }
- }
-
- return installedUnpacked;
- },
+ stageInstall(restartRequired, stagedAddon, isUpgrade) {
+ return Task.spawn((function*() {
+ let stagedJSON = stagedAddon.clone();
+ stagedJSON.leafName = this.addon.id + ".json";
+
+ let installedUnpacked = 0;
+
+ // First stage the file regardless of whether restarting is necessary
+ if (this.addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
+ logger.debug("Addon " + this.addon.id + " will be installed as " +
+ "an unpacked directory");
+ stagedAddon.leafName = this.addon.id;
+ yield OS.File.makeDir(stagedAddon.path);
+ yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
+ installedUnpacked = 1;
+ }
+ else {
+ logger.debug(`Addon ${this.addon.id} will be installed as a packed xpi`);
+ stagedAddon.leafName = this.addon.id + ".xpi";
+
+ yield OS.File.copy(this.file.path, stagedAddon.path);
+ }
+
+ if (restartRequired) {
+ // Point the add-on to its extracted files as the xpi may get deleted
+ this.addon._sourceBundle = stagedAddon;
+
+ // Cache the AddonInternal as it may have updated compatibility info
+ writeStringToFile(stagedJSON, JSON.stringify(this.addon));
+
+ logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
+ if (isUpgrade) {
+ delete this.existingAddon.pendingUpgrade;
+ this.existingAddon.pendingUpgrade = this.addon;
+ }
+ }
+
+ return installedUnpacked;
+ }).bind(this));
+ }
/**
* Removes any previously staged upgrade.
*/
- unstageInstall: function*(stagedAddon) {
- let stagedJSON = stagedAddon.clone();
- let removedAddon = stagedAddon.clone();
-
- stagedJSON.append(this.addon.id + ".json");
-
- if (stagedJSON.exists()) {
- stagedJSON.remove(true);
- }
-
- removedAddon.append(this.addon.id);
- yield removeAsync(removedAddon);
- removedAddon.leafName = this.addon.id + ".xpi";
- yield removeAsync(removedAddon);
- },
-
- getInterface: function(iid) {
+ unstageInstall(stagedAddon) {
+ return Task.spawn((function*() {
+ let stagedJSON = stagedAddon.clone();
+ let removedAddon = stagedAddon.clone();
+
+ stagedJSON.append(this.addon.id + ".json");
+
+ if (stagedJSON.exists()) {
+ stagedJSON.remove(true);
+ }
+
+ removedAddon.append(this.addon.id);
+ yield removeAsync(removedAddon);
+ removedAddon.leafName = this.addon.id + ".xpi";
+ yield removeAsync(removedAddon);
+ }).bind(this));
+ }
+
+ /**
+ * Postone a pending update, until restart or until the add-on resumes.
+ *
+ * @param {Function} resumeFunction - a function for the add-on to run
+ * when resuming.
+ */
+ postpone(resumeFunction) {
+ return Task.spawn((function*() {
+ this.state = AddonManager.STATE_POSTPONED;
+
+ let stagingDir = this.installLocation.getStagingDir();
+ let stagedAddon = stagingDir.clone();
+
+ yield this.installLocation.requestStagingDir();
+ yield this.unstageInstall(stagedAddon);
+
+ stagedAddon.append(this.addon.id);
+ stagedAddon.leafName = this.addon.id + ".xpi";
+
+ yield this.stageInstall(true, stagedAddon, true);
+
+ AddonManagerPrivate.callInstallListeners("onInstallPostponed",
+ this.listeners, this.wrapper)
+
+ // upgrade has been staged for restart, provide a way for it to call the
+ // resume function.
+ if (resumeFunction) {
+ let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
+ if (callback) {
+ callback({
+ version: this.version,
+ install: () => {
+ switch (this.state) {
+ case AddonManager.STATE_POSTPONED:
+ resumeFunction();
+ break;
+ default:
+ logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
+ break;
+ }
+ },
+ });
+ }
+ }
+ this.installLocation.releaseStagingDir();
+ }).bind(this));
+ }
+}
+
+class LocalAddonInstall extends AddonInstall {
+ /**
+ * Initialises this install to be an install from a local file.
+ *
+ * @returns Promise
+ * A Promise that resolves when the object is ready to use.
+ */
+ init() {
+ return Task.spawn((function*() {
+ this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
+
+ if (!this.file.exists()) {
+ logger.warn("XPI file " + this.file.path + " does not exist");
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_NETWORK_FAILURE;
+ XPIProvider.removeActiveInstall(this);
+ return;
+ }
+
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.progress = this.file.fileSize;
+ this.maxProgress = this.file.fileSize;
+
+ if (this.hash) {
+ let crypto = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ try {
+ crypto.initWithString(this.hash.algorithm);
+ }
+ catch (e) {
+ logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ XPIProvider.removeActiveInstall(this);
+ return;
+ }
+
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(this.file, -1, -1, false);
+ crypto.updateFromStream(fis, this.file.fileSize);
+ let calculatedHash = getHashStringForCrypto(crypto);
+ if (calculatedHash != this.hash.data) {
+ logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
+ this.hash.data + ")");
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ XPIProvider.removeActiveInstall(this);
+ return;
+ }
+ }
+
+ try {
+ yield this.loadManifest(this.file);
+ } catch ([error, message]) {
+ logger.warn("Invalid XPI", message);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = error;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onNewInstall",
+ this.listeners,
+ this.wrapper);
+ return;
+ }
+
+ let addon = yield new Promise(resolve => {
+ XPIDatabase.getVisibleAddonForID(this.addon.id, resolve);
+ });
+
+ this.existingAddon = addon;
+ if (addon)
+ applyBlocklistChanges(addon, this.addon);
+ this.addon.updateDate = Date.now();
+ this.addon.installDate = addon ? addon.installDate : this.addon.updateDate;
+
+ if (!this.addon.isCompatible) {
+ this.state = AddonManager.STATE_CHECKING;
+
+ yield new Promise(resolve => {
+ new UpdateChecker(this.addon, {
+ onUpdateFinished: aAddon => {
+ this.state = AddonManager.STATE_DOWNLOADED;
+ AddonManagerPrivate.callInstallListeners("onNewInstall",
+ this.listeners,
+ this.wrapper);
+ resolve();
+ }
+ }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ });
+ }
+ else {
+ AddonManagerPrivate.callInstallListeners("onNewInstall",
+ this.listeners,
+ this.wrapper);
+
+ }
+ }).bind(this));
+ }
+
+ install() {
+ if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ // For a local install, this state means that verification of the
+ // file failed (e.g., the hash or signature or manifest contents
+ // were invalid). It doesn't make sense to retry anything in this
+ // case but we have callers who don't know if their AddonInstall
+ // object is a local file or a download so accomodate them here.
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ return;
+ }
+ super.install();
+ }
+}
+
+class DownloadAddonInstall extends AddonInstall {
+ /**
+ * Instantiates a DownloadAddonInstall
+ *
+ * @param installLocation
+ * The InstallLocation the add-on will be installed into
+ * @param url
+ * The nsIURL to get the add-on from
+ * @param name
+ * An optional name for the add-on
+ * @param hash
+ * An optional hash for the add-on
+ * @param existingAddon
+ * The add-on this install will update if known
+ * @param browser
+ * The browser performing the install, used to display
+ * authentication prompts.
+ * @param type
+ * An optional type for the add-on
+ * @param icons
+ * Optional icons for the add-on
+ * @param version
+ * An optional version for the add-on
+ */
+ constructor(installLocation, url, hash, existingAddon, browser,
+ name, type, icons, version) {
+ super(installLocation, url, hash, existingAddon);
+
+ this.browser = browser;
+
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.name = name;
+ this.type = type;
+ this.version = version;
+ this.icons = icons;
+
+ this.stream = null;
+ this.crypto = null;
+ this.badCertHandler = null;
+ this.restartDownload = false;
+
+ AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
+ this.wrapper);
+ }
+
+ install() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.startDownload();
+ break;
+ case AddonManager.STATE_DOWNLOAD_FAILED:
+ case AddonManager.STATE_INSTALL_FAILED:
+ case AddonManager.STATE_CANCELLED:
+ this.removeTemporaryFile();
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.error = 0;
+ this.progress = 0;
+ this.maxProgress = -1;
+ this.hash = this.originalHash;
+ this.startDownload();
+ break;
+ default:
+ super.install();
+ }
+ }
+
+ cancel() {
+ if (this.state == AddonManager.STATE_DOWNLOADING) {
+ if (this.channel) {
+ logger.debug("Cancelling download of " + this.sourceURI.spec);
+ this.channel.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ } else {
+ super.cancel();
+ }
+ }
+
+ observe(aSubject, aTopic, aData) {
+ // Network is going offline
+ this.cancel();
+ }
+
+ /**
+ * Starts downloading the add-on's XPI file.
+ */
+ startDownload() {
+ this.state = AddonManager.STATE_DOWNLOADING;
+ if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
+ this.listeners, this.wrapper)) {
+ logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper)
+ return;
+ }
+
+ // If a listener changed our state then do not proceed with the download
+ if (this.state != AddonManager.STATE_DOWNLOADING)
+ return;
+
+ if (this.channel) {
+ // A previous download attempt hasn't finished cleaning up yet, signal
+ // that it should restart when complete
+ logger.debug("Waiting for previous download to complete");
+ this.restartDownload = true;
+ return;
+ }
+
+ this.openChannel();
+ }
+
+ openChannel() {
+ this.restartDownload = false;
+
+ try {
+ this.file = getTemporaryFile();
+ this.ownsTempFile = true;
+ this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
+ }
+ catch (e) {
+ logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_FILE_ACCESS;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ return;
+ }
+
+ let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
+ createInstance(Ci.nsIStreamListenerTee);
+ listener.init(this, this.stream);
+ try {
+ let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true);
+ this.badCertHandler = new CertUtils.BadCertHandler(!requireBuiltIn);
+
+ this.channel = NetUtil.newChannel({
+ uri: this.sourceURI,
+ loadUsingSystemPrincipal: true
+ });
+ this.channel.notificationCallbacks = this;
+ if (this.channel instanceof Ci.nsIHttpChannel) {
+ this.channel.setRequestHeader("Moz-XPI-Update", "1", true);
+ if (this.channel instanceof Ci.nsIHttpChannelInternal)
+ this.channel.forceAllowThirdPartyCookie = true;
+ }
+ this.channel.asyncOpen2(listener);
+
+ Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
+ }
+ catch (e) {
+ logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_NETWORK_FAILURE;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ }
+ }
+
+ /**
+ * Update the crypto hasher with the new data and call the progress listeners.
+ *
+ * @see nsIStreamListener
+ */
+ onDataAvailable(aRequest, aContext, aInputstream, aOffset, aCount) {
+ this.crypto.updateFromStream(aInputstream, aCount);
+ this.progress += aCount;
+ if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
+ this.listeners, this.wrapper)) {
+ // TODO cancel the download and make it available again (bug 553024)
+ }
+ }
+
+ /**
+ * Check the redirect response for a hash of the target XPI and verify that
+ * we don't end up on an insecure channel.
+ *
+ * @see nsIChannelEventSink
+ */
+ asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) {
+ if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
+ aOldChannel instanceof Ci.nsIHttpChannel) {
+ try {
+ let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
+ let hashSplit = hashStr.toLowerCase().split(":");
+ this.hash = {
+ algorithm: hashSplit[0],
+ data: hashSplit[1]
+ };
+ }
+ catch (e) {
+ }
+ }
+
+ // Verify that we don't end up on an insecure channel if we haven't got a
+ // hash to verify with (see bug 537761 for discussion)
+ if (!this.hash)
+ this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
+ else
+ aCallback.onRedirectVerifyCallback(Cr.NS_OK);
+
+ this.channel = aNewChannel;
+ }
+
+ /**
+ * This is the first chance to get at real headers on the channel.
+ *
+ * @see nsIStreamListener
+ */
+ onStartRequest(aRequest, aContext) {
+ this.crypto = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ if (this.hash) {
+ try {
+ this.crypto.initWithString(this.hash.algorithm);
+ }
+ catch (e) {
+ logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = AddonManager.ERROR_INCORRECT_HASH;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+ this.listeners, this.wrapper);
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+ }
+ else {
+ // We always need something to consume data from the inputstream passed
+ // to onDataAvailable so just create a dummy cryptohasher to do that.
+ this.crypto.initWithString("sha1");
+ }
+
+ this.progress = 0;
+ if (aRequest instanceof Ci.nsIChannel) {
+ try {
+ this.maxProgress = aRequest.contentLength;
+ }
+ catch (e) {
+ }
+ logger.debug("Download started for " + this.sourceURI.spec + " to file " +
+ this.file.path);
+ }
+ }
+
+ /**
+ * The download is complete.
+ *
+ * @see nsIStreamListener
+ */
+ onStopRequest(aRequest, aContext, aStatus) {
+ this.stream.close();
+ this.channel = null;
+ this.badCerthandler = null;
+ Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
+
+ // If the download was cancelled then update the state and send events
+ if (aStatus == Cr.NS_BINDING_ABORTED) {
+ if (this.state == AddonManager.STATE_DOWNLOADING) {
+ logger.debug("Cancelled download of " + this.sourceURI.spec);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+ this.listeners, this.wrapper);
+ // If a listener restarted the download then there is no need to
+ // remove the temporary file
+ if (this.state != AddonManager.STATE_CANCELLED)
+ return;
+ }
+
+ this.removeTemporaryFile();
+ if (this.restartDownload)
+ this.openChannel();
+ return;
+ }
+
+ logger.debug("Download of " + this.sourceURI.spec + " completed.");
+
+ if (Components.isSuccessCode(aStatus)) {
+ if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
+ if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
+ try {
+ CertUtils.checkCert(aRequest,
+ !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true));
+ }
+ catch (e) {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
+ return;
+ }
+ }
+
+ // convert the binary hash data to a hex string.
+ let calculatedHash = getHashStringForCrypto(this.crypto);
+ this.crypto = null;
+ if (this.hash && calculatedHash != this.hash.data) {
+ this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
+ "Downloaded file hash (" + calculatedHash +
+ ") did not match provided hash (" + this.hash.data + ")");
+ return;
+ }
+
+ this.loadManifest(this.file).then(() => {
+ if (this.addon.isCompatible) {
+ this.downloadCompleted();
+ }
+ else {
+ // TODO Should we send some event here (bug 557716)?
+ this.state = AddonManager.STATE_CHECKING;
+ new UpdateChecker(this.addon, {
+ onUpdateFinished: aAddon => this.downloadCompleted(),
+ }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ }
+ }, ([error, message]) => {
+ this.removeTemporaryFile();
+ this.downloadFailed(error, message);
+ });
+ }
+ else if (aRequest instanceof Ci.nsIHttpChannel) {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
+ aRequest.responseStatus + " " +
+ aRequest.responseStatusText);
+ }
+ else {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+ }
+ }
+ else {
+ this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+ }
+ }
+
+ /**
+ * Notify listeners that the download failed.
+ *
+ * @param aReason
+ * Something to log about the failure
+ * @param error
+ * The error code to pass to the listeners
+ */
+ downloadFailed(aReason, aError) {
+ logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
+ this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+ this.error = aReason;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
+ this.wrapper);
+
+ // If the listener hasn't restarted the download then remove any temporary
+ // file
+ if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+ logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
+ this.removeTemporaryFile();
+ }
+ else
+ logger.debug("downloadFailed: listener changed AddonInstall state for " +
+ this.sourceURI.spec + " to " + this.state);
+ }
+
+ /**
+ * Notify listeners that the download completed.
+ */
+ downloadCompleted() {
+ XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => {
+ if (aAddon)
+ this.existingAddon = aAddon;
+
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.addon.updateDate = Date.now();
+
+ if (this.existingAddon) {
+ this.addon.existingAddonID = this.existingAddon.id;
+ this.addon.installDate = this.existingAddon.installDate;
+ applyBlocklistChanges(this.existingAddon, this.addon);
+ }
+ else {
+ this.addon.installDate = this.addon.updateDate;
+ }
+
+ if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
+ this.listeners,
+ this.wrapper)) {
+ // If a listener changed our state then do not proceed with the install
+ if (this.state != AddonManager.STATE_DOWNLOADED)
+ return;
+
+ // If an upgrade listener is registered for this add-on, pass control
+ // over the upgrade to the add-on.
+ if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
+ logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`);
+ let resumeFn = () => {
+ logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.install();
+ }
+ this.postpone(resumeFn);
+ } else {
+ // no upgrade listener present, so proceed with normal install
+ this.install();
+ if (this.linkedInstalls) {
+ for (let install of this.linkedInstalls) {
+ if (install.state == AddonManager.STATE_DOWNLOADED)
+ install.install();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ getInterface(iid) {
if (iid.equals(Ci.nsIAuthPrompt2)) {
- let win = this.window;
- if (!win && this.browser)
- win = this.browser.ownerDocument.defaultView;
+ let win = null;
+ if (this.browser) {
+ win = this.browser.contentWindow || this.browser.ownerDocument.defaultView;
+ }
let factory = Cc["@mozilla.org/prompter;1"].
getService(Ci.nsIPromptFactory);
let prompt = factory.getPrompt(win, Ci.nsIAuthPrompt2);
if (this.browser && prompt instanceof Ci.nsILoginManagerPrompter)
prompt.browser = this.browser;
return prompt;
}
else if (iid.equals(Ci.nsIChannelEventSink)) {
return this;
}
return this.badCertHandler.getInterface(iid);
- },
+ }
/**
* Postone a pending update, until restart or until the add-on resumes.
*
* @param {Function} resumeFn - a function for the add-on to run
* when resuming.
*/
- postpone: Task.async(function*(resumeFn) {
- this.state = AddonManager.STATE_POSTPONED;
-
- let stagingDir = this.installLocation.getStagingDir();
- let stagedAddon = stagingDir.clone();
-
- yield this.installLocation.requestStagingDir();
- yield this.unstageInstall(stagedAddon);
-
- stagedAddon.append(this.addon.id);
- stagedAddon.leafName = this.addon.id + ".xpi";
-
- yield this.stageInstall(true, stagedAddon, true);
-
- AddonManagerPrivate.callInstallListeners("onInstallPostponed",
- this.listeners, this.wrapper)
-
- // upgrade has been staged for restart, provide a way for it to call the
- // resume function.
- let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
- if (callback) {
- callback({
- version: this.version,
- install: () => {
- switch (this.state) {
+ postpone(resumeFn) {
+ return Task.spawn((function*() {
+ this.state = AddonManager.STATE_POSTPONED;
+
+ let stagingDir = this.installLocation.getStagingDir();
+ let stagedAddon = stagingDir.clone();
+
+ yield this.installLocation.requestStagingDir();
+ yield this.unstageInstall(stagedAddon);
+
+ stagedAddon.append(this.addon.id);
+ stagedAddon.leafName = this.addon.id + ".xpi";
+
+ yield this.stageInstall(true, stagedAddon, true);
+
+ AddonManagerPrivate.callInstallListeners("onInstallPostponed",
+ this.listeners, this.wrapper)
+
+ // upgrade has been staged for restart, provide a way for it to call the
+ // resume function.
+ let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
+ if (callback) {
+ callback({
+ version: this.version,
+ install: () => {
+ switch (this.state) {
case AddonManager.STATE_POSTPONED:
if (resumeFn) {
resumeFn();
}
break;
default:
logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
break;
- }
- },
- });
- }
- // Release the staging directory lock, but since the staging dir is populated
- // it will not be removed until resumed or installed by restart.
- // See also cleanStagingDir()
- this.installLocation.releaseStagingDir();
- }
-)}
+ }
+ },
+ });
+ }
+ // Release the staging directory lock, but since the staging dir is populated
+ // it will not be removed until resumed or installed by restart.
+ // See also cleanStagingDir()
+ this.installLocation.releaseStagingDir();
+ }).bind(this));
+ }
+}
/**
- * Creates a new AddonInstall for an already staged install. Used when
- * installing the staged install failed for some reason.
- * @param aInstallLocation
- * The install location holding the staged install.
- * @param aDir
- * The directory holding the staged install
- * @param aManifest
- * The cached manifest for the install
+ * This class exists just for the specific case of staged add-ons that
+ * fail to install at startup. When that happens, the add-on remains
+ * staged but we want to keep track of it like other installs so that we
+ * can clean it up if the same add-on is installed again (see the comment
+ * about "pending installs for the same add-on" in AddonInstall.startInstall)
*/
-AddonInstall.createStagedInstall = function(aInstallLocation, aDir, aManifest) {
- let url = Services.io.newFileURI(aDir);
-
- let install = new AddonInstall(aInstallLocation, aDir);
-
- install.initStagedInstall(aManifest);
-};
+class StagedAddonInstall extends AddonInstall {
+ constructor(installLocation, dir, manifest) {
+ super(installLocation, dir);
+
+ this.name = manifest.name;
+ this.type = manifest.type;
+ this.version = manifest.version;
+ this.icons = manifest.icons;
+ this.releaseNotesURI = manifest.releaseNotesURI ?
+ NetUtil.newURI(manifest.releaseNotesURI) :
+ null;
+ this.sourceURI = manifest.sourceURI ?
+ NetUtil.newURI(manifest.sourceURI) :
+ null;
+ this.file = null;
+ this.addon = manifest;
+
+ this.state = AddonManager.STATE_INSTALLED;
+ }
+}
/**
* Creates a new AddonInstall to install an add-on from a local file.
*
- * @param aCallback
- * The callback to pass the new AddonInstall to
- * @param aFile
+ * @param file
* The file to install
- * @param aLocation
+ * @param location
* The location to install to
+ * @returns Promise
+ * A Promise that resolves with the new install object.
*/
-AddonInstall.createInstall = function(aCallback, aFile, aLocation = undefined) {
- if (!aLocation) {
- aLocation = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
- }
- let url = Services.io.newFileURI(aFile);
+function createLocalInstall(file, location) {
+ if (!location) {
+ location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+ }
+ let url = Services.io.newFileURI(file);
try {
- let install = new AddonInstall(aLocation, url);
- install.initLocalInstall(aCallback);
+ let install = new LocalAddonInstall(location, url);
+ return install.init().then(() => install);
}
catch (e) {
logger.error("Error creating install", e);
XPIProvider.removeActiveInstall(this);
- makeSafe(aCallback)(null);
- }
-};
+ return Promise.resolve(null);
+ }
+}
/**
* Creates a new AddonInstall to download and install a URL.
*
* @param aCallback
* The callback to pass the new AddonInstall to
* @param aUri
* The URI to download
@@ -6688,59 +6735,68 @@ AddonInstall.createInstall = function(aC
* A name for the add-on
* @param aIcons
* An icon URLs for the add-on
* @param aVersion
* A version for the add-on
* @param aBrowser
* The browser performing the install
*/
-AddonInstall.createDownload = function(aCallback, aUri, aHash, aName, aIcons,
- aVersion, aBrowser) {
+function createDownloadInstall(aCallback, aUri, aHash, aName, aIcons,
+ aVersion, aBrowser) {
let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
let url = NetUtil.newURI(aUri);
- let install = new AddonInstall(location, url, aHash, null, null, aBrowser);
- if (url instanceof Ci.nsIFileURL)
- install.initLocalInstall(aCallback);
- else
- install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback);
-};
+ if (url instanceof Ci.nsIFileURL) {
+ let install = new LocalAddonInstall(location, url, aHash);
+ install.init().then(() => { aCallback(install); });
+ } else {
+ let install = new DownloadAddonInstall(location, url, aHash, null,
+ aBrowser, aName, null, aIcons,
+ aVersion);
+ aCallback(install);
+ }
+}
/**
* Creates a new AddonInstall for an update.
*
* @param aCallback
* The callback to pass the new AddonInstall to
* @param aAddon
* The add-on being updated
* @param aUpdate
* The metadata about the new version from the update manifest
*/
-AddonInstall.createUpdate = function(aCallback, aAddon, aUpdate) {
+function createUpdate(aCallback, aAddon, aUpdate) {
let url = NetUtil.newURI(aUpdate.updateURL);
- let releaseNotesURI = null;
- try {
- if (aUpdate.updateInfoURL)
- releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
- }
- catch (e) {
- // If the releaseNotesURI cannot be parsed then just ignore it.
- }
-
- let install = new AddonInstall(aAddon._installLocation, url,
- aUpdate.updateHash, releaseNotesURI, aAddon);
- if (url instanceof Ci.nsIFileURL) {
- install.initLocalInstall(aCallback);
- }
- else {
- install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type,
- aAddon.icons, aUpdate.version, aCallback);
- }
-};
+
+ Task.spawn(function*() {
+ let install;
+ if (url instanceof Ci.nsIFileURL) {
+ install = new LocalAddonInstall(aAddon._installLocation, url,
+ aUpdate.updateHash, aAddon);
+ yield install.init();
+ } else {
+ install = new DownloadAddonInstall(aAddon._installLocation, url,
+ aUpdate.updateHash, aAddon, null,
+ aAddon.selectedLocale.name, aAddon.type,
+ aAddon.icons, aUpdate.version);
+ }
+ try {
+ if (aUpdate.updateInfoURL)
+ install.releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
+ }
+ catch (e) {
+ // If the releaseNotesURI cannot be parsed then just ignore it.
+ }
+
+ aCallback(install);
+ });
+}
// This map is shared between AddonInstallWrapper and AddonWrapper
const wrapperMap = new WeakMap();
let installFor = wrapper => wrapperMap.get(wrapper);
let addonFor = installFor;
/**
* Creates a wrapper for an AddonInstall that only exposes the public API
@@ -6980,17 +7036,17 @@ UpdateChecker.prototype = {
logger.debug("Found an existing AddonInstall for " + this.addon.id);
sendUpdateAvailableMessages(this, currentInstall);
}
else
sendUpdateAvailableMessages(this, null);
return;
}
- AddonInstall.createUpdate(aInstall => {
+ createUpdate(aInstall => {
sendUpdateAvailableMessages(this, aInstall);
}, this.addon, update);
}
else {
sendUpdateAvailableMessages(this, null);
}
},
@@ -8908,19 +8964,17 @@ Object.assign(SystemAddonInstallLocation
let state = { schema: 1, directory: newDir.leafName, addons: {} };
this._saveAddonSet(state);
this._nextDir = newDir;
let location = this;
let installs = [];
for (let addon of aAddons) {
- let install = yield new Promise(resolve => {
- AddonInstall.createInstall(resolve, addon._sourceBundle, location);
- });
+ let install = yield createLocalInstall(addon._sourceBundle, location);
installs.push(install);
}
let installAddon = Task.async(function*(install) {
// Make the new install own its temporary file.
install.ownsTempFile = true;
install.install();
});