summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/parent/ext-management.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/parent/ext-management.js')
-rw-r--r--toolkit/components/extensions/parent/ext-management.js354
1 files changed, 354 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/ext-management.js b/toolkit/components/extensions/parent/ext-management.js
new file mode 100644
index 0000000000..a930a8d38e
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-management.js
@@ -0,0 +1,354 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "strBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://global/locale/extensions.properties"
+ );
+});
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+
+// We can't use Services.prompt here at the moment, as tests need to mock
+// the prompt service. We could use sinon, but that didn't seem to work
+// with Android builds.
+// eslint-disable-next-line mozilla/use-services
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "promptService",
+ "@mozilla.org/prompter;1",
+ "nsIPromptService"
+);
+
+var { ExtensionError } = ExtensionUtils;
+
+const _ = (key, ...args) => {
+ if (args.length) {
+ return strBundle.formatStringFromName(key, args);
+ }
+ return strBundle.GetStringFromName(key);
+};
+
+const installType = addon => {
+ if (addon.temporarilyInstalled) {
+ return "development";
+ } else if (addon.foreignInstall) {
+ return "sideload";
+ } else if (addon.isSystem) {
+ return "other";
+ }
+ return "normal";
+};
+
+const 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;
+
+ let hostPerms = extension.allowedOrigins.patterns.map(
+ matcher => matcher.pattern
+ );
+
+ extInfo.permissions = Array.from(extension.permissions).filter(perm => {
+ return !hostPerms.includes(perm);
+ });
+ extInfo.hostPermissions = hostPerms;
+
+ 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;
+};
+
+// Some management APIs are intentionally limited.
+const allowedTypes = ["theme", "extension"];
+
+function checkAllowedAddon(addon) {
+ if (addon.isSystem || addon.isAPIExtension) {
+ return false;
+ }
+ if (addon.type == "extension" && !addon.isWebExtension) {
+ return false;
+ }
+ return allowedTypes.includes(addon.type);
+}
+
+class ManagementAddonListener extends ExtensionCommon.EventEmitter {
+ eventNames = ["onEnabled", "onDisabled", "onInstalled", "onUninstalled"];
+
+ hasAnyListeners() {
+ for (let event of this.eventNames) {
+ if (this.has(event)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ on(event, listener) {
+ if (!this.eventNames.includes(event)) {
+ throw new Error("unsupported event");
+ }
+ if (!this.hasAnyListeners()) {
+ AddonManager.addAddonListener(this);
+ }
+ super.on(event, listener);
+ }
+
+ off(event, listener) {
+ if (!this.eventNames.includes(event)) {
+ throw new Error("unsupported event");
+ }
+ super.off(event, listener);
+ if (!this.hasAnyListeners()) {
+ AddonManager.removeAddonListener(this);
+ }
+ }
+
+ getExtensionInfo(addon) {
+ let ext = WebExtensionPolicy.getByID(addon.id)?.extension;
+ return getExtensionInfoForAddon(ext, addon);
+ }
+
+ onEnabled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onEnabled", this.getExtensionInfo(addon));
+ }
+
+ onDisabled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onDisabled", this.getExtensionInfo(addon));
+ }
+
+ onInstalled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onInstalled", this.getExtensionInfo(addon));
+ }
+
+ onUninstalled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onUninstalled", this.getExtensionInfo(addon));
+ }
+}
+
+this.management = class extends ExtensionAPIPersistent {
+ addonListener = new ManagementAddonListener();
+
+ onShutdown() {
+ AddonManager.removeAddonListener(this.addonListener);
+ }
+
+ eventRegistrar(eventName) {
+ return ({ fire }) => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ this.addonListener.on(eventName, listener);
+ return {
+ unregister: () => {
+ this.addonListener.off(eventName, listener);
+ },
+ convert(_fire, context) {
+ fire = _fire;
+ },
+ };
+ };
+ }
+
+ PERSISTENT_EVENTS = {
+ onDisabled: this.eventRegistrar("onDisabled"),
+ onEnabled: this.eventRegistrar("onEnabled"),
+ onInstalled: this.eventRegistrar("onInstalled"),
+ onUninstalled: this.eventRegistrar("onUninstalled"),
+ };
+
+ getAPI(context) {
+ let { extension } = context;
+
+ return {
+ management: {
+ async get(id) {
+ let addon = await AddonManager.getAddonByID(id);
+ if (!addon) {
+ throw new ExtensionError(`No such addon ${id}`);
+ }
+ if (!checkAllowedAddon(addon)) {
+ throw new ExtensionError("get not allowed for this addon");
+ }
+ // If the extension is enabled get it and use it for more data.
+ let ext = WebExtensionPolicy.getByID(addon.id)?.extension;
+ return getExtensionInfoForAddon(ext, addon);
+ },
+
+ async getAll() {
+ let addons = await AddonManager.getAddonsByTypes(allowedTypes);
+ return addons.filter(checkAllowedAddon).map(addon => {
+ // If the extension is enabled get it and use it for more data.
+ let ext = WebExtensionPolicy.getByID(addon.id)?.extension;
+ return getExtensionInfoForAddon(ext, addon);
+ });
+ },
+
+ async install({ url, hash }) {
+ let listener = {
+ onDownloadEnded(install) {
+ if (install.addon.appDisabled || install.addon.type !== "theme") {
+ install.cancel();
+ return false;
+ }
+ },
+ };
+
+ let telemetryInfo = {
+ source: "extension",
+ method: "management-webext-api",
+ };
+ let install = await AddonManager.getInstallForURL(url, {
+ hash,
+ telemetryInfo,
+ triggeringPrincipal: extension.principal,
+ });
+ install.addListener(listener);
+ try {
+ await install.install();
+ } catch (e) {
+ Cu.reportError(e);
+ throw new ExtensionError("Incompatible addon");
+ }
+ await install.addon.enable();
+ return { id: install.addon.id };
+ },
+
+ async getSelf() {
+ let addon = await AddonManager.getAddonByID(extension.id);
+ return getExtensionInfoForAddon(extension, addon);
+ },
+
+ 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 =
+ Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING +
+ Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.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();
+ },
+
+ async setEnabled(id, enabled) {
+ let addon = await AddonManager.getAddonByID(id);
+ if (!addon) {
+ throw new ExtensionError(`No such addon ${id}`);
+ }
+ if (addon.type !== "theme") {
+ throw new ExtensionError("setEnabled applies only to theme addons");
+ }
+ if (addon.isSystem) {
+ throw new ExtensionError(
+ "setEnabled cannot be used with a system addon"
+ );
+ }
+ if (enabled) {
+ await addon.enable();
+ } else {
+ await addon.disable();
+ }
+ },
+
+ onDisabled: new EventManager({
+ context,
+ module: "management",
+ event: "onDisabled",
+ extensionApi: this,
+ }).api(),
+
+ onEnabled: new EventManager({
+ context,
+ module: "management",
+ event: "onEnabled",
+ extensionApi: this,
+ }).api(),
+
+ onInstalled: new EventManager({
+ context,
+ module: "management",
+ event: "onInstalled",
+ extensionApi: this,
+ }).api(),
+
+ onUninstalled: new EventManager({
+ context,
+ module: "management",
+ event: "onUninstalled",
+ extensionApi: this,
+ }).api(),
+ },
+ };
+ }
+};