1424 lines
41 KiB
JavaScript
1424 lines
41 KiB
JavaScript
/* 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/. */
|
|
|
|
import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
|
|
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
|
|
|
|
const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
|
|
const PRIVATE_BROWSING_PERMS = {
|
|
permissions: [PRIVATE_BROWSING_PERM_NAME],
|
|
origins: [],
|
|
};
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
|
|
AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
|
|
EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
|
|
Extension: "resource://gre/modules/Extension.sys.mjs",
|
|
ExtensionData: "resource://gre/modules/Extension.sys.mjs",
|
|
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
|
|
ExtensionProcessCrashObserver: "resource://gre/modules/Extension.sys.mjs",
|
|
GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs",
|
|
Management: "resource://gre/modules/Extension.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
});
|
|
|
|
const { debug, warn } = GeckoViewUtils.initLogging("Console");
|
|
|
|
export var DownloadTracker = new (class extends EventEmitter {
|
|
constructor() {
|
|
super();
|
|
|
|
// maps numeric IDs to DownloadItem objects
|
|
this._downloads = new Map();
|
|
}
|
|
|
|
onEvent(event, data, callback) {
|
|
switch (event) {
|
|
case "GeckoView:WebExtension:DownloadChanged": {
|
|
const downloadItem = this.getDownloadItemById(data.downloadItemId);
|
|
|
|
if (!downloadItem) {
|
|
callback.onError("Error: Trying to update unknown download");
|
|
return;
|
|
}
|
|
|
|
const delta = downloadItem.update(data);
|
|
if (delta) {
|
|
this.emit("download-changed", {
|
|
delta,
|
|
downloadItem,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
addDownloadItem(item) {
|
|
this._downloads.set(item.id, item);
|
|
}
|
|
|
|
/**
|
|
* Finds and returns a DownloadItem with a certain numeric ID
|
|
*
|
|
* @param {number} id
|
|
* @returns {DownloadItem} download item
|
|
*/
|
|
getDownloadItemById(id) {
|
|
return this._downloads.get(id);
|
|
}
|
|
})();
|
|
|
|
/** Provides common logic between page and browser actions */
|
|
export class ExtensionActionHelper {
|
|
constructor({
|
|
tabTracker,
|
|
windowTracker,
|
|
tabContext,
|
|
properties,
|
|
extension,
|
|
}) {
|
|
this.tabTracker = tabTracker;
|
|
this.windowTracker = windowTracker;
|
|
this.tabContext = tabContext;
|
|
this.properties = properties;
|
|
this.extension = extension;
|
|
}
|
|
|
|
getTab(aTabId) {
|
|
if (aTabId !== null) {
|
|
return this.tabTracker.getTab(aTabId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getWindow(aWindowId) {
|
|
if (aWindowId !== null) {
|
|
return this.windowTracker.getWindow(aWindowId);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
extractProperties(aAction) {
|
|
const merged = {};
|
|
for (const p of this.properties) {
|
|
merged[p] = aAction[p];
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
eventDispatcherFor(aTabId) {
|
|
if (!aTabId) {
|
|
return lazy.EventDispatcher.instance;
|
|
}
|
|
|
|
const windowId = lazy.GeckoViewTabBridge.tabIdToWindowId(aTabId);
|
|
const window = this.windowTracker.getWindow(windowId);
|
|
return window.WindowEventDispatcher;
|
|
}
|
|
|
|
sendRequest(aTabId, aData) {
|
|
return this.eventDispatcherFor(aTabId).sendRequest({
|
|
...aData,
|
|
aTabId,
|
|
extensionId: this.extension.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
class EmbedderPort {
|
|
constructor(portId, messenger) {
|
|
this.id = portId;
|
|
this.messenger = messenger;
|
|
this.dispatcher = lazy.EventDispatcher.byName(`port:${portId}`);
|
|
this.dispatcher.registerListener(this, [
|
|
"GeckoView:WebExtension:PortMessageFromApp",
|
|
"GeckoView:WebExtension:PortDisconnect",
|
|
]);
|
|
}
|
|
close() {
|
|
this.dispatcher.unregisterListener(this, [
|
|
"GeckoView:WebExtension:PortMessageFromApp",
|
|
"GeckoView:WebExtension:PortDisconnect",
|
|
]);
|
|
}
|
|
onPortDisconnect() {
|
|
this.dispatcher.sendRequest({
|
|
type: "GeckoView:WebExtension:Disconnect",
|
|
sender: this.sender,
|
|
});
|
|
this.close();
|
|
}
|
|
onPortMessage(holder) {
|
|
this.dispatcher.sendRequest({
|
|
type: "GeckoView:WebExtension:PortMessage",
|
|
data: holder.deserialize({}),
|
|
});
|
|
}
|
|
onEvent(aEvent, aData) {
|
|
debug`onEvent ${aEvent} ${aData}`;
|
|
|
|
switch (aEvent) {
|
|
case "GeckoView:WebExtension:PortMessageFromApp": {
|
|
const holder = new StructuredCloneHolder(
|
|
"GeckoView:WebExtension:PortMessageFromApp",
|
|
null,
|
|
aData.message
|
|
);
|
|
this.messenger.sendPortMessage(this.id, holder);
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:PortDisconnect": {
|
|
this.messenger.sendPortDisconnect(this.id);
|
|
this.close();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export class GeckoViewConnection {
|
|
constructor(sender, target, nativeApp, allowContentMessaging) {
|
|
this.sender = sender;
|
|
this.target = target;
|
|
this.nativeApp = nativeApp;
|
|
this.allowContentMessaging = allowContentMessaging;
|
|
|
|
if (!allowContentMessaging && sender.envType !== "addon_child") {
|
|
throw new Error(`Unexpected messaging sender: ${JSON.stringify(sender)}`);
|
|
}
|
|
}
|
|
|
|
get dispatcher() {
|
|
if (this.sender.envType === "addon_child") {
|
|
// If this is a WebExtension Page we will have a GeckoSession associated
|
|
// to it and thus a dispatcher.
|
|
const dispatcher = GeckoViewUtils.getDispatcherForWindow(
|
|
this.target.ownerGlobal
|
|
);
|
|
if (dispatcher) {
|
|
return dispatcher;
|
|
}
|
|
|
|
// No dispatcher means this message is coming from a background script,
|
|
// use the global event handler
|
|
return lazy.EventDispatcher.instance;
|
|
} else if (
|
|
this.sender.envType === "content_child" &&
|
|
this.allowContentMessaging
|
|
) {
|
|
// If this message came from a content script, send the message to
|
|
// the corresponding tab messenger so that GeckoSession can pick it
|
|
// up.
|
|
return GeckoViewUtils.getDispatcherForWindow(this.target.ownerGlobal);
|
|
}
|
|
|
|
throw new Error(`Uknown sender envType: ${this.sender.envType}`);
|
|
}
|
|
|
|
_sendMessage({ type, portId, data }) {
|
|
const message = {
|
|
type,
|
|
sender: this.sender,
|
|
data,
|
|
portId,
|
|
extensionId: this.sender.id,
|
|
nativeApp: this.nativeApp,
|
|
};
|
|
|
|
return this.dispatcher.sendRequestForResult(message);
|
|
}
|
|
|
|
sendMessage(data) {
|
|
return this._sendMessage({
|
|
type: "GeckoView:WebExtension:Message",
|
|
data: data.deserialize({}),
|
|
});
|
|
}
|
|
|
|
onConnect(portId, messenger) {
|
|
const port = new EmbedderPort(portId, messenger);
|
|
|
|
this._sendMessage({
|
|
type: "GeckoView:WebExtension:Connect",
|
|
data: {},
|
|
portId: port.id,
|
|
});
|
|
|
|
return port;
|
|
}
|
|
}
|
|
|
|
async function filterPromptPermissions(aPermissions) {
|
|
if (!aPermissions) {
|
|
return [];
|
|
}
|
|
const promptPermissions = [];
|
|
for (const permission of aPermissions) {
|
|
if (!(await lazy.Extension.shouldPromptFor(permission))) {
|
|
continue;
|
|
}
|
|
promptPermissions.push(permission);
|
|
}
|
|
return promptPermissions;
|
|
}
|
|
|
|
// Keep in sync with WebExtension.java
|
|
const FLAG_NONE = 0;
|
|
const FLAG_ALLOW_CONTENT_MESSAGING = 1 << 0;
|
|
|
|
function exportFlags(aPolicy) {
|
|
let flags = FLAG_NONE;
|
|
if (!aPolicy) {
|
|
return flags;
|
|
}
|
|
const { extension } = aPolicy;
|
|
if (extension.hasPermission("nativeMessagingFromContent")) {
|
|
flags |= FLAG_ALLOW_CONTENT_MESSAGING;
|
|
}
|
|
return flags;
|
|
}
|
|
|
|
function normalizePermissions(perms) {
|
|
if (perms?.permissions) {
|
|
perms = { ...perms };
|
|
perms.permissions = perms.permissions.filter(
|
|
perm => !perm.startsWith("internal:")
|
|
);
|
|
}
|
|
return perms;
|
|
}
|
|
|
|
async function exportExtension(aAddon, aSourceURI) {
|
|
// First, let's make sure the policy is ready if present
|
|
let policy = WebExtensionPolicy.getByID(aAddon.id);
|
|
if (policy?.readyPromise) {
|
|
policy = await policy.readyPromise;
|
|
}
|
|
const {
|
|
amoListingURL,
|
|
averageRating,
|
|
blocklistState,
|
|
creator,
|
|
description,
|
|
embedderDisabled,
|
|
fullDescription,
|
|
homepageURL,
|
|
icons,
|
|
id,
|
|
incognito,
|
|
isActive,
|
|
isBuiltin,
|
|
isCorrectlySigned,
|
|
isRecommended,
|
|
name,
|
|
optionsType,
|
|
optionsURL,
|
|
reviewCount,
|
|
reviewURL,
|
|
signedState,
|
|
sourceURI,
|
|
temporarilyInstalled,
|
|
userDisabled,
|
|
version,
|
|
} = aAddon;
|
|
let creatorName = null;
|
|
let creatorURL = null;
|
|
if (creator) {
|
|
const { name, url } = creator;
|
|
creatorName = name;
|
|
creatorURL = url;
|
|
}
|
|
const openOptionsPageInTab =
|
|
optionsType === lazy.AddonManager.OPTIONS_TYPE_TAB;
|
|
const disabledFlags = [];
|
|
if (userDisabled) {
|
|
disabledFlags.push("userDisabled");
|
|
}
|
|
if (blocklistState === Ci.nsIBlocklistService.STATE_BLOCKED) {
|
|
disabledFlags.push("blocklistDisabled");
|
|
} else if (blocklistState === Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
|
|
disabledFlags.push("softBlocklistDisabled");
|
|
}
|
|
if (embedderDisabled) {
|
|
disabledFlags.push("appDisabled");
|
|
}
|
|
// Add-ons without an `isCorrectlySigned` property are correctly signed as
|
|
// they aren't the correct type for signing.
|
|
if (lazy.AddonSettings.REQUIRE_SIGNING && isCorrectlySigned === false) {
|
|
disabledFlags.push("signatureDisabled");
|
|
}
|
|
if (lazy.AddonManager.checkCompatibility && !aAddon.isCompatible) {
|
|
disabledFlags.push("appVersionDisabled");
|
|
}
|
|
const baseURL = policy ? policy.getURL() : "";
|
|
let privateBrowsingAllowed;
|
|
if (policy) {
|
|
privateBrowsingAllowed = policy.privateBrowsingAllowed;
|
|
} else {
|
|
const { permissions } = await lazy.ExtensionPermissions.get(aAddon.id);
|
|
privateBrowsingAllowed = permissions.includes(PRIVATE_BROWSING_PERM_NAME);
|
|
}
|
|
|
|
let updateDate;
|
|
try {
|
|
updateDate = aAddon.updateDate?.toISOString();
|
|
} catch {
|
|
// `installDate` is used as a fallback for `updateDate` but only when the
|
|
// add-on is installed. Before that, `installDate` might be undefined,
|
|
// which would cause `updateDate` (and `installDate`) to be an "invalid
|
|
// date".
|
|
updateDate = null;
|
|
}
|
|
|
|
const requiredPermissions = aAddon.userPermissions?.permissions ?? [];
|
|
const requiredOrigins = aAddon.userPermissions?.origins ?? [];
|
|
const optionalPermissions = aAddon.optionalPermissions?.permissions ?? [];
|
|
const optionalOrigins = aAddon.optionalOriginsNormalized;
|
|
const grantedPermissions = normalizePermissions(
|
|
await lazy.ExtensionPermissions.get(id)
|
|
);
|
|
const grantedOptionalPermissions = grantedPermissions?.permissions ?? [];
|
|
const grantedOptionalOrigins = grantedPermissions?.origins ?? [];
|
|
|
|
return {
|
|
webExtensionId: id,
|
|
locationURI: aSourceURI != null ? aSourceURI.spec : "",
|
|
isBuiltIn: isBuiltin,
|
|
webExtensionFlags: exportFlags(policy),
|
|
metaData: {
|
|
amoListingURL,
|
|
averageRating,
|
|
baseURL,
|
|
blocklistState,
|
|
creatorName,
|
|
creatorURL,
|
|
description,
|
|
disabledFlags,
|
|
downloadUrl: sourceURI?.displaySpec,
|
|
enabled: isActive,
|
|
fullDescription,
|
|
homepageURL,
|
|
icons,
|
|
incognito,
|
|
isRecommended,
|
|
name,
|
|
openOptionsPageInTab,
|
|
optionsPageURL: optionsURL,
|
|
privateBrowsingAllowed,
|
|
reviewCount,
|
|
reviewURL,
|
|
signedState,
|
|
temporary: temporarilyInstalled,
|
|
updateDate,
|
|
version,
|
|
requiredPermissions,
|
|
requiredOrigins,
|
|
optionalPermissions,
|
|
optionalOrigins,
|
|
grantedOptionalPermissions,
|
|
grantedOptionalOrigins,
|
|
},
|
|
};
|
|
}
|
|
|
|
class ExtensionInstallListener {
|
|
constructor(aResolve, aInstall, aInstallId) {
|
|
this.install = aInstall;
|
|
this.installId = aInstallId;
|
|
this.resolve = result => {
|
|
aResolve(result);
|
|
lazy.EventDispatcher.instance.unregisterListener(this, [
|
|
"GeckoView:WebExtension:CancelInstall",
|
|
]);
|
|
};
|
|
lazy.EventDispatcher.instance.registerListener(this, [
|
|
"GeckoView:WebExtension:CancelInstall",
|
|
]);
|
|
}
|
|
|
|
async onEvent(aEvent, aData, aCallback) {
|
|
debug`onEvent ${aEvent} ${aData}`;
|
|
|
|
switch (aEvent) {
|
|
case "GeckoView:WebExtension:CancelInstall": {
|
|
const { installId } = aData;
|
|
if (this.installId !== installId) {
|
|
return;
|
|
}
|
|
this.cancelling = true;
|
|
let cancelled = false;
|
|
try {
|
|
this.install.cancel();
|
|
cancelled = true;
|
|
} catch (ex) {
|
|
// install may have already failed or been cancelled
|
|
debug`Unable to cancel the install installId ${installId}, Error: ${ex}`;
|
|
// When we attempt to cancel an install but the cancellation fails for
|
|
// some reasons (e.g., because it is too late), we need to revert this
|
|
// boolean property to allow another cancellation to be possible.
|
|
// Otherwise, events like `onDownloadCancelled` won't resolve and that
|
|
// will cause problems in the embedder.
|
|
this.cancelling = false;
|
|
}
|
|
aCallback.onSuccess({ cancelled });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
onDownloadCancelled(aInstall) {
|
|
debug`onDownloadCancelled state=${aInstall.state}`;
|
|
// Do not resolve we were told to CancelInstall,
|
|
// to prevent racing with that handler.
|
|
if (!this.cancelling) {
|
|
const { error: installError, state } = aInstall;
|
|
this.resolve({ installError, state });
|
|
}
|
|
}
|
|
|
|
onDownloadFailed(aInstall) {
|
|
debug`onDownloadFailed state=${aInstall.state}`;
|
|
const { error: installError, state } = aInstall;
|
|
this.resolve({ installError, state });
|
|
}
|
|
|
|
onDownloadEnded() {
|
|
// Nothing to do
|
|
}
|
|
|
|
onInstallCancelled(aInstall, aCancelledByUser) {
|
|
debug`onInstallCancelled state=${aInstall.state} cancelledByUser=${aCancelledByUser}`;
|
|
// Do not resolve we were told to CancelInstall,
|
|
// to prevent racing with that handler.
|
|
if (!this.cancelling) {
|
|
const { error: installError, state } = aInstall;
|
|
// An install can be cancelled by the user OR something else, e.g. when
|
|
// the blocklist prevents the install of a blocked add-on.
|
|
this.resolve({ installError, state, cancelledByUser: aCancelledByUser });
|
|
}
|
|
}
|
|
|
|
onInstallFailed(aInstall) {
|
|
debug`onInstallFailed state=${aInstall.state}`;
|
|
const { error: installError, state } = aInstall;
|
|
this.resolve({ installError, state });
|
|
}
|
|
|
|
onInstallPostponed(aInstall) {
|
|
debug`onInstallPostponed state=${aInstall.state}`;
|
|
const { error: installError, state } = aInstall;
|
|
this.resolve({ installError, state });
|
|
}
|
|
|
|
async onInstallEnded(aInstall, aAddon) {
|
|
debug`onInstallEnded addonId=${aAddon.id}`;
|
|
const extension = await exportExtension(aAddon, aInstall.sourceURI);
|
|
this.resolve({ extension });
|
|
}
|
|
}
|
|
|
|
class ExtensionPromptObserver {
|
|
constructor() {
|
|
Services.obs.addObserver(this, "webextension-permission-prompt");
|
|
Services.obs.addObserver(this, "webextension-optional-permission-prompt");
|
|
}
|
|
|
|
async permissionPromptRequest(aInstall, aAddon, aInfo) {
|
|
const { sourceURI } = aInstall;
|
|
const { permissions } = aInfo;
|
|
|
|
const extension = await exportExtension(aAddon, sourceURI);
|
|
const response = await lazy.EventDispatcher.instance.sendRequestForResult({
|
|
type: "GeckoView:WebExtension:InstallPrompt",
|
|
extension,
|
|
permissions: await filterPromptPermissions(permissions.permissions),
|
|
origins: permissions.origins,
|
|
});
|
|
|
|
if (response.allow) {
|
|
if (response.privateBrowsingAllowed) {
|
|
await lazy.ExtensionPermissions.add(aAddon.id, PRIVATE_BROWSING_PERMS);
|
|
} else {
|
|
await lazy.ExtensionPermissions.remove(
|
|
aAddon.id,
|
|
PRIVATE_BROWSING_PERMS
|
|
);
|
|
}
|
|
aInfo.resolve();
|
|
} else {
|
|
aInfo.reject();
|
|
}
|
|
}
|
|
|
|
async optionalPermissionPrompt(aExtensionId, aPermissions, resolve) {
|
|
const response = await lazy.EventDispatcher.instance.sendRequestForResult({
|
|
type: "GeckoView:WebExtension:OptionalPrompt",
|
|
extensionId: aExtensionId,
|
|
permissions: aPermissions,
|
|
});
|
|
resolve(response.allow);
|
|
}
|
|
|
|
observe(aSubject, aTopic) {
|
|
debug`observe ${aTopic}`;
|
|
|
|
switch (aTopic) {
|
|
case "webextension-permission-prompt": {
|
|
const { info } = aSubject.wrappedJSObject;
|
|
const { addon, install } = info;
|
|
this.permissionPromptRequest(install, addon, info);
|
|
break;
|
|
}
|
|
case "webextension-optional-permission-prompt": {
|
|
const { id, permissions, resolve } = aSubject.wrappedJSObject;
|
|
this.optionalPermissionPrompt(id, permissions, resolve);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class AddonInstallObserver {
|
|
constructor() {
|
|
Services.obs.addObserver(this, "addon-install-failed");
|
|
}
|
|
|
|
async onInstallationFailed(aAddon, aAddonName, aError) {
|
|
// aAddon could be null if we have a network error where we can't download the xpi file.
|
|
// aAddon could also be a valid object without an ID when the xpi file is corrupt.
|
|
let extension = null;
|
|
if (aAddon?.id) {
|
|
extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
}
|
|
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnInstallationFailed",
|
|
extension,
|
|
addonId: aAddon?.id,
|
|
addonName: aAddonName,
|
|
addonVersion: aAddon?.version,
|
|
error: aError,
|
|
});
|
|
}
|
|
|
|
observe(aSubject, aTopic) {
|
|
debug`observe ${aTopic}`;
|
|
switch (aTopic) {
|
|
case "addon-install-failed": {
|
|
aSubject.wrappedJSObject.installs.forEach(install => {
|
|
const { addon, error, name } = install;
|
|
// For some errors, we have a valid `addon` but not the `name` set on
|
|
// the `install` object yet so we check both here.
|
|
const addonName = name || addon?.name;
|
|
|
|
this.onInstallationFailed(addon, addonName, error);
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
new ExtensionPromptObserver();
|
|
new AddonInstallObserver();
|
|
|
|
class AddonManagerListener {
|
|
constructor() {
|
|
lazy.AddonManager.addAddonListener(this);
|
|
// Some extension properties are not going to be available right away after the extension
|
|
// have been installed (e.g. in particular metaData.optionsPageURL), the GeckoView event
|
|
// dispatched from onExtensionReady listener will be providing updated extension metadata to
|
|
// the GeckoView side when it is actually going to be available.
|
|
this.onExtensionReady = this.onExtensionReady.bind(this);
|
|
lazy.Management.on("ready", this.onExtensionReady);
|
|
lazy.Management.on("change-permissions", this.onOptionalPermissionsChanged);
|
|
}
|
|
|
|
async onOptionalPermissionsChanged(type, { extensionId }) {
|
|
// In xpcshell tests there wil be test extensions that trigger this event while the
|
|
// AddonManager has not been started at all, on the contrary on a regular browser
|
|
// instance the AddonManager is expected to be already fully started for an extension
|
|
// for the extension to be able to reach the "ready" state, and so we just silently
|
|
// early exit here if the AddonManager is not ready.
|
|
if (!lazy.AddonManager.isReady) {
|
|
return;
|
|
}
|
|
|
|
const addon = await lazy.AddonManager.getAddonByID(extensionId);
|
|
if (!addon) {
|
|
return;
|
|
}
|
|
const extension = await exportExtension(addon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnOptionalPermissionsChanged",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onExtensionReady(name, extInstance) {
|
|
// In xpcshell tests there wil be test extensions that trigger this event while the
|
|
// AddonManager has not been started at all, on the contrary on a regular browser
|
|
// instance the AddonManager is expected to be already fully started for an extension
|
|
// for the extension to be able to reach the "ready" state, and so we just silently
|
|
// early exit here if the AddonManager is not ready.
|
|
if (!lazy.AddonManager.isReady) {
|
|
return;
|
|
}
|
|
|
|
debug`onExtensionReady ${extInstance.id}`;
|
|
|
|
const addonWrapper = await lazy.AddonManager.getAddonByID(extInstance.id);
|
|
if (!addonWrapper) {
|
|
return;
|
|
}
|
|
|
|
const extension = await exportExtension(
|
|
addonWrapper,
|
|
/* aSourceURI */ null
|
|
);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnReady",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onDisabling(aAddon) {
|
|
debug`onDisabling ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnDisabling",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onDisabled(aAddon) {
|
|
debug`onDisabled ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnDisabled",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onEnabling(aAddon) {
|
|
debug`onEnabling ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnEnabling",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onEnabled(aAddon) {
|
|
debug`onEnabled ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnEnabled",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onUninstalling(aAddon) {
|
|
debug`onUninstalling ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnUninstalling",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onUninstalled(aAddon) {
|
|
debug`onUninstalled ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnUninstalled",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onInstalling(aAddon) {
|
|
debug`onInstalling ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnInstalling",
|
|
extension,
|
|
});
|
|
}
|
|
|
|
async onInstalled(aAddon) {
|
|
debug`onInstalled ${aAddon.id}`;
|
|
|
|
const extension = await exportExtension(aAddon, /* aSourceURI */ null);
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnInstalled",
|
|
extension,
|
|
});
|
|
}
|
|
}
|
|
|
|
new AddonManagerListener();
|
|
|
|
class ExtensionProcessListener {
|
|
constructor() {
|
|
this.onExtensionProcessCrash = this.onExtensionProcessCrash.bind(this);
|
|
lazy.Management.on("extension-process-crash", this.onExtensionProcessCrash);
|
|
|
|
lazy.EventDispatcher.instance.registerListener(this, [
|
|
"GeckoView:WebExtension:EnableProcessSpawning",
|
|
"GeckoView:WebExtension:DisableProcessSpawning",
|
|
]);
|
|
}
|
|
|
|
async onEvent(aEvent, aData) {
|
|
debug`onEvent ${aEvent} ${aData}`;
|
|
|
|
switch (aEvent) {
|
|
case "GeckoView:WebExtension:EnableProcessSpawning": {
|
|
debug`Extension process crash -> re-enable process spawning`;
|
|
lazy.ExtensionProcessCrashObserver.enableProcessSpawning();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async onExtensionProcessCrash(name, { childID, processSpawningDisabled }) {
|
|
debug`Extension process crash -> childID=${childID} processSpawningDisabled=${processSpawningDisabled}`;
|
|
|
|
// When an extension process has crashed too many times, Gecko will set
|
|
// `processSpawningDisabled` and no longer allow the extension process
|
|
// spawning. We only want to send a request to the embedder when we are
|
|
// disabling the process spawning. If process spawning is still enabled
|
|
// then we short circuit and don't notify the embedder.
|
|
if (!processSpawningDisabled) {
|
|
return;
|
|
}
|
|
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:OnDisabledProcessSpawning",
|
|
});
|
|
}
|
|
}
|
|
|
|
new ExtensionProcessListener();
|
|
|
|
class MobileWindowTracker extends EventEmitter {
|
|
constructor() {
|
|
super();
|
|
this._topWindow = null;
|
|
this._topNonPBWindow = null;
|
|
}
|
|
|
|
get topWindow() {
|
|
if (this._topWindow) {
|
|
return this._topWindow.get();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get topNonPBWindow() {
|
|
if (this._topNonPBWindow) {
|
|
return this._topNonPBWindow.get();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
setTabActive(aWindow, aActive) {
|
|
const { browser, tab: nativeTab, docShell } = aWindow;
|
|
nativeTab.active = aActive;
|
|
|
|
if (aActive) {
|
|
this._topWindow = Cu.getWeakReference(aWindow);
|
|
const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser);
|
|
if (!isPrivate) {
|
|
this._topNonPBWindow = this._topWindow;
|
|
}
|
|
this.emit("tab-activated", {
|
|
windowId: docShell.outerWindowID,
|
|
tabId: nativeTab.id,
|
|
isPrivate,
|
|
nativeTab,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export var mobileWindowTracker = new MobileWindowTracker();
|
|
|
|
async function updatePromptHandler(aInfo) {
|
|
const oldPerms = aInfo.existingAddon.userPermissions;
|
|
if (!oldPerms) {
|
|
// Updating from a legacy add-on, let it proceed
|
|
return;
|
|
}
|
|
|
|
const newPerms = aInfo.addon.userPermissions;
|
|
|
|
const difference = lazy.Extension.comparePermissions(oldPerms, newPerms);
|
|
|
|
// We only care about permissions that we can prompt the user for
|
|
const newPermissions = await filterPromptPermissions(difference.permissions);
|
|
const { origins: newOrigins } = difference;
|
|
|
|
// If there are no new permissions, just proceed
|
|
if (!newOrigins.length && !newPermissions.length) {
|
|
return;
|
|
}
|
|
|
|
const currentlyInstalled = await exportExtension(
|
|
aInfo.existingAddon,
|
|
/* aSourceURI */ null
|
|
);
|
|
const updatedExtension = await exportExtension(
|
|
aInfo.addon,
|
|
/* aSourceURI */ null
|
|
);
|
|
|
|
const response = await lazy.EventDispatcher.instance.sendRequestForResult({
|
|
type: "GeckoView:WebExtension:UpdatePrompt",
|
|
currentlyInstalled,
|
|
updatedExtension,
|
|
newPermissions,
|
|
newOrigins,
|
|
});
|
|
|
|
if (!response.allow) {
|
|
throw new Error("Extension update rejected.");
|
|
}
|
|
}
|
|
|
|
export var GeckoViewWebExtension = {
|
|
observe(aSubject, aTopic) {
|
|
debug`observe ${aTopic}`;
|
|
|
|
switch (aTopic) {
|
|
case "testing-installed-addon":
|
|
case "testing-uninstalled-addon": {
|
|
// We pretend devtools installed/uninstalled this addon so we don't
|
|
// have to add an API just for internal testing.
|
|
// TODO: assert this is under a test
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:DebuggerListUpdated",
|
|
});
|
|
break;
|
|
}
|
|
|
|
case "devtools-installed-addon": {
|
|
lazy.EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:DebuggerListUpdated",
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
async extensionById(aId) {
|
|
const addon = await lazy.AddonManager.getAddonByID(aId);
|
|
if (!addon) {
|
|
debug`Could not find extension with id=${aId}`;
|
|
return null;
|
|
}
|
|
return addon;
|
|
},
|
|
|
|
async ensureBuiltIn(aUri, aId) {
|
|
await lazy.AddonManager.readyPromise;
|
|
// Although the add-on is privileged in practice due to it being installed
|
|
// as a built-in extension, we pass isPrivileged=false since the exact flag
|
|
// doesn't matter as we are only using ExtensionData to read the version.
|
|
const extensionData = new lazy.ExtensionData(aUri, false);
|
|
const [extensionVersion, extension] = await Promise.all([
|
|
extensionData.getExtensionVersionWithoutValidation(),
|
|
this.extensionById(aId),
|
|
]);
|
|
|
|
if (!extension || extensionVersion != extension.version) {
|
|
return this.installBuiltIn(aUri);
|
|
}
|
|
|
|
const exported = await exportExtension(extension, aUri);
|
|
return { extension: exported };
|
|
},
|
|
|
|
async installBuiltIn(aUri) {
|
|
await lazy.AddonManager.readyPromise;
|
|
const addon = await lazy.AddonManager.installBuiltinAddon(aUri.spec);
|
|
const exported = await exportExtension(addon, aUri);
|
|
return { extension: exported };
|
|
},
|
|
|
|
async installWebExtension(aInstallId, aUri, installMethod) {
|
|
const install = await lazy.AddonManager.getInstallForURL(aUri.spec, {
|
|
telemetryInfo: {
|
|
source: "geckoview-app",
|
|
method: installMethod || undefined,
|
|
},
|
|
});
|
|
const promise = new Promise(resolve => {
|
|
install.addListener(
|
|
new ExtensionInstallListener(resolve, install, aInstallId)
|
|
);
|
|
});
|
|
|
|
lazy.AddonManager.installAddonFromAOM(null, aUri, install);
|
|
|
|
return promise;
|
|
},
|
|
|
|
async setPrivateBrowsingAllowed(aId, aAllowed) {
|
|
if (aAllowed) {
|
|
await lazy.ExtensionPermissions.add(aId, PRIVATE_BROWSING_PERMS);
|
|
} else {
|
|
await lazy.ExtensionPermissions.remove(aId, PRIVATE_BROWSING_PERMS);
|
|
}
|
|
|
|
// Reload the extension if it is already enabled. This ensures any change
|
|
// on the private browsing permission is properly handled.
|
|
const addon = await this.extensionById(aId);
|
|
if (addon.isActive) {
|
|
await addon.reload();
|
|
}
|
|
|
|
return exportExtension(addon, /* aSourceURI */ null);
|
|
},
|
|
|
|
async uninstallWebExtension(aId) {
|
|
const extension = await this.extensionById(aId);
|
|
if (!extension) {
|
|
throw new Error(`Could not find an extension with id='${aId}'.`);
|
|
}
|
|
|
|
return extension.uninstall();
|
|
},
|
|
|
|
async browserActionClick(aId) {
|
|
const policy = WebExtensionPolicy.getByID(aId);
|
|
if (!policy) {
|
|
return undefined;
|
|
}
|
|
|
|
const browserAction = this.browserActions.get(policy.extension);
|
|
if (!browserAction) {
|
|
return undefined;
|
|
}
|
|
|
|
return browserAction.triggerClickOrPopup();
|
|
},
|
|
|
|
async pageActionClick(aId) {
|
|
const policy = WebExtensionPolicy.getByID(aId);
|
|
if (!policy) {
|
|
return undefined;
|
|
}
|
|
|
|
const pageAction = this.pageActions.get(policy.extension);
|
|
if (!pageAction) {
|
|
return undefined;
|
|
}
|
|
|
|
return pageAction.triggerClickOrPopup();
|
|
},
|
|
|
|
async actionDelegateAttached(aId) {
|
|
const policy = WebExtensionPolicy.getByID(aId);
|
|
if (!policy) {
|
|
debug`Could not find extension with id=${aId}`;
|
|
return;
|
|
}
|
|
|
|
const { extension } = policy;
|
|
|
|
const browserAction = this.browserActions.get(extension);
|
|
if (browserAction) {
|
|
// Send information about this action to the delegate
|
|
browserAction.updateOnChange(null);
|
|
}
|
|
|
|
const pageAction = this.pageActions.get(extension);
|
|
if (pageAction) {
|
|
pageAction.updateOnChange(null);
|
|
}
|
|
},
|
|
|
|
async enableWebExtension(aId, aSource) {
|
|
const extension = await this.extensionById(aId);
|
|
if (aSource === "user") {
|
|
await extension.enable();
|
|
} else if (aSource === "app") {
|
|
await extension.setEmbedderDisabled(false);
|
|
}
|
|
return exportExtension(extension, /* aSourceURI */ null);
|
|
},
|
|
|
|
async disableWebExtension(aId, aSource) {
|
|
const extension = await this.extensionById(aId);
|
|
if (aSource === "user") {
|
|
await extension.disable();
|
|
} else if (aSource === "app") {
|
|
await extension.setEmbedderDisabled(true);
|
|
}
|
|
return exportExtension(extension, /* aSourceURI */ null);
|
|
},
|
|
|
|
/**
|
|
* @return A promise resolved with either an AddonInstall object if an update
|
|
* is available or null if no update is found.
|
|
*/
|
|
checkForUpdate(aAddon) {
|
|
return new Promise(resolve => {
|
|
const listener = {
|
|
onUpdateAvailable(aAddon, install) {
|
|
install.promptHandler = updatePromptHandler;
|
|
resolve(install);
|
|
},
|
|
onNoUpdateAvailable() {
|
|
resolve(null);
|
|
},
|
|
};
|
|
aAddon.findUpdates(
|
|
listener,
|
|
lazy.AddonManager.UPDATE_WHEN_USER_REQUESTED
|
|
);
|
|
});
|
|
},
|
|
|
|
async updateWebExtension(aId) {
|
|
// Refresh the cached metadata when necessary. This allows us to always
|
|
// export relatively recent metadata to the embedder.
|
|
if (lazy.AddonRepository.isMetadataStale()) {
|
|
// We use a promise to avoid more than one call to `backgroundUpdateCheck()`
|
|
// when `updateWebExtension()` is called for multiple add-ons in parallel.
|
|
if (!this._promiseAddonRepositoryUpdate) {
|
|
this._promiseAddonRepositoryUpdate =
|
|
lazy.AddonRepository.backgroundUpdateCheck().finally(() => {
|
|
this._promiseAddonRepositoryUpdate = null;
|
|
});
|
|
}
|
|
await this._promiseAddonRepositoryUpdate;
|
|
}
|
|
|
|
// Early-return when extension updates are disabled.
|
|
if (!lazy.AddonManager.updateEnabled) {
|
|
return null;
|
|
}
|
|
|
|
const extension = await this.extensionById(aId);
|
|
|
|
const install = await this.checkForUpdate(extension);
|
|
if (!install) {
|
|
return null;
|
|
}
|
|
const promise = new Promise(resolve => {
|
|
install.addListener(new ExtensionInstallListener(resolve));
|
|
});
|
|
install.install();
|
|
return promise;
|
|
},
|
|
|
|
validateBuiltInLocation(aLocationUri, aCallback) {
|
|
let uri;
|
|
try {
|
|
uri = Services.io.newURI(aLocationUri);
|
|
} catch (ex) {
|
|
aCallback.onError(`Could not parse uri: ${aLocationUri}. Error: ${ex}`);
|
|
return null;
|
|
}
|
|
|
|
if (uri.scheme !== "resource" || uri.host !== "android") {
|
|
aCallback.onError(`Only resource://android/... URIs are allowed.`);
|
|
return null;
|
|
}
|
|
|
|
if (uri.fileName !== "") {
|
|
aCallback.onError(
|
|
`This URI does not point to a folder. Note: folders URIs must end with a "/".`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
return uri;
|
|
},
|
|
|
|
/* eslint-disable complexity */
|
|
async onEvent(aEvent, aData, aCallback) {
|
|
debug`onEvent ${aEvent} ${aData}`;
|
|
|
|
switch (aEvent) {
|
|
case "GeckoView:BrowserAction:Click": {
|
|
const popupUrl = await this.browserActionClick(aData.extensionId);
|
|
aCallback.onSuccess(popupUrl);
|
|
break;
|
|
}
|
|
case "GeckoView:PageAction:Click": {
|
|
const popupUrl = await this.pageActionClick(aData.extensionId);
|
|
aCallback.onSuccess(popupUrl);
|
|
break;
|
|
}
|
|
case "GeckoView:WebExtension:MenuClick": {
|
|
aCallback.onError(`Not implemented`);
|
|
break;
|
|
}
|
|
case "GeckoView:WebExtension:MenuShow": {
|
|
aCallback.onError(`Not implemented`);
|
|
break;
|
|
}
|
|
case "GeckoView:WebExtension:MenuHide": {
|
|
aCallback.onError(`Not implemented`);
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:ActionDelegate:Attached": {
|
|
this.actionDelegateAttached(aData.extensionId);
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:Get": {
|
|
const extension = await this.extensionById(aData.extensionId);
|
|
if (!extension) {
|
|
aCallback.onError(
|
|
`Could not find extension with id: ${aData.extensionId}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
aCallback.onSuccess({
|
|
extension: await exportExtension(extension, /* aSourceURI */ null),
|
|
});
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:SetPBAllowed": {
|
|
const { extensionId, allowed } = aData;
|
|
try {
|
|
const extension = await this.setPrivateBrowsingAllowed(
|
|
extensionId,
|
|
allowed
|
|
);
|
|
aCallback.onSuccess({ extension });
|
|
} catch (ex) {
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:AddOptionalPermissions": {
|
|
const { extensionId, permissions, origins } = aData;
|
|
try {
|
|
const addon = await this.extensionById(extensionId);
|
|
const normalized = lazy.ExtensionPermissions.normalizeOptional(
|
|
{
|
|
permissions,
|
|
origins,
|
|
},
|
|
addon.optionalPermissions
|
|
);
|
|
const policy = WebExtensionPolicy.getByID(addon.id);
|
|
await lazy.ExtensionPermissions.add(
|
|
extensionId,
|
|
normalized,
|
|
policy?.extension
|
|
);
|
|
const extension = await exportExtension(addon, /* aSourceURI */ null);
|
|
aCallback.onSuccess({ extension });
|
|
} catch (ex) {
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:RemoveOptionalPermissions": {
|
|
const { extensionId, permissions, origins } = aData;
|
|
try {
|
|
const addon = await this.extensionById(extensionId);
|
|
const normalized = lazy.ExtensionPermissions.normalizeOptional(
|
|
{ permissions, origins },
|
|
addon.optionalPermissions
|
|
);
|
|
const policy = WebExtensionPolicy.getByID(addon.id);
|
|
await lazy.ExtensionPermissions.remove(
|
|
addon.id,
|
|
normalized,
|
|
policy?.extension
|
|
);
|
|
const extension = await exportExtension(addon, /* aSourceURI */ null);
|
|
aCallback.onSuccess({ extension });
|
|
} catch (ex) {
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:Install": {
|
|
const { locationUri, installId, installMethod } = aData;
|
|
let uri;
|
|
try {
|
|
uri = Services.io.newURI(locationUri);
|
|
} catch (ex) {
|
|
aCallback.onError(`Could not parse uri: ${locationUri}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await this.installWebExtension(
|
|
installId,
|
|
uri,
|
|
installMethod
|
|
);
|
|
if (result.extension) {
|
|
aCallback.onSuccess(result);
|
|
} else {
|
|
aCallback.onError(result);
|
|
}
|
|
} catch (ex) {
|
|
debug`Install exception error ${ex}`;
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:EnsureBuiltIn": {
|
|
const { locationUri, webExtensionId } = aData;
|
|
const uri = this.validateBuiltInLocation(locationUri, aCallback);
|
|
if (!uri) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await this.ensureBuiltIn(uri, webExtensionId);
|
|
if (result.extension) {
|
|
aCallback.onSuccess(result);
|
|
} else {
|
|
aCallback.onError(result);
|
|
}
|
|
} catch (ex) {
|
|
debug`Install exception error ${ex}`;
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:InstallBuiltIn": {
|
|
const uri = this.validateBuiltInLocation(aData.locationUri, aCallback);
|
|
if (!uri) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await this.installBuiltIn(uri);
|
|
if (result.extension) {
|
|
aCallback.onSuccess(result);
|
|
} else {
|
|
aCallback.onError(result);
|
|
}
|
|
} catch (ex) {
|
|
debug`Install exception error ${ex}`;
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:Uninstall": {
|
|
try {
|
|
await this.uninstallWebExtension(aData.webExtensionId);
|
|
aCallback.onSuccess();
|
|
} catch (ex) {
|
|
debug`Failed uninstall ${ex}`;
|
|
aCallback.onError(
|
|
`This extension cannot be uninstalled. Error: ${ex}.`
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:Enable": {
|
|
try {
|
|
const { source, webExtensionId } = aData;
|
|
if (source !== "user" && source !== "app") {
|
|
throw new Error("Illegal source parameter");
|
|
}
|
|
const extension = await this.enableWebExtension(
|
|
webExtensionId,
|
|
source
|
|
);
|
|
aCallback.onSuccess({ extension });
|
|
} catch (ex) {
|
|
debug`Failed enable ${ex}`;
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:Disable": {
|
|
try {
|
|
const { source, webExtensionId } = aData;
|
|
if (source !== "user" && source !== "app") {
|
|
throw new Error("Illegal source parameter");
|
|
}
|
|
const extension = await this.disableWebExtension(
|
|
webExtensionId,
|
|
source
|
|
);
|
|
aCallback.onSuccess({ extension });
|
|
} catch (ex) {
|
|
debug`Failed disable ${ex}`;
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:List": {
|
|
try {
|
|
await lazy.AddonManager.readyPromise;
|
|
const addons = await lazy.AddonManager.getAddonsByTypes([
|
|
"extension",
|
|
]);
|
|
const extensions = await Promise.all(
|
|
addons.map(addon => exportExtension(addon, /* aSourceURI */ null))
|
|
);
|
|
|
|
aCallback.onSuccess({ extensions });
|
|
} catch (ex) {
|
|
debug`Failed list ${ex}`;
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:Update": {
|
|
try {
|
|
const { webExtensionId } = aData;
|
|
const result = await this.updateWebExtension(webExtensionId);
|
|
if (result === null || result.extension) {
|
|
aCallback.onSuccess(result);
|
|
} else {
|
|
aCallback.onError(result);
|
|
}
|
|
} catch (ex) {
|
|
debug`Failed update ${ex}`;
|
|
aCallback.onError(`Unexpected error: ${ex}`);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
// WeakMap[Extension -> BrowserAction]
|
|
GeckoViewWebExtension.browserActions = new WeakMap();
|
|
// WeakMap[Extension -> PageAction]
|
|
GeckoViewWebExtension.pageActions = new WeakMap();
|