360 lines
10 KiB
JavaScript
360 lines
10 KiB
JavaScript
/* -*- 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";
|
|
|
|
ChromeUtils.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";
|
|
} else if (addon.isInstalledByEnterprisePolicy) {
|
|
return "admin";
|
|
}
|
|
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) {
|
|
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" &&
|
|
!extension.isInstalledByEnterprisePolicy
|
|
) {
|
|
throw new ExtensionError(
|
|
"setEnabled can only be used for themes or by addons installed by enterprise policy"
|
|
);
|
|
}
|
|
if (!checkAllowedAddon(addon)) {
|
|
throw new ExtensionError("setEnabled not allowed for this 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(),
|
|
},
|
|
};
|
|
}
|
|
};
|