--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -674,16 +674,17 @@ var AddonManagerInternal = {
typeListeners: [],
pendingProviders: new Set(),
providers: new Set(),
providerShutdowns: new Map(),
types: {},
startupChanges: {},
// Store telemetry details per addon provider
telemetryDetails: {},
+ upgradeListeners: new Map(),
recordTimestamp: function(name, value) {
this.TelemetryTimestamps.add(name, value);
},
validateBlocklist: function() {
let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
@@ -1457,17 +1458,17 @@ var AddonManagerInternal = {
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);
+ logger.debug(`Starting upgrade install of ${aAddon.id}`);
aInstall.install();
}
},
onUpdateFinished: aAddon => { logger.debug("onUpdateFinished for ${id}", aAddon); resolve(); }
}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
}));
}
@@ -2243,16 +2244,66 @@ var AddonManagerInternal = {
let pos = 0;
while (pos < this.installListeners.length) {
if (this.installListeners[pos] == aListener)
this.installListeners.splice(pos, 1);
else
pos++;
}
},
+ /*
+ * Adds new or overrides existing UpgradeListener.
+ *
+ * @param aInstanceID
+ * The instance ID of an addon to listen to register a listener for.
+ * @param aCallback
+ * The callback to invoke when updates are available for this addon.
+ * @throws if there is no addon matching the instanceID
+ */
+ addUpgradeListener: function(aInstanceID, aCallback) {
+ if (!aInstanceID || typeof aInstanceID != "symbol")
+ throw Components.Exception("aInstanceID must be a symbol",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ if (!aCallback || typeof aCallback != "function")
+ throw Components.Exception("aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ this.getAddonByInstanceID(aInstanceID).then(wrapper => {
+ if (!wrapper) {
+ throw Error("No addon matching instanceID:", aInstanceID.toString());
+ }
+ let addonId = wrapper.addonId();
+ logger.debug(`Registering upgrade listener for ${addonId}`)
+ this.upgradeListeners.set(addonId, aCallback);
+ });
+ },
+
+ /**
+ * Removes an UpgradeListener if the listener is registered.
+ *
+ * @param aInstanceID
+ * The instance ID of the addon to remove
+ */
+ removeUpgradeListener: function(aInstanceID) {
+ if (!aInstanceID || typeof aInstanceID != "symbol")
+ throw Components.Exception("aInstanceID must be a symbol",
+ Cr.NS_ERROR_INVALID_ARG);
+
+ this.getAddonByInstanceID(aInstanceID).then(addon => {
+ if (!addon) {
+ throw Error("No addon for instanceID:", aInstanceID.toString());
+ }
+ if (this.upgradeListeners.has(addon.id)) {
+ this.upgradeListeners.delete(addon.id);
+ } else {
+ throw Error("No upgrade listener registered for addon ID:", addon.id);
+ }
+ });
+ },
/**
* Installs a temporary add-on from a local file or directory.
* @param aFile
* An nsIFile for the file or directory of the add-on to be
* temporarily installed.
* @return a Promise that rejects if the add-on is not a valid restartless
* add-on or if the same ID is already temporarily installed.
@@ -3055,16 +3106,24 @@ this.AddonManagerPrivate = {
if ("onUpdateFinished" in listener) {
safeCall(listener.onUpdateFinished.bind(listener), addon);
}
},
get webExtensionsMinPlatformVersion() {
return gWebExtensionsMinPlatformVersion;
},
+
+ hasUpgradeListener: function(aId) {
+ return AddonManagerInternal.upgradeListeners.has(aId);
+ },
+
+ getUpgradeListener: function(aId) {
+ return AddonManagerInternal.upgradeListeners.get(aId);
+ },
};
/**
* This is the public API that UI and developers should be calling. All methods
* just forward to AddonManagerInternal.
*/
this.AddonManager = {
// Constants for the AddonInstall.state property
@@ -3075,24 +3134,26 @@ this.AddonManager = {
// The install is being downloaded.
["STATE_DOWNLOADING", 1],
// The install is checking for compatibility information.
["STATE_CHECKING", 2],
// The install is downloaded and ready to install.
["STATE_DOWNLOADED", 3],
// The download failed.
["STATE_DOWNLOAD_FAILED", 4],
+ // The install has been postponed.
+ ["STATE_POSTPONED", 5],
// The add-on is being installed.
- ["STATE_INSTALLING", 5],
+ ["STATE_INSTALLING", 6],
// The add-on has been installed.
- ["STATE_INSTALLED", 6],
+ ["STATE_INSTALLED", 7],
// The install failed.
- ["STATE_INSTALL_FAILED", 7],
+ ["STATE_INSTALL_FAILED", 8],
// The install has been cancelled.
- ["STATE_CANCELLED", 8],
+ ["STATE_CANCELLED", 9],
]),
// Constants representing different types of errors while downloading an
// add-on.
// These will show up as AddonManager.ERROR_* (eg, ERROR_NETWORK_FAILURE)
_errors: new Map([
// The download failed due to network problems.
["ERROR_NETWORK_FAILURE", -1],
@@ -3415,16 +3476,28 @@ this.AddonManager = {
addInstallListener: function(aListener) {
AddonManagerInternal.addInstallListener(aListener);
},
removeInstallListener: function(aListener) {
AddonManagerInternal.removeInstallListener(aListener);
},
+ getUpgradeListener: function(aId) {
+ return AddonManagerInternal.upgradeListeners.get(aId);
+ },
+
+ addUpgradeListener: function(aInstanceID, aCallback) {
+ AddonManagerInternal.addUpgradeListener(aInstanceID, aCallback);
+ },
+
+ removeUpgradeListener: function(aInstanceID) {
+ AddonManagerInternal.removeUpgradeListener(aInstanceID);
+ },
+
addAddonListener: function(aListener) {
AddonManagerInternal.addAddonListener(aListener);
},
removeAddonListener: function(aListener) {
AddonManagerInternal.removeAddonListener(aListener);
},
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -5363,16 +5363,19 @@ AddonInstall.prototype = {
this.state = AddonManager.STATE_AVAILABLE;
this.error = 0;
this.progress = 0;
this.maxProgress = -1;
this.hash = this.originalHash;
XPIProvider.installs.push(this);
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");
}
@@ -5415,16 +5418,28 @@ AddonInstall.prototype = {
this.existingAddon.pendingUpgrade = null;
}
AddonManagerPrivate.callAddonListeners("onOperationCancelled", this.addon.wrapper);
AddonManagerPrivate.callInstallListeners("onInstallCancelled",
this.listeners, this.wrapper);
break;
+ case AddonManager.STATE_POSTPONED:
+ logger.debug(`Cancelling postponed install of ${this.addon.id}`);
+ this.state = AddonManager.STATE_CANCELLED;
+ XPIProvider.removeActiveInstall(this);
+ AddonManagerPrivate.callInstallListeners("onInstallCancelled",
+ this.listeners, this.wrapper);
+ this.removeTemporaryFile();
+
+ 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
@@ -6005,22 +6020,68 @@ AddonInstall.prototype = {
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;
- this.install();
-
- if (this.linkedInstalls) {
- for (let install of this.linkedInstalls) {
- if (install.state == AddonManager.STATE_DOWNLOADED)
- install.install();
+ // 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(`${this.addon.id} has an upgrade listener, postponing until restart`);
+ this.state = AddonManager.STATE_POSTPONED;
+
+ let stagingDir = this.installLocation.getStagingDir();
+ let stagedAddon = stagingDir.clone();
+
+ Task.spawn((function*() {
+ 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, notify the add-on and give
+ // it a way to resume.
+ let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
+ callback({
+ install: () => {
+ switch (this.state) {
+ case AddonManager.STATE_INSTALLED:
+ // this addon has already been installed, nothing to do
+ logger.warn(`${this.addon.id} tried to resume postponed upgrade, but it's already installed`);
+ break;
+ case AddonManager.STATE_POSTPONED:
+ logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.install();
+ break;
+ default:
+ logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
+ break;
+ }
+ },
+ });
+ }).bind(this));
+ } 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();
+ }
}
}
}
});
},
// 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
@@ -6059,72 +6120,29 @@ AddonInstall.prototype = {
this.addon.wrapper,
requiresRestart);
let stagingDir = this.installLocation.getStagingDir();
let stagedAddon = stagingDir.clone();
Task.spawn((function*() {
let installedUnpacked = 0;
+
yield this.installLocation.requestStagingDir();
- // Remove any staged items for this add-on
+ // remove any previously staged files
+ yield this.unstageInstall(stagedAddon);
+
stagedAddon.append(this.addon.id);
- yield removeAsync(stagedAddon);
stagedAddon.leafName = this.addon.id + ".xpi";
- yield removeAsync(stagedAddon);
-
- // 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);
- }
+
+ installedUnpacked = yield this.stageInstall(requiresRestart, stagedAddon, isUpgrade);
if (requiresRestart) {
- // 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
- let stagedJSON = stagedAddon.clone();
- stagedJSON.leafName = this.addon.id + ".json";
- if (stagedJSON.exists())
- stagedJSON.remove(true);
- let stream = Cc["@mozilla.org/network/file-output-stream;1"].
- createInstance(Ci.nsIFileOutputStream);
- let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
- createInstance(Ci.nsIConverterOutputStream);
-
- try {
- stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
- FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
- 0);
- converter.init(stream, "UTF-8", 0, 0x0000);
- converter.writeString(JSON.stringify(this.addon));
- }
- finally {
- converter.close();
- stream.close();
- }
-
- logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
this.state = AddonManager.STATE_INSTALLED;
- if (isUpgrade) {
- delete this.existingAddon.pendingUpgrade;
- this.existingAddon.pendingUpgrade = this.addon;
- }
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);
@@ -6217,33 +6235,109 @@ AddonInstall.prototype = {
// listeners because important cleanup hasn't been done yet
XPIProvider.unloadBootstrapScope(this.addon.id);
}
}
XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked);
recordAddonTelemetry(this.addon);
}
}).bind(this)).then(null, (e) => {
- logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e);
+ logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec + " to " + stagedAddon.path, e);
if (stagedAddon.exists())
recursiveRemove(stagedAddon);
this.state = AddonManager.STATE_INSTALL_FAILED;
this.error = AddonManager.ERROR_FILE_ACCESS;
XPIProvider.removeActiveInstall(this);
AddonManagerPrivate.callAddonListeners("onOperationCancelled",
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
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+
+ try {
+ stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
+ 0);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ converter.writeString(JSON.stringify(this.addon));
+ }
+ finally {
+ converter.close();
+ stream.close();
+ }
+
+ 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;
+ },
+
+ /**
+ * 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) {
if (iid.equals(Ci.nsIAuthPrompt2)) {
let win = this.window;
if (!win && this.browser)
win = this.browser.ownerDocument.defaultView;
let factory = Cc["@mozilla.org/prompter;1"].
getService(Ci.nsIPromptFactory);
@@ -7398,16 +7492,20 @@ AddonWrapper.prototype = {
*/
function PrivateWrapper(aAddon) {
AddonWrapper.call(this, aAddon);
}
PrivateWrapper.prototype = Object.create(AddonWrapper.prototype);
Object.assign(PrivateWrapper.prototype, {
+ addonId() {
+ return this.id;
+ },
+
/**
* Defines a global context to be used in the console
* of the add-on debugging window.
*
* @param global
* The object to set as global context. Must be a window object.
*/
setDebugGlobal(global) {
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/bootstrap.js
@@ -0,0 +1,10 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_complete@tests.mozilla.org";
+
+function install(data, reason) {}
+
+function startup(data, reason) {}
+
+function shutdown(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test_delay_update_complete@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Delay Update Complete</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/bootstrap.js
@@ -0,0 +1,10 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_defer@tests.mozilla.org";
+
+function install(data, reason) {}
+
+function startup(data, reason) {}
+
+function shutdown(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test_delay_update_defer@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Delay Update Defer</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/bootstrap.js
@@ -0,0 +1,8 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+function install(data, reason) {}
+
+function startup(data, reason) {}
+
+function shutdown(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>test_delay_update_ignore@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Delay Update Ignore</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+ <em:updateURL>http://localhost:4444/data/test_delay_updates_ignore.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_complete/bootstrap.js
@@ -0,0 +1,24 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_complete@tests.mozilla.org";
+const INSTALL_COMPLETE_PREF = "bootstraptest.install_complete_done";
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+ // apply update immediately
+ if (data.hasOwnProperty("instanceID") && data.instanceID) {
+ AddonManager.addUpgradeListener(data.instanceID, (upgrade) => {
+ upgrade.install();
+ });
+ } else {
+ throw Error("no instanceID passed to bootstrap startup");
+ }
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_defer/bootstrap.js
@@ -0,0 +1,34 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_complete@tests.mozilla.org";
+const INSTALL_COMPLETE_PREF = "bootstraptest.install_complete_done";
+
+// global reference to hold upgrade object
+let gUpgrade;
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+ // do not apply update immediately, hold on to for later
+ if (data.hasOwnProperty("instanceID") && data.instanceID) {
+ AddonManager.addUpgradeListener(data.instanceID, (upgrade) => {
+ gUpgrade = upgrade;
+ });
+ } else {
+ throw Error("no instanceID passed to bootstrap startup");
+ }
+
+ // add a listener so the test can pass control back
+ AddonManager.addAddonListener({
+ onFakeEvent: () => {
+ gUpgrade.install();
+ }
+ })
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_ignore/bootstrap.js
@@ -0,0 +1,26 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_ignore@tests.mozilla.org";
+const TEST_IGNORE_PREF = "delaytest.ignore";
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+ Services.prefs.setBoolPref(TEST_IGNORE_PREF, false);
+
+ // explicitly ignore update, will be queued for next restart
+ if (data.hasOwnProperty("instanceID") && data.instanceID) {
+ AddonManager.addUpgradeListener(data.instanceID, (upgrade) => {
+ Services.prefs.setBoolPref(TEST_IGNORE_PREF, true);
+ });
+ } else {
+ throw Error("no instanceID passed to bootstrap startup");
+ }
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:extension:test_delay_update_complete@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <!-- app id compatible update available -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_delay_update_complete_v2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:extension:test_delay_update_defer@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <!-- app id compatible update available -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_delay_update_defer_v2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:extension:test_delay_update_ignore@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <!-- app id compatible update available -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_delay_update_ignore_v2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1685,17 +1685,18 @@ function completeAllInstalls(aInstalls,
do_execute_soon(aCallback);
}
let listener = {
onDownloadFailed: installCompleted,
onDownloadCancelled: installCompleted,
onInstallFailed: installCompleted,
onInstallCancelled: installCompleted,
- onInstallEnded: installCompleted
+ onInstallEnded: installCompleted,
+ onInstallPostponed: installCompleted,
};
aInstalls.forEach(function(aInstall) {
aInstall.addListener(listener);
aInstall.install();
});
}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update.js
@@ -0,0 +1,260 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that delaying an update works
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+Components.utils.import("resource://testing-common/httpd.js");
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const tempdir = gTmpD.clone();
+
+const IGNORE_ID = "test_delay_update_ignore@tests.mozilla.org";
+const COMPLETE_ID = "test_delay_update_complete@tests.mozilla.org";
+const DEFER_ID = "test_delay_update_defer@tests.mozilla.org";
+
+const TEST_IGNORE_PREF = "delaytest.ignore";
+
+// Note that we would normally use BootstrapMonitor but it currently requires
+// the objects in `data` to be serializable, and we need a real reference to the
+// `instanceID` symbol to test.
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+// Create and configure the HTTP server.
+let testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_delay_updates_complete.rdf", testserver);
+mapFile("/data/test_delay_updates_ignore.rdf", testserver);
+mapFile("/data/test_delay_updates_defer.rdf", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+function* createIgnoreAddon() {
+ writeInstallRDFToDir({
+ id: IGNORE_ID,
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ updateURL: `http://localhost:${gPort}/data/test_delay_updates_ignore.rdf`,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Delay Update Ignore",
+ }, profileDir, IGNORE_ID, "bootstrap.js");
+
+ let unpacked_addon = profileDir.clone();
+ unpacked_addon.append(IGNORE_ID);
+ do_get_file("data/test_delay_update_ignore/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+}
+
+function* createCompleteAddon() {
+ writeInstallRDFToDir({
+ id: COMPLETE_ID,
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ updateURL: `http://localhost:${gPort}/data/test_delay_updates_complete.rdf`,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Delay Update Complete",
+ }, profileDir, COMPLETE_ID, "bootstrap.js");
+
+ let unpacked_addon = profileDir.clone();
+ unpacked_addon.append(COMPLETE_ID);
+ do_get_file("data/test_delay_update_complete/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+}
+
+function* createDeferAddon() {
+ writeInstallRDFToDir({
+ id: DEFER_ID,
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ updateURL: `http://localhost:${gPort}/data/test_delay_updates_defer.rdf`,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Delay Update Defer",
+ }, profileDir, DEFER_ID, "bootstrap.js");
+
+ let unpacked_addon = profileDir.clone();
+ unpacked_addon.append(DEFER_ID);
+ do_get_file("data/test_delay_update_defer/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+}
+
+// add-on registers upgrade listener, and ignores update.
+add_task(function*() {
+
+ yield createIgnoreAddon();
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Delay Update Ignore");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_POSTPONED);
+
+ // addon upgrade has been delayed
+ let addon_postponed = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Test Delay Update Ignore");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+ do_check_true(Services.prefs.getBoolPref(TEST_IGNORE_PREF));
+
+ // restarting allows upgrade to proceed
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "Test Delay Update Ignore");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
+
+// add-on registers upgrade listener, and allows update.
+add_task(function*() {
+
+ yield createCompleteAddon();
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Delay Update Complete");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ yield promiseCompleteAllInstalls([install]);
+
+ // upgrade is initially postponed
+ let addon_postponed = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Test Delay Update Complete");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // addon upgrade has been allowed
+ let [addon_allowed] = yield promiseAddonEvent("onInstalled");
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "Test Delay Update Complete");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // restarting changes nothing
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "Test Delay Update Complete");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
+
+// add-on registers upgrade listener, initially defers update then allows upgrade
+add_task(function*() {
+
+ yield createDeferAddon();
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Delay Update Defer");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ yield promiseCompleteAllInstalls([install]);
+
+ // upgrade is initially postponed
+ let addon_postponed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Test Delay Update Defer");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // add-on will not allow upgrade until fake event fires
+ AddonManagerPrivate.callAddonListeners("onFakeEvent");
+
+ // addon upgrade has been allowed
+ let [addon_allowed] = yield promiseAddonEvent("onInstalled");
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "Test Delay Update Defer");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // restarting changes nothing
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "Test Delay Update Defer");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -6,26 +6,28 @@
const IGNORE = ["getPreferredIconURL", "escapeAddonURI",
"shouldAutoUpdate", "getStartupChanges",
"addTypeListener", "removeTypeListener",
"addAddonListener", "removeAddonListener",
"addInstallListener", "removeInstallListener",
"addManagerListener", "removeManagerListener",
"mapURIToAddonID", "shutdown", "init",
- "stateToString", "errorToString"];
+ "stateToString", "errorToString", "getUpgradeListener",
+ "addUpgradeListener", "removeUpgradeListener"];
const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
"AddonScreenshot", "AddonType", "startup", "shutdown",
"registerProvider", "unregisterProvider",
"addStartupChange", "removeStartupChange",
"recordTimestamp", "recordSimpleMeasure",
"recordException", "getSimpleMeasures", "simpleTimer",
"setTelemetryDetails", "getTelemetryDetails",
- "callNoUpdateListeners", "backgroundUpdateTimerHandler"];
+ "callNoUpdateListeners", "backgroundUpdateTimerHandler",
+ "hasUpgradeListener", "getUpgradeListener"];
function test_functions() {
for (let prop in AddonManager) {
if (IGNORE.indexOf(prop) != -1)
continue;
if (typeof AddonManager[prop] != "function")
continue;
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -31,11 +31,12 @@ skip-if = appname != "firefox"
[test_system_update.js]
[test_system_reset.js]
[test_XPIcancel.js]
[test_XPIStates.js]
[test_temporary.js]
[test_proxies.js]
[test_proxy.js]
[test_pass_symbol.js]
+[test_delay_update.js]
[include:xpcshell-shared.ini]