From d5049daf81c5dcf9654f3fa6968fae3bcc2b3e51 Mon Sep 17 00:00:00 2001 From: Gregory Szorc Date: Fri, 28 Mar 2014 20:17:04 +0100 Subject: [PATCH] Bug 975000 - Disable updating and compatibility checking for Experiments; r=Unfocused Experiment add-ons are installed and updated via the Experiments Manager service. With this change, the Add-ons Manager lets experiment add-ons play by their own rules without interference. --HG-- extra : rebase_source : 4a7c30f8ce36a64f91f9fee49da9061eda568b99 --- browser/experiments/Experiments.jsm | 23 ++++---- toolkit/mozapps/extensions/AddonManager.jsm | 20 ++++++- .../extensions/LightweightThemeManager.jsm | 7 +-- .../extensions/internal/XPIProvider.jsm | 52 +++++++++++++++++-- .../test/addons/test_experiment1/install.rdf | 17 ++++++ .../test/browser/browser_experiments.js | 20 +++++-- .../test/xpcshell/test_experiment.js | 48 +++++++++++++++++ .../extensions/test/xpcshell/test_shutdown.js | 3 +- .../test/xpcshell/xpcshell-shared.ini | 1 + 9 files changed, 166 insertions(+), 25 deletions(-) create mode 100644 toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_experiment.js diff --git a/browser/experiments/Experiments.jsm b/browser/experiments/Experiments.jsm index fd3b6a5e9006..0d4ff4dfad9b 100644 --- a/browser/experiments/Experiments.jsm +++ b/browser/experiments/Experiments.jsm @@ -1376,8 +1376,9 @@ Experiments.ExperimentEntry.prototype = { deferred.reject(new Error(message)); }; + let entry = this; let listener = { - onDownloadEnded: install => { + onDownloadEnded: function (install) { gLogger.trace("ExperimentEntry::_installAddon() - onDownloadEnded for " + this.id); if (install.existingAddon) { @@ -1388,9 +1389,9 @@ Experiments.ExperimentEntry.prototype = { gLogger.error("ExperimentEntry::_installAddon() - onDownloadEnded, wrong addon type"); install.cancel(); } - }, + }.bind(entry), - onInstallStarted: install => { + onInstallStarted: function (install) { gLogger.trace("ExperimentEntry::_installAddon() - onInstallStarted for " + this.id); if (install.existingAddon) { @@ -1401,9 +1402,9 @@ Experiments.ExperimentEntry.prototype = { gLogger.error("ExperimentEntry::_installAddon() - onInstallStarted, wrong addon type"); return false; } - }, + }.bind(entry), - onInstallEnded: install => { + onInstallEnded: function (install) { gLogger.trace("ExperimentEntry::_installAddon() - install ended for " + this.id); this._lastChangedDate = this._policy.now(); this._startDate = this._policy.now(); @@ -1419,13 +1420,13 @@ Experiments.ExperimentEntry.prototype = { this._homepageURL = addon.homepageURL || ""; deferred.resolve(); - }, - }; + }.bind(entry), - ["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"] - .forEach(what => { - listener[what] = install => failureHandler(install, what) - }); + onDownloadCancelled: failureHandler.bind(entry), + onDownloadFailed: failureHandler.bind(entry), + onInstallCancelled: failureHandler.bind(entry), + onInstallFailed: failureHandler.bind(entry), + }; install.addListener(listener); install.install(); diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm index 46da6db81543..eb0fc7039057 100644 --- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -2317,7 +2317,25 @@ this.AddonManagerPrivate = { return { done: () => this.recordSimpleMeasure(aName, Date.now() - startTime) }; - } + }, + + /** + * Helper to call update listeners when no update is available. + * + * This can be used as an implementation for Addon.findUpdates() when + * no update mechanism is available. + */ + callNoUpdateListeners: function (addon, listener, reason, appVersion, platformVersion) { + if ("onNoCompatibilityUpdateAvailable" in listener) { + safeCall(listener.onNoCompatibilityUpdateAvailable, addon); + } + if ("onNoUpdateAvailable" in listener) { + safeCall(listener.onNoUpdateAvailable, addon); + } + if ("onUpdateFinished" in listener) { + safeCall(listener.onUpdateFinished, addon); + } + }, }; /** diff --git a/toolkit/mozapps/extensions/LightweightThemeManager.jsm b/toolkit/mozapps/extensions/LightweightThemeManager.jsm index 8c9f30492d3f..615f792b6e4c 100644 --- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm +++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm @@ -508,12 +508,7 @@ function AddonWrapper(aTheme) { }; this.findUpdates = function AddonWrapper_findUpdates(listener, reason, appVersion, platformVersion) { - if ("onNoCompatibilityUpdateAvailable" in listener) - listener.onNoCompatibilityUpdateAvailable(this); - if ("onNoUpdateAvailable" in listener) - listener.onNoUpdateAvailable(this); - if ("onUpdateFinished" in listener) - listener.onUpdateFinished(this); + AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion); }; } diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index e0607d964a1a..991c76f01909 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -915,6 +915,17 @@ function loadManifestFromRDF(aUri, aStream) { addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; + // Experiments are managed and updated through an external "experiments + // manager." So disable some built-in mechanisms. + if (addon.type == "experiment") { + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; + addon.updateURL = null; + addon.updateKey = null; + + addon.targetApplications = []; + addon.targetPlatforms = []; + } + // Load the storage service before NSS (nsIRandomGenerator), // to avoid a SQLite initialization error (bug 717904). let storage = Services.storage; @@ -6014,6 +6025,17 @@ AddonInternal.prototype = { }, isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) { + // Experiments are installed through an external mechanism that + // limits target audience to compatible clients. We trust it knows what + // it's doing and skip compatibility checks. + // + // This decision does forfeit defense in depth. If the experiments system + // is ever wrong about targeting an add-on to a specific application + // or platform, the client will likely see errors. + if (this.type == "experiment") { + return true; + } + let app = this.matchingTargetApplication; if (!app) return false; @@ -6398,6 +6420,11 @@ function AddonWrapper(aAddon) { return aAddon.applyBackgroundUpdates; }); this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) { + if (this.type == "experiment") { + logger.warn("Setting applyBackgroundUpdates on an experiment is not supported."); + return; + } + if (val != AddonManager.AUTOUPDATE_DEFAULT && val != AddonManager.AUTOUPDATE_DISABLE && val != AddonManager.AUTOUPDATE_ENABLE) { @@ -6498,22 +6525,33 @@ function AddonWrapper(aAddon) { if (!(aAddon.inDatabase)) return permissions; + // Experiments can only be uninstalled. An uninstall reflects the user + // intent of "disable this experiment." This is partially managed by the + // experiments manager. + if (aAddon.type == "experiment") { + return AddonManager.PERM_CAN_UNINSTALL; + } + if (!aAddon.appDisabled) { - if (this.userDisabled) + if (this.userDisabled) { permissions |= AddonManager.PERM_CAN_ENABLE; - else if (aAddon.type != "theme") + } + else if (aAddon.type != "theme") { permissions |= AddonManager.PERM_CAN_DISABLE; + } } // Add-ons that are in locked install locations, or are pending uninstall // cannot be upgraded or uninstalled if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) { // Add-ons that are installed by a file link cannot be upgraded - if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) + if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) { permissions |= AddonManager.PERM_CAN_UPGRADE; + } permissions |= AddonManager.PERM_CAN_UNINSTALL; } + return permissions; }); @@ -6595,6 +6633,14 @@ function AddonWrapper(aAddon) { }; this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { + // Short-circuit updates for experiments because updates are handled + // through the Experiments Manager. + if (this.type == "experiment") { + AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason, + aAppVersion, aPlatformVersion); + return; + } + new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion); }; diff --git a/toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf new file mode 100644 index 000000000000..9508b1562e78 --- /dev/null +++ b/toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf @@ -0,0 +1,17 @@ + + + + + + experiment1@tests.mozilla.org + 1.0 + 128 + true + + + Test Experiment 1 + Test Description + + + diff --git a/toolkit/mozapps/extensions/test/browser/browser_experiments.js b/toolkit/mozapps/extensions/test/browser/browser_experiments.js index 6c2b771897aa..05ca5dc415ef 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_experiments.js +++ b/toolkit/mozapps/extensions/test/browser/browser_experiments.js @@ -50,9 +50,7 @@ add_test(function testActiveExperiment() { install_addon("addons/browser_experiment1.xpi", (addon) => { gInstalledAddons.push(addon); - // This may change if we remove compatibility checking from experiments. - // Putting this check here so a test fails if preconditions change. - Assert.equal(addon.isActive, false, "Add-on is not active."); + Assert.ok(addon.isActive, "Add-on is active."); Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible."); @@ -133,3 +131,19 @@ add_test(function testOpenPreferences() { EventUtils.synthesizeMouseAtCenter(btn, {}, gManagerWindow); }); }); + +add_test(function testButtonPresence() { + gCategoryUtilities.openType("experiment", (win) => { + let item = get_addon_element(gManagerWindow, "test-experiment1@experiments.mozilla.org"); + Assert.ok(item, "Got add-on element."); + + let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn"); + // Corresponds to the uninstall permission. + is_element_visible(el, "Remove button is visible."); + // Corresponds to lack of disable permission. + el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn"); + is_element_hidden(el, "Disable button not visible."); + + run_next_test(); + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js b/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js new file mode 100644 index 000000000000..88d9d31b66e3 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + startupManager(); + + run_next_test(); +} + +add_test(function test_experiment() { + AddonManager.getInstallForFile(do_get_addon("test_experiment1"), (install) => { + completeAllInstalls([install], () => { + AddonManager.getAddonByID("experiment1@tests.mozilla.org", (addon) => { + Assert.ok(addon, "Addon is found."); + + Assert.ok(addon.isActive, "Add-on is active."); + Assert.equal(addon.updateURL, null, "No updateURL for experiments."); + Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE, + "Background updates are disabled."); + Assert.equal(addon.permissions, AddonManager.PERM_CAN_UNINSTALL, + "Permissions are minimal."); + + // Setting applyBackgroundUpdates should not work. + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE; + Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE, + "Setting applyBackgroundUpdates shouldn't do anything."); + + let noCompatibleCalled = false; + let noUpdateCalled = false; + let finishedCalled = false; + + let listener = { + onNoCompatibilityUpdateAvailable: () => { noCompatibleCalled = true; }, + onNoUpdateAvailable: () => { noUpdateCalled = true; }, + onUpdateFinished: () => { finishedCalled = true; }, + }; + + addon.findUpdates(listener, "testing", null, null); + Assert.ok(noCompatibleCalled, "Listener called."); + Assert.ok(noUpdateCalled, "Listener called."); + Assert.ok(finishedCalled, "Listener called."); + + run_next_test(); + }); + }); + }); +}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js index c9c67d8138b5..6bf3f27d890a 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js @@ -17,7 +17,8 @@ const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride", "addStartupChange", "removeStartupChange", "recordTimestamp", "recordSimpleMeasure", "recordException", "getSimpleMeasures", "simpleTimer", - "setTelemetryDetails", "getTelemetryDetails"]; + "setTelemetryDetails", "getTelemetryDetails", + "callNoUpdateListeners"]; function test_functions() { for (let prop in AddonManager) { diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini index d0ced7767e9e..644dbdb128b3 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini @@ -163,6 +163,7 @@ fail-if = os == "android" # Bug 676992: test consistently hangs on Android skip-if = os == "android" [test_error.js] +[test_experiment.js] [test_filepointer.js] # Bug 676992: test consistently hangs on Android skip-if = os == "android"