зеркало из https://github.com/mozilla/gecko-dev.git
1088 строки
29 KiB
JavaScript
1088 строки
29 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/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"ExtensionActionHelper",
|
|
"GeckoViewConnection",
|
|
"GeckoViewWebExtension",
|
|
"mobileWindowTracker",
|
|
];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
const { GeckoViewUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/GeckoViewUtils.jsm"
|
|
);
|
|
const { EventEmitter } = ChromeUtils.import(
|
|
"resource://gre/modules/EventEmitter.jsm"
|
|
);
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
const PRIVATE_BROWSING_PERMISSION = {
|
|
permissions: ["internal:privateBrowsingAllowed"],
|
|
origins: [],
|
|
};
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
AddonManager: "resource://gre/modules/AddonManager.jsm",
|
|
EventDispatcher: "resource://gre/modules/Messaging.jsm",
|
|
Extension: "resource://gre/modules/Extension.jsm",
|
|
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
|
|
GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.jsm",
|
|
Management: "resource://gre/modules/Extension.jsm",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"mimeService",
|
|
"@mozilla.org/mime;1",
|
|
"nsIMIMEService"
|
|
);
|
|
|
|
const { debug, warn } = GeckoViewUtils.initLogging("Console"); // eslint-disable-line no-unused-vars
|
|
|
|
/** Provides common logic between page and browser actions */
|
|
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 EventDispatcher.instance;
|
|
}
|
|
|
|
const windowId = GeckoViewTabBridge.tabIdToWindowId(aTabId);
|
|
const window = this.windowTracker.getWindow(windowId);
|
|
return window.WindowEventDispatcher;
|
|
}
|
|
|
|
sendRequestForResult(aTabId, aData) {
|
|
return this.eventDispatcherFor(aTabId).sendRequestForResult({
|
|
...aData,
|
|
aTabId,
|
|
extensionId: this.extension.id,
|
|
});
|
|
}
|
|
|
|
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;
|
|
EventDispatcher.instance.registerListener(this, [
|
|
"GeckoView:WebExtension:PortMessageFromApp",
|
|
"GeckoView:WebExtension:PortDisconnect",
|
|
]);
|
|
}
|
|
close() {
|
|
EventDispatcher.instance.unregisterListener(this, [
|
|
"GeckoView:WebExtension:PortMessageFromApp",
|
|
"GeckoView:WebExtension:PortDisconnect",
|
|
]);
|
|
}
|
|
onEvent(aEvent, aData, aCallback) {
|
|
debug`onEvent ${aEvent} ${aData}`;
|
|
|
|
if (this.id !== aData.portId) {
|
|
return;
|
|
}
|
|
|
|
switch (aEvent) {
|
|
case "GeckoView:WebExtension:PortMessageFromApp": {
|
|
const holder = new StructuredCloneHolder(aData.message);
|
|
this.messenger.sendPortMessage(this.id, holder);
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:WebExtension:PortDisconnect": {
|
|
this.messenger.sendPortDisconnect(this.id);
|
|
this.close();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class GeckoViewConnection {
|
|
constructor(sender, nativeApp, allowContentMessaging) {
|
|
this.sender = sender;
|
|
this.nativeApp = nativeApp;
|
|
if (allowContentMessaging) {
|
|
this.allowContentMessaging = allowContentMessaging;
|
|
} else {
|
|
const scope = GeckoViewWebExtension.extensionScopes.get(
|
|
sender.extensionId
|
|
);
|
|
if (scope) {
|
|
this.allowContentMessaging = scope.allowContentMessaging;
|
|
} else {
|
|
this.allowContentMessaging = false;
|
|
}
|
|
}
|
|
|
|
if (!this.allowContentMessaging && !sender.verified) {
|
|
throw new Error(`Unexpected messaging sender: ${JSON.stringify(sender)}`);
|
|
}
|
|
}
|
|
|
|
get dispatcher() {
|
|
const target = this.sender.actor.browsingContext.top.embedderElement;
|
|
|
|
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(
|
|
target.ownerGlobal
|
|
);
|
|
if (dispatcher) {
|
|
return dispatcher;
|
|
}
|
|
|
|
// No dispatcher means this message is coming from a background script,
|
|
// use the global event handler
|
|
return 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(target.ownerGlobal);
|
|
}
|
|
|
|
throw new Error(`Uknown sender envType: ${this.sender.envType}`);
|
|
}
|
|
|
|
_sendMessage({ type, portId, data }) {
|
|
const message = {
|
|
type,
|
|
sender: this.sender,
|
|
data,
|
|
portId,
|
|
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);
|
|
|
|
port.onPortMessage = holder =>
|
|
this._sendMessage({
|
|
type: "GeckoView:WebExtension:PortMessage",
|
|
portId: port.id,
|
|
data: holder.deserialize({}),
|
|
});
|
|
|
|
port.onPortDisconnect = () => {
|
|
EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:Disconnect",
|
|
sender: this.sender,
|
|
portId: port.id,
|
|
});
|
|
port.close();
|
|
};
|
|
|
|
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 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;
|
|
}
|
|
|
|
async function exportExtension(aAddon, aPermissions, 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 {
|
|
creator,
|
|
description,
|
|
homepageURL,
|
|
signedState,
|
|
name,
|
|
icons,
|
|
version,
|
|
optionsURL,
|
|
optionsType,
|
|
isRecommended,
|
|
blocklistState,
|
|
userDisabled,
|
|
embedderDisabled,
|
|
isActive,
|
|
isBuiltin,
|
|
id,
|
|
} = aAddon;
|
|
let creatorName = null;
|
|
let creatorURL = null;
|
|
if (creator) {
|
|
const { name, url } = creator;
|
|
creatorName = name;
|
|
creatorURL = url;
|
|
}
|
|
const openOptionsPageInTab = optionsType === AddonManager.OPTIONS_TYPE_TAB;
|
|
const disabledFlags = [];
|
|
if (userDisabled) {
|
|
disabledFlags.push("userDisabled");
|
|
}
|
|
if (blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
|
|
disabledFlags.push("blocklistDisabled");
|
|
}
|
|
if (embedderDisabled) {
|
|
disabledFlags.push("appDisabled");
|
|
}
|
|
const baseURL = policy ? policy.getURL() : "";
|
|
const privateBrowsingAllowed = policy ? policy.privateBrowsingAllowed : false;
|
|
const promptPermissions = aPermissions
|
|
? await filterPromptPermissions(aPermissions.permissions)
|
|
: [];
|
|
return {
|
|
webExtensionId: id,
|
|
locationURI: aSourceURI != null ? aSourceURI.spec : "",
|
|
isBuiltIn: isBuiltin,
|
|
webExtensionFlags: exportFlags(policy),
|
|
metaData: {
|
|
origins: aPermissions ? aPermissions.origins : [],
|
|
promptPermissions,
|
|
description,
|
|
enabled: isActive,
|
|
disabledFlags,
|
|
version,
|
|
creatorName,
|
|
creatorURL,
|
|
homepageURL,
|
|
name,
|
|
optionsPageURL: optionsURL,
|
|
openOptionsPageInTab,
|
|
isRecommended,
|
|
blocklistState,
|
|
signedState,
|
|
icons,
|
|
baseURL,
|
|
privateBrowsingAllowed,
|
|
},
|
|
};
|
|
}
|
|
|
|
class ExtensionInstallListener {
|
|
constructor(aResolve, aInstall, aInstallId) {
|
|
this.install = aInstall;
|
|
this.installId = aInstallId;
|
|
this.resolve = result => {
|
|
aResolve(result);
|
|
EventDispatcher.instance.unregisterListener(this, [
|
|
"GeckoView:WebExtension:CancelInstall",
|
|
]);
|
|
};
|
|
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 (_) {
|
|
// install may have already failed or been cancelled
|
|
}
|
|
aCallback.onSuccess({ cancelled });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
onDownloadCancelled(aInstall) {
|
|
// 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) {
|
|
const { error: installError, state } = aInstall;
|
|
this.resolve({ installError, state });
|
|
}
|
|
|
|
onDownloadEnded() {
|
|
// Nothing to do
|
|
}
|
|
|
|
onInstallCancelled(aInstall) {
|
|
// 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 });
|
|
}
|
|
}
|
|
|
|
onInstallFailed(aInstall) {
|
|
const { error: installError, state } = aInstall;
|
|
this.resolve({ installError, state });
|
|
}
|
|
|
|
onInstallPostponed(aInstall) {
|
|
const { error: installError, state } = aInstall;
|
|
this.resolve({ installError, state });
|
|
}
|
|
|
|
async onInstallEnded(aInstall, aAddon) {
|
|
const addonId = aAddon.id;
|
|
const { sourceURI } = aInstall;
|
|
|
|
if (aAddon.userDisabled || aAddon.embedderDisabled) {
|
|
const extension = await exportExtension(
|
|
aAddon,
|
|
aAddon.userPermissions,
|
|
sourceURI
|
|
);
|
|
this.resolve({ extension });
|
|
return; // we don't want to wait until extension is enabled, so return early.
|
|
}
|
|
|
|
const onReady = async (name, { id }) => {
|
|
if (id != addonId) {
|
|
return;
|
|
}
|
|
Management.off("ready", onReady);
|
|
const extension = await exportExtension(
|
|
aAddon,
|
|
aAddon.userPermissions,
|
|
sourceURI
|
|
);
|
|
this.resolve({ extension });
|
|
};
|
|
Management.on("ready", onReady);
|
|
}
|
|
}
|
|
|
|
class ExtensionPromptObserver {
|
|
constructor() {
|
|
Services.obs.addObserver(this, "webextension-permission-prompt");
|
|
}
|
|
|
|
async permissionPrompt(aInstall, aAddon, aInfo) {
|
|
const { sourceURI } = aInstall;
|
|
const { permissions } = aInfo;
|
|
const extension = await exportExtension(aAddon, permissions, sourceURI);
|
|
const response = await EventDispatcher.instance.sendRequestForResult({
|
|
type: "GeckoView:WebExtension:InstallPrompt",
|
|
extension,
|
|
});
|
|
|
|
if (response.allow) {
|
|
aInfo.resolve();
|
|
} else {
|
|
aInfo.reject();
|
|
}
|
|
}
|
|
|
|
observe(aSubject, aTopic, aData) {
|
|
debug`observe ${aTopic}`;
|
|
|
|
switch (aTopic) {
|
|
case "webextension-permission-prompt": {
|
|
const { info } = aSubject.wrappedJSObject;
|
|
const { addon, install } = info;
|
|
this.permissionPrompt(install, addon, info);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
new ExtensionPromptObserver();
|
|
|
|
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, windowUtils } = aWindow;
|
|
tab.active = aActive;
|
|
|
|
if (aActive) {
|
|
this._topWindow = Cu.getWeakReference(aWindow);
|
|
const isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
|
|
if (!isPrivate) {
|
|
this._topNonPBWindow = this._topWindow;
|
|
}
|
|
this.emit("tab-activated", {
|
|
windowId: windowUtils.outerWindowID,
|
|
tabId: tab.id,
|
|
isPrivate,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = 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,
|
|
oldPerms
|
|
);
|
|
const updatedExtension = await exportExtension(aInfo.addon, newPerms);
|
|
const response = await EventDispatcher.instance.sendRequestForResult({
|
|
type: "GeckoView:WebExtension:UpdatePrompt",
|
|
currentlyInstalled,
|
|
updatedExtension,
|
|
newPermissions,
|
|
newOrigins,
|
|
});
|
|
|
|
if (!response.allow) {
|
|
throw new Error("Extension update rejected.");
|
|
}
|
|
}
|
|
|
|
var GeckoViewWebExtension = {
|
|
observe(aSubject, aTopic, aData) {
|
|
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
|
|
EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:DebuggerListUpdated",
|
|
});
|
|
break;
|
|
}
|
|
|
|
case "devtools-installed-addon": {
|
|
EventDispatcher.instance.sendRequest({
|
|
type: "GeckoView:WebExtension:DebuggerListUpdated",
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
async registerWebExtension(aId, aUri, allowContentMessaging, aCallback) {
|
|
const params = {
|
|
id: aId,
|
|
resourceURI: aUri,
|
|
temporarilyInstalled: true,
|
|
builtIn: true,
|
|
};
|
|
|
|
let file;
|
|
if (aUri instanceof Ci.nsIFileURL) {
|
|
file = aUri.file;
|
|
}
|
|
|
|
const scope = Extension.getBootstrapScope(aId, file);
|
|
scope.allowContentMessaging = allowContentMessaging;
|
|
// Always allow built-in extensions to run in private browsing
|
|
ExtensionPermissions.add(aId, PRIVATE_BROWSING_PERMISSION);
|
|
this.extensionScopes.set(aId, scope);
|
|
|
|
await scope.startup(params, undefined);
|
|
|
|
scope.extension.callOnClose({
|
|
close: () => this.extensionScopes.delete(aId),
|
|
});
|
|
},
|
|
|
|
async unregisterWebExtension(aId, aCallback) {
|
|
try {
|
|
const scope = this.extensionScopes.get(aId);
|
|
await scope.shutdown();
|
|
this.extensionScopes.delete(aId);
|
|
aCallback.onSuccess();
|
|
} catch (ex) {
|
|
aCallback.onError(`Error unregistering WebExtension ${aId}. ${ex}`);
|
|
}
|
|
},
|
|
|
|
async extensionById(aId) {
|
|
const scope = this.extensionScopes.get(aId);
|
|
if (!scope) {
|
|
// Check if this is an installed extension
|
|
const addon = await AddonManager.getAddonByID(aId);
|
|
if (!addon) {
|
|
debug`Could not find extension with id=${aId}`;
|
|
return null;
|
|
}
|
|
return addon;
|
|
}
|
|
|
|
return scope.extension;
|
|
},
|
|
|
|
async installBuiltIn(aUri) {
|
|
const addon = await AddonManager.installBuiltinAddon(aUri.spec);
|
|
const exported = await exportExtension(addon, addon.userPermissions, aUri);
|
|
return { extension: exported };
|
|
},
|
|
|
|
async installWebExtension(aInstallId, aUri) {
|
|
const install = await AddonManager.getInstallForURL(aUri.spec, {
|
|
telemetryInfo: {
|
|
source: "geckoview-app",
|
|
},
|
|
});
|
|
const promise = new Promise(resolve => {
|
|
install.addListener(
|
|
new ExtensionInstallListener(resolve, install, aInstallId)
|
|
);
|
|
});
|
|
|
|
const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
|
const mimeType = mimeService.getTypeFromURI(aUri);
|
|
AddonManager.installAddonFromWebpage(
|
|
mimeType,
|
|
null,
|
|
systemPrincipal,
|
|
install
|
|
);
|
|
|
|
return promise;
|
|
},
|
|
|
|
async setPrivateBrowsingAllowed(aId, aAllowed) {
|
|
if (aAllowed) {
|
|
await ExtensionPermissions.add(aId, PRIVATE_BROWSING_PERMISSION);
|
|
} else {
|
|
await ExtensionPermissions.remove(aId, PRIVATE_BROWSING_PERMISSION);
|
|
}
|
|
|
|
// 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, addon.userPermissions, /* 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;
|
|
}
|
|
|
|
const browserAction = this.browserActions.get(policy.extension);
|
|
if (!browserAction) {
|
|
return;
|
|
}
|
|
|
|
browserAction.click();
|
|
},
|
|
|
|
async pageActionClick(aId) {
|
|
const policy = WebExtensionPolicy.getByID(aId);
|
|
if (!policy) {
|
|
return;
|
|
}
|
|
|
|
const pageAction = this.pageActions.get(policy.extension);
|
|
if (!pageAction) {
|
|
return;
|
|
}
|
|
|
|
pageAction.click();
|
|
},
|
|
|
|
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,
|
|
extension.userPermissions,
|
|
/* 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,
|
|
extension.userPermissions,
|
|
/* 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, AddonManager.UPDATE_WHEN_USER_REQUESTED);
|
|
});
|
|
},
|
|
|
|
async updateWebExtension(aId) {
|
|
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;
|
|
},
|
|
|
|
/* eslint-disable complexity */
|
|
async onEvent(aEvent, aData, aCallback) {
|
|
debug`onEvent ${aEvent} ${aData}`;
|
|
|
|
switch (aEvent) {
|
|
case "GeckoView:BrowserAction:Click": {
|
|
this.browserActionClick(aData.extensionId);
|
|
break;
|
|
}
|
|
case "GeckoView:PageAction:Click": {
|
|
this.pageActionClick(aData.extensionId);
|
|
break;
|
|
}
|
|
// TODO: Remove deprecated Bug 1634504
|
|
case "GeckoView:RegisterWebExtension": {
|
|
let uri;
|
|
try {
|
|
uri = Services.io.newURI(aData.locationUri);
|
|
} catch (ex) {
|
|
aCallback.onError(`Could not parse URI: ${aData.locationUri}`);
|
|
return;
|
|
}
|
|
if (
|
|
!uri ||
|
|
(!(uri instanceof Ci.nsIFileURL) && !(uri instanceof Ci.nsIJARURI))
|
|
) {
|
|
aCallback.onError(
|
|
`Extension does not point to a resource URI or a file URL. extension=${aData.locationUri}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (uri.fileName != "" && uri.fileExtension != "xpi") {
|
|
aCallback.onError(
|
|
`Extension does not point to a folder or an .xpi file. Hint: the path needs to end with a "/" to be considered a folder. extension=${aData.locationUri}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this.extensionScopes.has(aData.id)) {
|
|
aCallback.onError(
|
|
`An extension with id='${aData.id}' has already been registered.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.registerWebExtension(
|
|
aData.id,
|
|
uri,
|
|
aData.allowContentMessaging
|
|
).then(aCallback.onSuccess, error =>
|
|
aCallback.onError(
|
|
`An error occurred while registering the WebExtension ${aData.locationUri}: ${error}.`
|
|
)
|
|
);
|
|
break;
|
|
}
|
|
|
|
case "GeckoView:ActionDelegate:Attached": {
|
|
this.actionDelegateAttached(aData.extensionId);
|
|
break;
|
|
}
|
|
|
|
// TODO: Remove deprecated Bug 1634504
|
|
case "GeckoView:UnregisterWebExtension": {
|
|
if (!this.extensionScopes.has(aData.id)) {
|
|
aCallback.onError(
|
|
`Could not find an extension with id='${aData.id}'.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.unregisterWebExtension(aData.id, aCallback);
|
|
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,
|
|
extension.userPermissions,
|
|
/* 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:Install": {
|
|
const { locationUri, installId } = 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);
|
|
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 { locationUri } = aData;
|
|
let uri;
|
|
try {
|
|
uri = Services.io.newURI(locationUri);
|
|
} catch (ex) {
|
|
aCallback.onError(
|
|
`Could not parse uri: ${locationUri}. Error: ${ex}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (uri.scheme !== "resource" || uri.host !== "android") {
|
|
aCallback.onError(`Only resource://android/... URIs are allowed.`);
|
|
return;
|
|
}
|
|
|
|
if (uri.fileName !== "") {
|
|
aCallback.onError(
|
|
`This URI does not point to a folder. Note: folders URIs must end with a "/".`
|
|
);
|
|
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 {
|
|
const addons = await AddonManager.getAddonsByTypes(["extension"]);
|
|
const extensions = await Promise.all(
|
|
addons.map(addon =>
|
|
exportExtension(addon, addon.userPermissions, 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;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
// TODO: Remove deprecated Bug 1634504
|
|
GeckoViewWebExtension.extensionScopes = new Map();
|
|
// WeakMap[Extension -> BrowserAction]
|
|
GeckoViewWebExtension.browserActions = new WeakMap();
|
|
// WeakMap[Extension -> PageAction]
|
|
GeckoViewWebExtension.pageActions = new WeakMap();
|
|
Services.obs.addObserver(GeckoViewWebExtension, "devtools-installed-addon");
|
|
Services.obs.addObserver(GeckoViewWebExtension, "testing-installed-addon");
|
|
Services.obs.addObserver(GeckoViewWebExtension, "testing-uninstalled-addon");
|