--- a/browser/components/extensions/test/mochitest/mochitest.ini
+++ b/browser/components/extensions/test/mochitest/mochitest.ini
@@ -1,6 +1,7 @@
[DEFAULT]
support-files =
../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+ ../../../../../toolkit/components/extensions/test/mochitest/file_sample.html
tags = webextensions
[test_ext_all_apis.html]
--- a/mobile/android/components/extensions/test/mochitest/mochitest.ini
+++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini
@@ -1,11 +1,12 @@
[DEFAULT]
support-files =
../../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+ ../../../../../../toolkit/components/extensions/test/mochitest/file_sample.html
../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js
context.html
context_tabs_onUpdated_iframe.html
context_tabs_onUpdated_page.html
file_bypass_cache.sjs
file_dummy.html
file_iframe_document.html
file_iframe_document.sjs
--- a/toolkit/components/extensions/ext-management.js
+++ b/toolkit/components/extensions/ext-management.js
@@ -6,16 +6,27 @@ XPCOMUtils.defineLazyGetter(this, "strBu
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");
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyGetter(this, "GlobalManager", () => {
+ const {GlobalManager} = Cu.import("resource://gre/modules/Extension.jsm", {});
+ return GlobalManager;
+});
+
+var {
+ ExtensionError,
+} = ExtensionUtils;
function _(key, ...args) {
if (args.length) {
return strBundle.formatStringFromName(key, args, args.length);
}
return strBundle.GetStringFromName(key);
}
@@ -25,85 +36,218 @@ function installType(addon) {
} else if (addon.foreignInstall) {
return "sideload";
} else if (addon.isSystem) {
return "other";
}
return "normal";
}
+function getExtensionInfoForAddon(extension, addon) {
+ let extInfo = {
+ id: addon.id,
+ name: addon.name,
+ description: addon.description || "",
+ version: addon.version,
+ mayDisable: !!(addon.permissions & AddonManager.PERM_CAN_DISABLE),
+ enabled: addon.isActive,
+ optionsUrl: addon.optionsURL || "",
+ installType: installType(addon),
+ type: addon.type,
+ };
+
+ if (extension) {
+ let m = extension.manifest;
+ extInfo.permissions = Array.from(extension.permissions).filter(perm => {
+ return !extension.whiteListedHosts.pat.includes(perm);
+ });
+ extInfo.hostPermissions = extension.whiteListedHosts.pat;
+ extInfo.shortName = m.short_name || "";
+ if (m.icons) {
+ extInfo.icons = Object.keys(m.icons).map(key => {
+ return {size: Number(key), url: m.icons[key]};
+ });
+ }
+ }
+
+ if (!addon.isActive) {
+ extInfo.disabledReason = "unknown";
+ }
+ if (addon.homepageURL) {
+ extInfo.homepageUrl = addon.homepageURL;
+ }
+ if (addon.updateURL) {
+ extInfo.updateUrl = addon.updateURL;
+ }
+ return extInfo;
+}
+
+const listenerMap = new WeakMap();
+// Some management APIs are intentionally limited.
+const allowedTypes = ["theme"];
+
+class AddonListener {
+ constructor() {
+ AddonManager.addAddonListener(this);
+ EventEmitter.decorate(this);
+ }
+
+ release() {
+ AddonManager.removeAddonListener(this);
+ }
+
+ getExtensionInfo(addon) {
+ let ext = addon.isWebExtension && GlobalManager.extensionMap.get(addon.id);
+ return getExtensionInfoForAddon(ext, addon);
+ }
+
+ onEnabled(addon) {
+ if (!allowedTypes.includes(addon.type)) {
+ return;
+ }
+ this.emit("onEnabled", this.getExtensionInfo(addon));
+ }
+
+ onDisabled(addon) {
+ if (!allowedTypes.includes(addon.type)) {
+ return;
+ }
+ this.emit("onDisabled", this.getExtensionInfo(addon));
+ }
+
+ onInstalled(addon) {
+ if (!allowedTypes.includes(addon.type)) {
+ return;
+ }
+ this.emit("onInstalled", this.getExtensionInfo(addon));
+ }
+
+ onUninstalled(addon) {
+ if (!allowedTypes.includes(addon.type)) {
+ return;
+ }
+ this.emit("onUninstalled", this.getExtensionInfo(addon));
+ }
+}
+
+let addonListener;
+
+function getListener(extension, context) {
+ if (!listenerMap.has(extension)) {
+ if (!addonListener) {
+ addonListener = new AddonListener();
+ }
+ listenerMap.set(extension, {});
+ context.callOnClose({
+ close: () => {
+ listenerMap.delete(extension);
+ if (listenerMap.length === 0) {
+ addonListener.release();
+ addonListener = null;
+ }
+ },
+ });
+ }
+ return addonListener;
+}
+
this.management = class extends ExtensionAPI {
getAPI(context) {
let {extension} = context;
return {
management: {
- getSelf: function() {
- return new Promise((resolve, reject) => AddonManager.getAddonByID(extension.id, addon => {
- try {
- let m = extension.manifest;
- let extInfo = {
- id: extension.id,
- name: addon.name,
- shortName: m.short_name || "",
- description: addon.description || "",
- version: addon.version,
- mayDisable: !!(addon.permissions & AddonManager.PERM_CAN_DISABLE),
- enabled: addon.isActive,
- optionsUrl: addon.optionsURL || "",
- permissions: Array.from(extension.permissions).filter(perm => {
- return !extension.whiteListedHosts.pat.includes(perm);
- }),
- hostPermissions: extension.whiteListedHosts.pat,
- installType: installType(addon),
- };
- if (addon.homepageURL) {
- extInfo.homepageUrl = addon.homepageURL;
- }
- if (addon.updateURL) {
- extInfo.updateUrl = addon.updateURL;
- }
- if (m.icons) {
- extInfo.icons = Object.keys(m.icons).map(key => {
- return {size: Number(key), url: m.icons[key]};
- });
- }
+ async getAll() {
+ let addons = await AddonManager.getAddonsByTypes(allowedTypes);
+ return addons.map(addon => {
+ // If the extension is enabled get it and use it for more data.
+ let ext = addon.isWebExtension && GlobalManager.extensionMap.get(addon.id);
+ return getExtensionInfoForAddon(ext, addon);
+ });
+ },
+
+ async getSelf() {
+ let addon = await AddonManager.getAddonByID(extension.id);
+ return getExtensionInfoForAddon(extension, addon);
+ },
- resolve(extInfo);
- } catch (err) {
- reject(err);
+ async uninstallSelf(options) {
+ 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) {
+ throw new ExtensionError("User cancelled uninstall of extension");
+ }
+ }
+ let addon = await AddonManager.getAddonByID(extension.id);
+ let canUninstall = Boolean(addon.permissions & AddonManager.PERM_CAN_UNINSTALL);
+ if (!canUninstall) {
+ throw new ExtensionError("The add-on cannot be uninstalled");
+ }
+ addon.uninstall();
},
- 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);
- }
- });
- });
+ async setEnabled(id, enabled) {
+ let addon = await AddonManager.getAddonByID(id);
+ if (!addon) {
+ throw new ExtensionError(`No such addon ${id}`);
+ }
+ if (!allowedTypes.includes(addon.type)) {
+ throw new ExtensionError("setEnabled applies only to theme addons");
+ }
+ addon.userDisabled = !enabled;
},
+
+ onDisabled: new SingletonEventManager(context, "management.onDisabled", fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getListener(extension, context).on("onDisabled", listener);
+ return () => {
+ getListener(extension, context).off("onDisabled", listener);
+ };
+ }).api(),
+
+ onEnabled: new SingletonEventManager(context, "management.onEnabled", fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getListener(extension, context).on("onEnabled", listener);
+ return () => {
+ getListener(extension, context).off("onEnabled", listener);
+ };
+ }).api(),
+
+ onInstalled: new SingletonEventManager(context, "management.onInstalled", fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getListener(extension, context).on("onInstalled", listener);
+ return () => {
+ getListener(extension, context).off("onInstalled", listener);
+ };
+ }).api(),
+
+ onUninstalled: new SingletonEventManager(context, "management.onUninstalled", fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getListener(extension, context).on("onUninstalled", listener);
+ return () => {
+ getListener(extension, context).off("onUninstalled", listener);
+ };
+ }).api(),
+
},
};
}
};
--- a/toolkit/components/extensions/schemas/management.json
+++ b/toolkit/components/extensions/schemas/management.json
@@ -64,17 +64,18 @@
"type": "string"
},
"name": {
"description": "The name of this extension.",
"type": "string"
},
"shortName": {
"description": "A short version of the name of this extension.",
- "type": "string"
+ "type": "string",
+ "optional": true
},
"description": {
"description": "The description of this extension.",
"type": "string"
},
"version": {
"description": "The <a href='manifest/version'>version</a> of this extension.",
"type": "string"
@@ -121,40 +122,41 @@
"optional": true,
"items": {
"$ref": "IconInfo"
}
},
"permissions": {
"description": "Returns a list of API based permissions.",
"type": "array",
+ "optional": true,
"items" : {
"type": "string"
}
},
"hostPermissions": {
"description": "Returns a list of host based permissions.",
"type": "array",
+ "optional": true,
"items" : {
"type": "string"
}
},
"installType": {
"description": "How the extension was installed.",
"$ref": "ExtensionInstallType"
}
}
}
],
"functions": [
{
"name": "getAll",
"type": "function",
"permissions": ["management"],
- "unsupported": true,
"description": "Returns a list of information about installed extensions.",
"async": "callback",
"parameters": [
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": [
@@ -239,12 +241,87 @@
},
{
"name": "callback",
"type": "function",
"optional": true,
"parameters": []
}
]
+ },
+ {
+ "name": "setEnabled",
+ "type": "function",
+ "permissions": ["management"],
+ "description": "Enables or disables the given add-on.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "id",
+ "type": "string",
+ "description": "ID of the add-on to enable/disable."
+ },
+ {
+ "name": "enabled",
+ "type": "boolean",
+ "description": "Whether to enable or disable the add-on."
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onDisabled",
+ "type": "function",
+ "permissions": ["management"],
+ "description": "Fired when an addon has been disabled.",
+ "parameters": [
+ {
+ "name": "info",
+ "$ref": "ExtensionInfo"
+ }
+ ]
+ },
+ {
+ "name": "onEnabled",
+ "type": "function",
+ "permissions": ["management"],
+ "description": "Fired when an addon has been enabled.",
+ "parameters": [
+ {
+ "name": "info",
+ "$ref": "ExtensionInfo"
+ }
+ ]
+ },
+ {
+ "name": "onInstalled",
+ "type": "function",
+ "permissions": ["management"],
+ "description": "Fired when an addon has been installed.",
+ "parameters": [
+ {
+ "name": "info",
+ "$ref": "ExtensionInfo"
+ }
+ ]
+ },
+ {
+ "name": "onUninstalled",
+ "type": "function",
+ "permissions": ["management"],
+ "description": "Fired when an addon has been uninstalled.",
+ "parameters": [
+ {
+ "name": "info",
+ "$ref": "ExtensionInfo"
+ }
+ ]
}
]
}
]
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -2,8 +2,9 @@
support-files =
head.js
[browser_ext_themes_chromeparity.js]
[browser_ext_themes_dynamic_updates.js]
[browser_ext_themes_lwtsupport.js]
[browser_ext_themes_multiple_backgrounds.js]
[browser_ext_themes_persistence.js]
+[browser_ext_management_themes.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_management_themes.js
@@ -0,0 +1,133 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {LightweightThemeManager} = Cu.import("resource://gre/modules/LightweightThemeManager.jsm", {});
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.themes.enabled", true]],
+ });
+});
+
+add_task(async function test_management_themes() {
+ let theme = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "name": "Simple theme test",
+ "version": "1.0",
+ "description": "test theme",
+ "theme": {
+ "images": {
+ "headerURL": "image1.png",
+ },
+ },
+ },
+ files: {
+ "image1.png": BACKGROUND,
+ },
+ useAddonManager: "temporary",
+ });
+
+ async function background() {
+ browser.management.onInstalled.addListener(info => {
+ browser.test.log(`${info.name} was installed`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onInstalled", info.name);
+ });
+ browser.management.onDisabled.addListener(info => {
+ browser.test.log(`${info.name} was disabled`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onDisabled", info.name);
+ });
+ browser.management.onEnabled.addListener(info => {
+ browser.test.log(`${info.name} was enabled`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onEnabled", info.name);
+ });
+ browser.management.onUninstalled.addListener(info => {
+ browser.test.log(`${info.name} was uninstalled`);
+ browser.test.assertEq(info.type, "theme", "addon is theme");
+ browser.test.sendMessage("onUninstalled", info.name);
+ });
+
+ async function getAddon(type) {
+ let addons = await browser.management.getAll();
+ // We get the 3 built-in themes plus the lwt and our addon.
+ browser.test.assertEq(5, addons.length, "got expected addons");
+ let found;
+ for (let addon of addons) {
+ browser.test.assertEq(addon.type, "theme", "addon is theme");
+ if (type == "theme" && addon.id.includes("temporary-addon")) {
+ found = addon;
+ } else if (type == "enabled" && addon.enabled) {
+ found = addon;
+ }
+ }
+ return found;
+ }
+
+ browser.test.onMessage.addListener(async (msg) => {
+ let theme = await getAddon("theme");
+ browser.test.assertEq(theme.description, "test theme", "description is correct");
+ browser.test.assertTrue(theme.enabled, "theme is enabled");
+ await browser.management.setEnabled(theme.id, false);
+
+ theme = await getAddon("theme");
+
+ browser.test.assertTrue(!theme.enabled, "theme is disabled");
+ let addon = getAddon("enabled");
+ browser.test.assertTrue(addon, "another theme was enabled");
+
+ await browser.management.setEnabled(theme.id, true);
+ theme = await getAddon("theme");
+ addon = await getAddon("enabled");
+ browser.test.assertEq(theme.id, addon.id, "theme is enabled");
+
+ browser.test.sendMessage("done");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["management"],
+ },
+ background,
+ });
+ await extension.startup();
+
+ // Test LWT
+ LightweightThemeManager.currentTheme = {
+ id: "lwt@personas.mozilla.org",
+ version: "1",
+ name: "Bling",
+ description: "SO MUCH BLING!",
+ author: "Pixel Pusher",
+ homepageURL: "http://mochi.test:8888/data/index.html",
+ headerURL: "http://mochi.test:8888/data/header.png",
+ previewURL: "http://mochi.test:8888/data/preview.png",
+ iconURL: "http://mochi.test:8888/data/icon.png",
+ textcolor: Math.random().toString(),
+ accentcolor: Math.random().toString(),
+ };
+ is(await extension.awaitMessage("onInstalled"), "Bling", "LWT installed");
+ is(await extension.awaitMessage("onDisabled"), "Default", "default disabled");
+ is(await extension.awaitMessage("onEnabled"), "Bling", "LWT enabled");
+
+ await theme.startup();
+ is(await extension.awaitMessage("onInstalled"), "Simple theme test", "webextension theme installed");
+ is(await extension.awaitMessage("onDisabled"), "Bling", "LWT disabled");
+ // no enabled event when installed.
+
+ extension.sendMessage("test");
+ is(await extension.awaitMessage("onEnabled"), "Default", "default enabled");
+ is(await extension.awaitMessage("onDisabled"), "Simple theme test", "addon disabled");
+ is(await extension.awaitMessage("onEnabled"), "Simple theme test", "addon enabled");
+ is(await extension.awaitMessage("onDisabled"), "Default", "default disabled");
+ await extension.awaitMessage("done");
+
+ await Promise.all([
+ theme.unload(),
+ extension.awaitMessage("onUninstalled"),
+ ]);
+ await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_management.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management.js
@@ -1,20 +1,30 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+add_task(function* setup() {
+ yield ExtensionTestUtils.startAddonManager();
+});
+
add_task(function* test_management_schema() {
- function background() {
+ async function background() {
browser.test.assertTrue(browser.management, "browser.management API exists");
+ let self = await browser.management.getSelf();
+ browser.test.assertEq(browser.runtime.id, self.id, "got self");
browser.test.notifyPass("management-schema");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["management"],
},
background: `(${background})()`,
+ useAddonManager: "temporary",
});
yield extension.startup();
yield extension.awaitFinish("management-schema");
yield extension.unload();
});