Bug 1220136 - WebExtensions support chrome.management.uninstallSelf, r?kmag
MozReview-Commit-ID: L80ynbuFr7U
--- a/toolkit/components/extensions/ext-management.js
+++ b/toolkit/components/extensions/ext-management.js
@@ -1,14 +1,30 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
+ const stringSvc = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
+ return stringSvc.createBundle("chrome://global/locale/extensions.properties");
+});
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "promptService",
+ "@mozilla.org/embedcomp/prompt-service;1",
+ "nsIPromptService");
+
+function _(key, ...args) {
+ if (args.length) {
+ return strBundle.formatStringFromName(key, args, args.length);
+ }
+ return strBundle.GetStringFromName(key);
+}
function installType(addon) {
if (addon.temporarilyInstalled) {
return "development";
} else if (addon.foreignInstall) {
return "sideload";
} else if (addon.isSystem) {
return "other";
@@ -47,16 +63,47 @@ extensions.registerSchemaAPI("management
}
if (m.icons) {
extInfo.icons = Object.keys(m.icons).map(key => {
return {size: Number(key), url: m.icons[key]};
});
}
resolve(extInfo);
- } catch (e) {
- reject(e);
+ } catch (err) {
+ reject(err);
}
}));
},
+
+ uninstallSelf: function(options) {
+ return new Promise((resolve, reject) => {
+ if (options && options.showConfirmDialog) {
+ let message = _("uninstall.confirmation.message", extension.name);
+ if (options.dialogMessage) {
+ message = `${options.dialogMessage}\n${message}`;
+ }
+ let title = _("uninstall.confirmation.title", extension.name);
+ let buttonFlags = promptService.BUTTON_POS_0 * promptService.BUTTON_TITLE_IS_STRING +
+ promptService.BUTTON_POS_1 * promptService.BUTTON_TITLE_IS_STRING;
+ let button0Title = _("uninstall.confirmation.button-0.label");
+ let button1Title = _("uninstall.confirmation.button-1.label");
+ let response = promptService.confirmEx(null, title, message, buttonFlags, button0Title, button1Title, null, null, {value: 0});
+ if (response == 1) {
+ return reject({message: "User cancelled uninstall of extension"});
+ }
+ }
+ AddonManager.getAddonByID(extension.id, addon => {
+ let canUninstall = Boolean(addon.permissions & AddonManager.PERM_CAN_UNINSTALL);
+ if (!canUninstall) {
+ return reject({message: "The add-on cannot be uninstalled"});
+ }
+ try {
+ addon.uninstall();
+ } catch (err) {
+ return reject(err);
+ }
+ });
+ });
+ },
},
};
});
--- a/toolkit/components/extensions/schemas/management.json
+++ b/toolkit/components/extensions/schemas/management.json
@@ -212,29 +212,33 @@
}
]
}
]
},
{
"name": "uninstallSelf",
"type": "function",
- "unsupported": true,
"description": "Uninstalls the calling extension. Note: This function can be used without requesting the 'management' permission in the manifest.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "options",
"optional": true,
"properties": {
"showConfirmDialog": {
"type": "boolean",
"optional": true,
"description": "Whether or not a confirm-uninstall dialog should prompt the user. Defaults to false."
+ },
+ "dialogMessage": {
+ "type": "string",
+ "optional": true,
+ "description": "The message to display to a user when being asked to confirm removal of the extension."
}
}
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": []
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js
@@ -0,0 +1,137 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://testing-common/AddonTestUtils.jsm");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+const {promiseAddonByID} = AddonTestUtils;
+const id = "uninstall_self_test@tests.mozilla.com";
+
+const manifest = {
+ applications: {
+ gecko: {
+ id,
+ },
+ },
+ name: "test extension name",
+ version: "1.0",
+};
+
+const waitForUninstalled = new Promise(resolve => {
+ const listener = {
+ onUninstalled: (addon) => {
+ equal(addon.id, id, "The expected add-on has been uninstalled");
+ AddonManager.getAddonByID(addon.id, checkedAddon => {
+ equal(checkedAddon, null, "Add-on no longer exists");
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ });
+ },
+ };
+ AddonManager.addAddonListener(listener);
+});
+
+let promptService = {
+ _response: null,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
+ confirmEx: function(...args) {
+ this._confirmExArgs = args;
+ return this._response;
+ },
+};
+
+add_task(function* setup() {
+ let fakePromptService = MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(fakePromptService);
+ });
+ yield ExtensionTestUtils.startAddonManager();
+});
+
+add_task(function* test_management_uninstall_no_prompt() {
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.management.uninstallSelf();
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield waitForUninstalled;
+ yield extension.markUnloaded();
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
+
+add_task(function* test_management_uninstall_prompt_uninstall() {
+ promptService._response = 0;
+
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.management.uninstallSelf({showConfirmDialog: true});
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield waitForUninstalled;
+ yield extension.markUnloaded();
+
+ // Test localization strings
+ equal(promptService._confirmExArgs[1], `Uninstall ${manifest.name}`);
+ equal(promptService._confirmExArgs[2],
+ `The extension “${manifest.name}” is requesting to be uninstalled. What would you like to do?`);
+ equal(promptService._confirmExArgs[4], "Uninstall");
+ equal(promptService._confirmExArgs[5], "Keep Installed");
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
+
+add_task(function* test_management_uninstall_prompt_keep() {
+ promptService._response = 1;
+
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.management.uninstallSelf({showConfirmDialog: true}).then(() => {
+ browser.test.fail("uninstallSelf rejects when user declines uninstall");
+ }, error => {
+ browser.test.assertEq("User cancelled uninstall of extension",
+ error.message,
+ "Expected rejection when user declines uninstall");
+ browser.test.sendMessage("uninstall-rejected");
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ let addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on is installed");
+ extension.sendMessage("uninstall");
+ yield extension.awaitMessage("uninstall-rejected");
+ addon = yield promiseAddonByID(id);
+ notEqual(addon, null, "Add-on remains installed");
+ yield extension.unload();
+ Services.obs.notifyObservers(extension.extension.file, "flush-cache-entry", null);
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -33,16 +33,17 @@ skip-if = os == "android"
skip-if = os == "android"
[test_ext_experiments.js]
skip-if = release_build
[test_ext_extension.js]
[test_ext_idle.js]
[test_ext_json_parser.js]
[test_ext_localStorage.js]
[test_ext_management.js]
+[test_ext_management_uninstall_self.js]
[test_ext_manifest_content_security_policy.js]
[test_ext_manifest_incognito.js]
[test_ext_manifest_minimum_chrome_version.js]
[test_ext_onmessage_removelistener.js]
[test_ext_runtime_connect_no_receiver.js]
[test_ext_runtime_getPlatformInfo.js]
[test_ext_runtime_sendMessage.js]
[test_ext_runtime_sendMessage_errors.js]
--- a/toolkit/locales/en-US/chrome/global/extensions.properties
+++ b/toolkit/locales/en-US/chrome/global/extensions.properties
@@ -13,8 +13,17 @@ csp.error.illegal-protocol = ‘%1$S’ directive contains a forbidden %2$S: protocol source
#LOCALIZATION NOTE (csp.error.missing-host) %2$S a protocol name, such as "http", which appears as "http:", as it would in a URL.
csp.error.missing-host = %2$S: protocol requires a host in ‘%1$S’ directives
#LOCALIZATION NOTE (csp.error.missing-source) %1$S is the name of a CSP directive, such as "script-src". %2$S is the name of a CSP source, usually 'self'.
csp.error.missing-source = ‘%1$S’ must include the source %2$S
#LOCALIZATION NOTE (csp.error.illegal-host-wildcard) %2$S a protocol name, such as "http", which appears as "http:", as it would in a URL.
csp.error.illegal-host-wildcard = %2$S: wildcard sources in ‘%1$S’ directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)
+
+#LOCALIZATION NOTE (uninstall.confirmation.title) %S is the name of the extension which is about to be uninstalled.
+uninstall.confirmation.title = Uninstall %S
+
+#LOCALIZATION NOTE (uninstall.confirmation.message) %S is the name of the extension which is about to be uninstalled.
+uninstall.confirmation.message = The extension “%S” is requesting to be uninstalled. What would you like to do?
+
+uninstall.confirmation.button-0.label = Uninstall
+uninstall.confirmation.button-1.label = Keep Installed