Bug 1330823 Part 2 permission prompts for interactive extension updates
MozReview-Commit-ID: HI41MbwkqW6
--- a/browser/base/content/test/general/browser_extension_update.js
+++ b/browser/base/content/test/general/browser_extension_update.js
@@ -1,13 +1,28 @@
const {AddonManagerPrivate} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
const URL_BASE = "https://example.com/browser/browser/base/content/test/general";
const ID = "update@tests.mozilla.org";
+function promiseInstallAddon(url) {
+ return AddonManager.getInstallForURL(url, null, "application/x-xpinstall")
+ .then(install => {
+ ok(install, "Created install");
+ return new Promise(resolve => {
+ install.addListener({
+ onInstallEnded(_install, addon) {
+ resolve(addon);
+ },
+ });
+ install.install();
+ });
+ });
+}
+
function promiseViewLoaded(tab, viewid) {
let win = tab.linkedBrowser.contentWindow;
if (win.gViewController && !win.gViewController.isLoading &&
win.gViewController.currentViewId == viewid) {
return Promise.resolve();
}
return new Promise(resolve => {
@@ -39,80 +54,60 @@ function promisePopupNotificationShown(n
});
}
function getBadgeStatus() {
let menuButton = document.getElementById("PanelUI-menu-button");
return menuButton.getAttribute("badge-status");
}
-function promiseUpdateDownloaded(addon) {
+function promiseInstallEvent(addon, event) {
return new Promise(resolve => {
- let listener = {
- onDownloadEnded(install) {
- if (install.addon.id == addon.id) {
- AddonManager.removeInstallListener(listener);
- resolve();
- }
- },
+ let listener = {};
+ listener[event] = (install, ...args) => {
+ if (install.addon.id == addon.id) {
+ AddonManager.removeInstallListener(listener);
+ resolve(...args);
+ }
};
AddonManager.addInstallListener(listener);
});
}
-function promiseUpgrade(addon) {
- return new Promise(resolve => {
- let listener = {
- onInstallEnded(install, newAddon) {
- if (newAddon.id == addon.id) {
- AddonManager.removeInstallListener(listener);
- resolve(newAddon);
- }
- },
- };
- AddonManager.addInstallListener(listener);
- });
-}
-
-add_task(function* () {
- yield SpecialPowers.pushPrefEnv({set: [
- // Turn on background updates
- ["extensions.update.enabled", true],
-
- // Point updates to the local mochitest server
- ["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
-
+// Set some prefs that apply to all the tests in this file
+add_task(function setup() {
+ return SpecialPowers.pushPrefEnv({set: [
// We don't have pre-pinned certificates for the local mochitest server
["extensions.install.requireBuiltInCerts", false],
["extensions.update.requireBuiltInCerts", false],
// XXX remove this when prompts are enabled by default
["extensions.webextPermissionPrompts", true],
]});
+});
+
+add_task(function* test_background_update() {
+ yield SpecialPowers.pushPrefEnv({set: [
+ // Turn on background updates
+ ["extensions.update.enabled", true],
+
+ // Point updates to the local mochitest server
+ ["extensions.update.background.url", `${URL_BASE}/browser_webext_update.json`],
+ ]});
// Install version 1.0 of the test extension
- let url1 = `${URL_BASE}/browser_webext_update1.xpi`;
- let install = yield AddonManager.getInstallForURL(url1, null, "application/x-xpinstall");
- ok(install, "Created install");
-
- let addon = yield new Promise(resolve => {
- install.addListener({
- onInstallEnded(_install, _addon) {
- resolve(_addon);
- },
- });
- install.install();
- });
+ let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update1.xpi`);
ok(addon, "Addon was installed");
is(getBadgeStatus(), "", "Should not start out with an addon alert badge");
// Trigger an update check and wait for the update for this addon
// to be downloaded.
- let updatePromise = promiseUpdateDownloaded(addon);
+ let updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
+
AddonManagerPrivate.backgroundUpdateCheck();
yield updatePromise;
is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
// Find the menu entry for the update
yield PanelUI.show();
@@ -147,17 +142,17 @@ add_task(function* () {
is(getBadgeStatus(), "", "Addon alert badge should be gone");
yield PanelUI.show();
addons = document.getElementById("PanelUI-footer-addons");
is(addons.children.length, 0, "Update menu entries should be gone");
yield PanelUI.hide();
// Re-check for an update
- updatePromise = promiseUpdateDownloaded(addon);
+ updatePromise = promiseInstallEvent(addon, "onDownloadEnded");
yield AddonManagerPrivate.backgroundUpdateCheck();
yield updatePromise;
is(getBadgeStatus(), "addon-alert", "Should have addon alert badge");
// Find the menu entry for the update
yield PanelUI.show();
@@ -174,19 +169,134 @@ add_task(function* () {
is(tab.linkedBrowser.currentURI.spec, "about:addons");
yield promiseViewLoaded(tab, VIEW);
win = tab.linkedBrowser.contentWindow;
ok(!win.gViewController.isLoading, "about:addons view is fully loaded");
is(win.gViewController.currentViewId, VIEW, "about:addons is at extensions list");
// Wait for the permission prompt and accept it this time
- updatePromise = promiseUpgrade(addon);
+ updatePromise = promiseInstallEvent(addon, "onInstallEnded");
panel = yield popupPromise;
panel.button.click();
addon = yield updatePromise;
is(addon.version, "2.0", "Should have upgraded to the new version");
yield BrowserTestUtils.removeTab(tab);
is(getBadgeStatus(), "", "Addon alert badge should be gone");
+
+ addon.uninstall();
+ yield SpecialPowers.popPrefEnv();
});
+
+// Helper function to test a specific scenario for interactive updates.
+// `checkFn` is a callable that triggers a check for updates.
+// `autoUpdate` specifies whether the test should be run with
+// updates applied automatically or not.
+function* interactiveUpdateTest(autoUpdate, checkFn) {
+ yield SpecialPowers.pushPrefEnv({set: [
+ ["extensions.update.autoUpdateDefault", autoUpdate],
+
+ // Point updates to the local mochitest server
+ ["extensions.update.url", `${URL_BASE}/browser_webext_update.json`],
+ ]});
+
+ // Trigger an update check, manually applying the update if we're testing
+ // without auto-update.
+ function* triggerUpdate(win, addon) {
+ let manualUpdatePromise;
+ if (!autoUpdate) {
+ manualUpdatePromise = new Promise(resolve => {
+ let listener = {
+ onNewInstall() {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ },
+ };
+ AddonManager.addInstallListener(listener);
+ });
+ }
+
+ checkFn(win, addon);
+
+ if (manualUpdatePromise) {
+ yield manualUpdatePromise;
+
+ let item = win.document.getElementById("addon-list")
+ .children.find(_item => _item.value == ID);
+ EventUtils.synthesizeMouseAtCenter(item._updateBtn, {}, win);
+ }
+ }
+
+ // Install version 1.0 of the test extension
+ let addon = yield promiseInstallAddon(`${URL_BASE}/browser_webext_update1.xpi`);
+ ok(addon, "Addon was installed");
+ is(addon.version, "1.0", "Version 1 of the addon is installed");
+
+ // Open add-ons manager and navigate to extensions list
+ let loadPromise = new Promise(resolve => {
+ let listener = (subject, topic) => {
+ if (subject.location.href == "about:addons") {
+ Services.obs.removeObserver(listener, topic);
+ resolve(subject);
+ }
+ };
+ Services.obs.addObserver(listener, "EM-loaded", false);
+ });
+ let tab = gBrowser.addTab("about:addons");
+ gBrowser.selectedTab = tab;
+ let win = yield loadPromise;
+
+ const VIEW = "addons://list/extension";
+ let viewPromise = promiseViewLoaded(tab, VIEW);
+ win.loadView(VIEW);
+ yield viewPromise;
+
+ // Trigger an update check
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ yield triggerUpdate(win, addon);
+ let panel = yield popupPromise;
+
+ // Click the cancel button, wait to see the cancel event
+ let cancelPromise = promiseInstallEvent(addon, "onInstallCancelled");
+ panel.secondaryButton.click();
+ yield cancelPromise;
+
+ addon = yield AddonManager.getAddonByID(ID);
+ is(addon.version, "1.0", "Should still be running the old version");
+
+ // Trigger a new update check
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ yield triggerUpdate(win, addon);
+
+ // This time, accept the upgrade
+ let updatePromise = promiseInstallEvent(addon, "onInstallEnded");
+ panel = yield popupPromise;
+ panel.button.click();
+
+ addon = yield updatePromise;
+ is(addon.version, "2.0", "Should have upgraded");
+
+ yield BrowserTestUtils.removeTab(tab);
+ addon.uninstall();
+ yield SpecialPowers.popPrefEnv();
+}
+
+// Invoke the "Check for Updates" menu item
+function checkAll(win) {
+ win.gViewController.doCommand("cmd_findAllUpdates");
+}
+
+// Test "Check for Updates" with both auto-update settings
+add_task(() => interactiveUpdateTest(true, checkAll));
+add_task(() => interactiveUpdateTest(false, checkAll));
+
+
+// Invoke an invidual extension's "Find Updates" menu item
+function checkOne(win, addon) {
+ win.gViewController.doCommand("cmd_findItemUpdates", addon);
+}
+
+// Test "Find Updates" with both auto-update settings
+add_task(() => interactiveUpdateTest(true, checkOne));
+add_task(() => interactiveUpdateTest(false, checkOne));
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -16,16 +16,18 @@ var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/DownloadUtils.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", "resource:///modules/E10SUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
"resource://gre/modules/ExtensionParent.jsm");
const CONSTANTS = {};
Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS);
const SIGNING_REQUIRED = CONSTANTS.REQUIRE_SIGNING ?
true :
Services.prefs.getBoolPref("xpinstall.signatures.required");
@@ -33,16 +35,19 @@ const SIGNING_REQUIRED = CONSTANTS.REQUI
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Experiments",
"resource:///modules/experiments/Experiments.jsm");
+XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
+ "extensions.webextPermissionPrompts", false);
+
const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
const PREF_XPI_ENABLED = "xpinstall.enabled";
const PREF_MAXRESULTS = "extensions.getAddons.maxResults";
const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled";
const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden";
const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
@@ -689,16 +694,50 @@ var gEventManager = {
this.refreshGlobalWarning();
},
onUpdateModeChanged() {
this.refreshAutoUpdateDefault();
}
};
+function attachUpdateHandler(install) {
+ if (!WEBEXT_PERMISSION_PROMPTS) {
+ return;
+ }
+
+ install.promptHandler = (info) => {
+ let oldPerms = info.existingAddon.userPermissions || {hosts: [], permissions: []};
+ let newPerms = info.addon.userPermissions;
+
+ let difference = Extension.comparePermissions(oldPerms, newPerms);
+
+ // If there are no new permissions, just proceed
+ if (difference.hosts.length == 0 && difference.permissions.length == 0) {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ let subject = {
+ wrappedJSObject: {
+ target: getBrowserElement(),
+ info: {
+ type: "update",
+ addon: info.addon,
+ icon: info.addon.icon,
+ permissions: difference,
+ resolve,
+ reject,
+ },
+ },
+ };
+ Services.obs.notifyObservers(subject, "webextension-permission-prompt", null);
+ });
+ };
+}
var gViewController = {
viewPort: null,
currentViewId: "",
currentViewObj: null,
currentViewRequest: 0,
viewObjects: {},
viewChangeCallback: null,
@@ -1076,16 +1115,20 @@ var gViewController = {
}
}
var updateInstallListener = {
onDownloadFailed() {
pendingChecks--;
updateStatus();
},
+ onInstallCancelled() {
+ pendingChecks--;
+ updateStatus();
+ },
onInstallFailed() {
pendingChecks--;
updateStatus();
},
onInstallEnded(aInstall, aAddon) {
pendingChecks--;
numUpdated++;
if (isPending(aInstall.existingAddon, "upgrade"))
@@ -1093,16 +1136,17 @@ var gViewController = {
updateStatus();
}
};
var updateCheckListener = {
onUpdateAvailable(aAddon, aInstall) {
gEventManager.delegateAddonEvent("onUpdateAvailable",
[aAddon, aInstall]);
+ attachUpdateHandler(aInstall);
if (AddonManager.shouldAutoUpdate(aAddon)) {
aInstall.addListener(updateInstallListener);
aInstall.install();
} else {
pendingChecks--;
numManualUpdates++;
updateStatus();
}
@@ -1138,16 +1182,17 @@ var gViewController = {
return false;
return hasPermission(aAddon, "upgrade");
},
doCommand(aAddon) {
var listener = {
onUpdateAvailable(aAddon, aInstall) {
gEventManager.delegateAddonEvent("onUpdateAvailable",
[aAddon, aInstall]);
+ attachUpdateHandler(aInstall);
if (AddonManager.shouldAutoUpdate(aAddon))
aInstall.install();
},
onNoUpdateAvailable(aAddon) {
gEventManager.delegateAddonEvent("onNoUpdateAvailable",
[aAddon]);
}
};