forked from mirrors/gecko-dev
`context.isBackgroundContext` is used to determine whether the background context participates in the event page suspension mechanism. This should exclude child frames, despite them being tagged as background contexts as well. To determine whether a context is the top context, the browsingContext is forwarded from recvCreateProxyContext to the ExtensionPageContextParent constructor. To minimize the extent of changes, browsingContext is not stored in the class instance. Only the following are stored, derived from browsingContext: - xulBrowser (pre-existing) - isTopContext This patch also clarifies the expected types that are involved with comments and assertions: The availability of browsingContext was previously not obvious in the code due to the presence of `?` in `actor.browsingContext?.`, introduced in part 1.6 of bug 1688040. Differential Revision: https://phabricator.services.mozilla.com/D183755
2314 lines
69 KiB
JavaScript
2314 lines
69 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/. */
|
|
|
|
/**
|
|
* This module contains code for managing APIs that need to run in the
|
|
* parent process, and handles the parent side of operations that need
|
|
* to be proxied from ExtensionChild.jsm.
|
|
*/
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
|
|
BroadcastConduit: "resource://gre/modules/ConduitsParent.sys.mjs",
|
|
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
|
|
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
|
|
ExtensionActivityLog: "resource://gre/modules/ExtensionActivityLog.sys.mjs",
|
|
ExtensionData: "resource://gre/modules/Extension.sys.mjs",
|
|
GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
|
|
MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.sys.mjs",
|
|
NativeApp: "resource://gre/modules/NativeMessaging.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
Schemas: "resource://gre/modules/Schemas.sys.mjs",
|
|
getErrorNameForTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetters(lazy, {
|
|
aomStartup: [
|
|
"@mozilla.org/addons/addon-manager-startup;1",
|
|
"amIAddonManagerStartup",
|
|
],
|
|
});
|
|
|
|
import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
|
|
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
|
|
|
|
const DUMMY_PAGE_URI = Services.io.newURI(
|
|
"chrome://extensions/content/dummy.xhtml"
|
|
);
|
|
|
|
var { BaseContext, CanOfAPIs, SchemaAPIManager, SpreadArgs, defineLazyGetter } =
|
|
ExtensionCommon;
|
|
|
|
var {
|
|
DefaultMap,
|
|
DefaultWeakMap,
|
|
ExtensionError,
|
|
promiseDocumentLoaded,
|
|
promiseEvent,
|
|
promiseObserved,
|
|
} = ExtensionUtils;
|
|
|
|
const ERROR_NO_RECEIVERS =
|
|
"Could not establish connection. Receiving end does not exist.";
|
|
|
|
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
|
|
const CATEGORY_EXTENSION_MODULES = "webextension-modules";
|
|
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
|
|
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
|
|
|
|
let schemaURLs = new Set();
|
|
|
|
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
|
|
|
|
let GlobalManager;
|
|
let ParentAPIManager;
|
|
let StartupCache;
|
|
|
|
function verifyActorForContext(actor, context) {
|
|
if (JSWindowActorParent.isInstance(actor)) {
|
|
let target = actor.browsingContext.top.embedderElement;
|
|
if (context.parentMessageManager !== target.messageManager) {
|
|
throw new Error("Got message on unexpected message manager");
|
|
}
|
|
} else if (JSProcessActorParent.isInstance(actor)) {
|
|
if (actor.manager.remoteType !== context.extension.remoteType) {
|
|
throw new Error("Got message from unexpected process");
|
|
}
|
|
}
|
|
}
|
|
|
|
// This object loads the ext-*.js scripts that define the extension API.
|
|
let apiManager = new (class extends SchemaAPIManager {
|
|
constructor() {
|
|
super("main", lazy.Schemas);
|
|
this.initialized = null;
|
|
|
|
/* eslint-disable mozilla/balanced-listeners */
|
|
this.on("startup", (e, extension) => {
|
|
return extension.apiManager.onStartup(extension);
|
|
});
|
|
|
|
this.on("update", async (e, { id, resourceURI, isPrivileged }) => {
|
|
let modules = this.eventModules.get("update");
|
|
if (modules.size == 0) {
|
|
return;
|
|
}
|
|
|
|
let extension = new lazy.ExtensionData(resourceURI, isPrivileged);
|
|
await extension.loadManifest();
|
|
|
|
return Promise.all(
|
|
Array.from(modules).map(async apiName => {
|
|
let module = await this.asyncLoadModule(apiName);
|
|
module.onUpdate(id, extension.manifest);
|
|
})
|
|
);
|
|
});
|
|
|
|
this.on("uninstall", (e, { id }) => {
|
|
let modules = this.eventModules.get("uninstall");
|
|
return Promise.all(
|
|
Array.from(modules).map(async apiName => {
|
|
let module = await this.asyncLoadModule(apiName);
|
|
return module.onUninstall(id);
|
|
})
|
|
);
|
|
});
|
|
/* eslint-enable mozilla/balanced-listeners */
|
|
|
|
// Handle any changes that happened during startup
|
|
let disabledIds = lazy.AddonManager.getStartupChanges(
|
|
lazy.AddonManager.STARTUP_CHANGE_DISABLED
|
|
);
|
|
if (disabledIds.length) {
|
|
this._callHandlers(disabledIds, "disable", "onDisable");
|
|
}
|
|
|
|
let uninstalledIds = lazy.AddonManager.getStartupChanges(
|
|
lazy.AddonManager.STARTUP_CHANGE_UNINSTALLED
|
|
);
|
|
if (uninstalledIds.length) {
|
|
this._callHandlers(uninstalledIds, "uninstall", "onUninstall");
|
|
}
|
|
}
|
|
|
|
getModuleJSONURLs() {
|
|
return Array.from(
|
|
Services.catMan.enumerateCategory(CATEGORY_EXTENSION_MODULES),
|
|
({ value }) => value
|
|
);
|
|
}
|
|
|
|
// Loads all the ext-*.js scripts currently registered.
|
|
lazyInit() {
|
|
if (this.initialized) {
|
|
return this.initialized;
|
|
}
|
|
|
|
let modulesPromise = StartupCache.other.get(["parentModules"], () =>
|
|
this.loadModuleJSON(this.getModuleJSONURLs())
|
|
);
|
|
|
|
let scriptURLs = [];
|
|
for (let { value } of Services.catMan.enumerateCategory(
|
|
CATEGORY_EXTENSION_SCRIPTS
|
|
)) {
|
|
scriptURLs.push(value);
|
|
}
|
|
|
|
let promise = (async () => {
|
|
let scripts = await Promise.all(
|
|
scriptURLs.map(url => ChromeUtils.compileScript(url))
|
|
);
|
|
|
|
this.initModuleData(await modulesPromise);
|
|
|
|
this.initGlobal();
|
|
for (let script of scripts) {
|
|
script.executeInGlobal(this.global);
|
|
}
|
|
|
|
// Load order matters here. The base manifest defines types which are
|
|
// extended by other schemas, so needs to be loaded first.
|
|
return lazy.Schemas.load(BASE_SCHEMA).then(() => {
|
|
let promises = [];
|
|
for (let { value } of Services.catMan.enumerateCategory(
|
|
CATEGORY_EXTENSION_SCHEMAS
|
|
)) {
|
|
promises.push(lazy.Schemas.load(value));
|
|
}
|
|
for (let [url, { content }] of this.schemaURLs) {
|
|
promises.push(lazy.Schemas.load(url, content));
|
|
}
|
|
for (let url of schemaURLs) {
|
|
promises.push(lazy.Schemas.load(url));
|
|
}
|
|
return Promise.all(promises).then(() => {
|
|
lazy.Schemas.updateSharedSchemas();
|
|
});
|
|
});
|
|
})();
|
|
|
|
Services.mm.addMessageListener("Extension:GetFrameData", this);
|
|
|
|
this.initialized = promise;
|
|
return this.initialized;
|
|
}
|
|
|
|
receiveMessage({ target }) {
|
|
let data = GlobalManager.frameData.get(target) || {};
|
|
Object.assign(data, this.global.tabTracker.getBrowserData(target));
|
|
return data;
|
|
}
|
|
|
|
// Call static handlers for the given event on the given extension ids,
|
|
// and set up a shutdown blocker to ensure they all complete.
|
|
_callHandlers(ids, event, method) {
|
|
let promises = Array.from(this.eventModules.get(event))
|
|
.map(async modName => {
|
|
let module = await this.asyncLoadModule(modName);
|
|
return ids.map(id => module[method](id));
|
|
})
|
|
.flat();
|
|
if (event === "disable") {
|
|
promises.push(...ids.map(id => this.emit("disable", id)));
|
|
}
|
|
if (event === "enabling") {
|
|
promises.push(...ids.map(id => this.emit("enabling", id)));
|
|
}
|
|
|
|
lazy.AsyncShutdown.profileBeforeChange.addBlocker(
|
|
`Extension API ${event} handlers for ${ids.join(",")}`,
|
|
Promise.all(promises)
|
|
);
|
|
}
|
|
})();
|
|
|
|
// Receives messages related to the extension messaging API and forwards them
|
|
// to relevant child messengers. Also handles Native messaging and GeckoView.
|
|
const ProxyMessenger = {
|
|
/**
|
|
* @typedef {object} ParentPort
|
|
* @property {function(StructuredCloneHolder)} onPortMessage
|
|
* @property {function()} onPortDisconnect
|
|
*/
|
|
|
|
/** @type {Map<number, ParentPort>} */
|
|
ports: new Map(),
|
|
|
|
init() {
|
|
this.conduit = new lazy.BroadcastConduit(ProxyMessenger, {
|
|
id: "ProxyMessenger",
|
|
reportOnClosed: "portId",
|
|
recv: ["PortConnect", "PortMessage", "NativeMessage", "RuntimeMessage"],
|
|
cast: ["PortConnect", "PortMessage", "PortDisconnect", "RuntimeMessage"],
|
|
});
|
|
},
|
|
|
|
openNative(nativeApp, sender) {
|
|
let context = ParentAPIManager.getContextById(sender.childId);
|
|
if (context.extension.hasPermission("geckoViewAddons")) {
|
|
return new lazy.GeckoViewConnection(
|
|
this.getSender(context.extension, sender),
|
|
sender.actor.browsingContext.top.embedderElement,
|
|
nativeApp,
|
|
context.extension.hasPermission("nativeMessagingFromContent")
|
|
);
|
|
} else if (sender.verified) {
|
|
return new lazy.NativeApp(context, nativeApp);
|
|
}
|
|
sender = this.getSender(context.extension, sender);
|
|
throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
|
|
},
|
|
|
|
recvNativeMessage({ nativeApp, holder }, { sender }) {
|
|
const app = this.openNative(nativeApp, sender);
|
|
|
|
// Track in-flight NativeApp sendMessage requests as
|
|
// a NativeApp port destroyed when the request
|
|
// has been handled.
|
|
const promiseSendMessage = app.sendMessage(holder);
|
|
const sendMessagePort = {
|
|
native: true,
|
|
senderChildId: sender.childId,
|
|
};
|
|
this.trackNativeAppPort(sendMessagePort);
|
|
const untrackSendMessage = () => this.untrackNativeAppPort(sendMessagePort);
|
|
promiseSendMessage.then(untrackSendMessage, untrackSendMessage);
|
|
|
|
return promiseSendMessage;
|
|
},
|
|
|
|
getSender(extension, source) {
|
|
let sender = {
|
|
contextId: source.id,
|
|
id: source.extensionId,
|
|
envType: source.envType,
|
|
url: source.url,
|
|
};
|
|
|
|
if (JSWindowActorParent.isInstance(source.actor)) {
|
|
let browser = source.actor.browsingContext.top.embedderElement;
|
|
let data =
|
|
browser && apiManager.global.tabTracker.getBrowserData(browser);
|
|
if (data?.tabId > 0) {
|
|
sender.tab = extension.tabManager.get(data.tabId, null)?.convert();
|
|
// frameId is documented to only be set if sender.tab is set.
|
|
sender.frameId = source.frameId;
|
|
}
|
|
}
|
|
|
|
return sender;
|
|
},
|
|
|
|
getTopBrowsingContextId(tabId) {
|
|
// If a tab alredy has content scripts, no need to check private browsing.
|
|
let tab = apiManager.global.tabTracker.getTab(tabId, null);
|
|
if (!tab || (tab.browser || tab).getAttribute("pending") === "true") {
|
|
// No receivers in discarded tabs, so bail early to keep the browser lazy.
|
|
throw new ExtensionError(ERROR_NO_RECEIVERS);
|
|
}
|
|
let browser = tab.linkedBrowser || tab.browser;
|
|
return browser.browsingContext.id;
|
|
},
|
|
|
|
// TODO: Rework/simplify this and getSender/getTopBC after bug 1580766.
|
|
async normalizeArgs(arg, sender) {
|
|
arg.extensionId = arg.extensionId || sender.extensionId;
|
|
let extension = GlobalManager.extensionMap.get(arg.extensionId);
|
|
if (!extension) {
|
|
return Promise.reject({ message: ERROR_NO_RECEIVERS });
|
|
}
|
|
await extension.wakeupBackground?.();
|
|
|
|
arg.sender = this.getSender(extension, sender);
|
|
arg.topBC = arg.tabId && this.getTopBrowsingContextId(arg.tabId);
|
|
return arg.tabId ? "tab" : "messenger";
|
|
},
|
|
|
|
async recvRuntimeMessage(arg, { sender }) {
|
|
arg.firstResponse = true;
|
|
let kind = await this.normalizeArgs(arg, sender);
|
|
let result = await this.conduit.castRuntimeMessage(kind, arg);
|
|
if (!result) {
|
|
// "throw new ExtensionError" cannot be used because then the stack of the
|
|
// sendMessage call would not be added to the error object generated by
|
|
// context.normalizeError. Test coverage by test_ext_error_location.js.
|
|
return Promise.reject({ message: ERROR_NO_RECEIVERS });
|
|
}
|
|
return result.value;
|
|
},
|
|
|
|
async recvPortConnect(arg, { sender }) {
|
|
if (arg.native) {
|
|
let port = this.openNative(arg.name, sender).onConnect(arg.portId, this);
|
|
port.senderChildId = sender.childId;
|
|
port.native = true;
|
|
this.ports.set(arg.portId, port);
|
|
this.trackNativeAppPort(port);
|
|
return;
|
|
}
|
|
|
|
// PortMessages that follow will need to wait for the port to be opened.
|
|
let resolvePort;
|
|
this.ports.set(arg.portId, new Promise(res => (resolvePort = res)));
|
|
|
|
let kind = await this.normalizeArgs(arg, sender);
|
|
let all = await this.conduit.castPortConnect(kind, arg);
|
|
resolvePort();
|
|
|
|
// If there are no active onConnect listeners.
|
|
if (!all.some(x => x.value)) {
|
|
throw new ExtensionError(ERROR_NO_RECEIVERS);
|
|
}
|
|
},
|
|
|
|
async recvPortMessage({ holder }, { sender }) {
|
|
if (sender.native) {
|
|
// If the nativeApp port connect fails (e.g. if triggered by a content
|
|
// script), the portId may not be in the map (because it did throw in
|
|
// the openNative method).
|
|
return this.ports.get(sender.portId)?.onPortMessage(holder);
|
|
}
|
|
// NOTE: the following await make sure we await for promised ports
|
|
// (ports that were not yet open when added to the Map,
|
|
// see recvPortConnect).
|
|
await this.ports.get(sender.portId);
|
|
this.sendPortMessage(sender.portId, holder, !sender.source);
|
|
},
|
|
|
|
recvConduitClosed(sender) {
|
|
let app = this.ports.get(sender.portId);
|
|
if (this.ports.delete(sender.portId) && sender.native) {
|
|
this.untrackNativeAppPort(app);
|
|
return app.onPortDisconnect();
|
|
}
|
|
this.sendPortDisconnect(sender.portId, null, !sender.source);
|
|
},
|
|
|
|
sendPortMessage(portId, holder, source = true) {
|
|
this.conduit.castPortMessage("port", { portId, source, holder });
|
|
},
|
|
|
|
sendPortDisconnect(portId, error, source = true) {
|
|
let port = this.ports.get(portId);
|
|
this.untrackNativeAppPort(port);
|
|
this.conduit.castPortDisconnect("port", { portId, source, error });
|
|
this.ports.delete(portId);
|
|
},
|
|
|
|
trackNativeAppPort(port) {
|
|
if (!port?.native) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let context = ParentAPIManager.getContextById(port.senderChildId);
|
|
context?.trackNativeAppPort(port);
|
|
} catch {
|
|
// getContextById will throw if the context has been destroyed
|
|
// in the meantime.
|
|
}
|
|
},
|
|
|
|
untrackNativeAppPort(port) {
|
|
if (!port?.native) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let context = ParentAPIManager.getContextById(port.senderChildId);
|
|
context?.untrackNativeAppPort(port);
|
|
} catch {
|
|
// getContextById will throw if the context has been destroyed
|
|
// in the meantime.
|
|
}
|
|
},
|
|
};
|
|
ProxyMessenger.init();
|
|
|
|
// Responsible for loading extension APIs into the right globals.
|
|
GlobalManager = {
|
|
// Map[extension ID -> Extension]. Determines which extension is
|
|
// responsible for content under a particular extension ID.
|
|
extensionMap: new Map(),
|
|
initialized: false,
|
|
|
|
/** @type {WeakMap<Browser, object>} Extension Context init data. */
|
|
frameData: new WeakMap(),
|
|
|
|
init(extension) {
|
|
if (this.extensionMap.size == 0) {
|
|
apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
|
|
this.initialized = true;
|
|
}
|
|
this.extensionMap.set(extension.id, extension);
|
|
},
|
|
|
|
uninit(extension) {
|
|
this.extensionMap.delete(extension.id);
|
|
|
|
if (this.extensionMap.size == 0 && this.initialized) {
|
|
apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
|
|
this.initialized = false;
|
|
}
|
|
},
|
|
|
|
_onExtensionBrowser(type, browser, data = {}) {
|
|
data.viewType = browser.getAttribute("webextension-view-type");
|
|
if (data.viewType) {
|
|
GlobalManager.frameData.set(browser, data);
|
|
}
|
|
},
|
|
|
|
getExtension(extensionId) {
|
|
return this.extensionMap.get(extensionId);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* The proxied parent side of a context in ExtensionChild.jsm, for the
|
|
* parent side of a proxied API.
|
|
*/
|
|
class ProxyContextParent extends BaseContext {
|
|
constructor(envType, extension, params, browsingContext, principal) {
|
|
super(envType, extension);
|
|
|
|
this.childId = params.childId;
|
|
this.uri = Services.io.newURI(params.url);
|
|
|
|
this.incognito = params.incognito;
|
|
|
|
this.listenerPromises = new Set();
|
|
|
|
// browsingContext is null when subclassed by BackgroundWorkerContextParent.
|
|
const xulBrowser = browsingContext?.top.embedderElement;
|
|
// This message manager is used by ParentAPIManager to send messages and to
|
|
// close the ProxyContext if the underlying message manager closes. This
|
|
// message manager object may change when `xulBrowser` swaps docshells, e.g.
|
|
// when a tab is moved to a different window.
|
|
// TODO: Is xulBrowser correct for ContentScriptContextParent? Messages
|
|
// through the xulBrowser won't reach cross-process iframes.
|
|
this.messageManagerProxy =
|
|
xulBrowser && new lazy.MessageManagerProxy(xulBrowser);
|
|
|
|
Object.defineProperty(this, "principal", {
|
|
value: principal,
|
|
enumerable: true,
|
|
configurable: true,
|
|
});
|
|
|
|
this.listenerProxies = new Map();
|
|
|
|
this.pendingEventBrowser = null;
|
|
this.callContextData = null;
|
|
|
|
// Set of active NativeApp ports.
|
|
this.activeNativePorts = new WeakSet();
|
|
|
|
// Set of pending queryRunListener promises.
|
|
this.runListenerPromises = new Set();
|
|
|
|
apiManager.emit("proxy-context-load", this);
|
|
}
|
|
|
|
get isProxyContextParent() {
|
|
return true;
|
|
}
|
|
|
|
trackRunListenerPromise(runListenerPromise) {
|
|
if (
|
|
// The extension was already shutdown.
|
|
!this.extension ||
|
|
// Not a non persistent background script context.
|
|
!this.isBackgroundContext ||
|
|
this.extension.persistentBackground
|
|
) {
|
|
return;
|
|
}
|
|
const clearFromSet = () =>
|
|
this.runListenerPromises.delete(runListenerPromise);
|
|
runListenerPromise.then(clearFromSet, clearFromSet);
|
|
this.runListenerPromises.add(runListenerPromise);
|
|
}
|
|
|
|
clearPendingRunListenerPromises() {
|
|
this.runListenerPromises.clear();
|
|
}
|
|
|
|
get pendingRunListenerPromisesCount() {
|
|
return this.runListenerPromises.size;
|
|
}
|
|
|
|
trackNativeAppPort(port) {
|
|
if (
|
|
// Not a native port.
|
|
!port?.native ||
|
|
// Not a non persistent background script context.
|
|
!this.isBackgroundContext ||
|
|
this.extension?.persistentBackground ||
|
|
// The extension was already shutdown.
|
|
!this.extension
|
|
) {
|
|
return;
|
|
}
|
|
this.activeNativePorts.add(port);
|
|
}
|
|
|
|
untrackNativeAppPort(port) {
|
|
this.activeNativePorts.delete(port);
|
|
}
|
|
|
|
get hasActiveNativeAppPorts() {
|
|
return !!ChromeUtils.nondeterministicGetWeakSetKeys(this.activeNativePorts)
|
|
.length;
|
|
}
|
|
|
|
/**
|
|
* Call the `callable` parameter with `context.callContextData` set to the value passed
|
|
* as the first parameter of this method.
|
|
*
|
|
* `context.callContextData` is expected to:
|
|
* - don't be set when context.withCallContextData is being called
|
|
* - be set back to null right after calling the `callable` function, without
|
|
* awaiting on any async code that the function may be running internally
|
|
*
|
|
* The callable method itself is responsabile of eventually retrieve the value initially set
|
|
* on the `context.callContextData` before any code executed asynchronously (e.g. from a
|
|
* callback or after awaiting internally on a promise if the `callable` function was async).
|
|
*
|
|
* @param {object} callContextData
|
|
* @param {boolean} callContextData.isHandlingUserInput
|
|
* @param {Function} callable
|
|
*
|
|
* @returns {any} Returns the value returned by calling the `callable` method.
|
|
*/
|
|
withCallContextData({ isHandlingUserInput }, callable) {
|
|
if (this.callContextData) {
|
|
Cu.reportError(
|
|
`Unexpected pre-existing callContextData on "${this.extension?.policy.debugName}" contextId ${this.contextId}`
|
|
);
|
|
}
|
|
|
|
try {
|
|
this.callContextData = {
|
|
isHandlingUserInput,
|
|
};
|
|
return callable();
|
|
} finally {
|
|
this.callContextData = null;
|
|
}
|
|
}
|
|
|
|
async withPendingBrowser(browser, callable) {
|
|
let savedBrowser = this.pendingEventBrowser;
|
|
this.pendingEventBrowser = browser;
|
|
try {
|
|
let result = await callable();
|
|
return result;
|
|
} finally {
|
|
this.pendingEventBrowser = savedBrowser;
|
|
}
|
|
}
|
|
|
|
logActivity(type, name, data) {
|
|
// The base class will throw so we catch any subclasses that do not implement.
|
|
// We do not want to throw here, but we also do not log here.
|
|
}
|
|
|
|
get cloneScope() {
|
|
return this.sandbox;
|
|
}
|
|
|
|
applySafe(callback, args) {
|
|
// There's no need to clone when calling listeners for a proxied
|
|
// context.
|
|
return this.applySafeWithoutClone(callback, args);
|
|
}
|
|
|
|
get xulBrowser() {
|
|
return this.messageManagerProxy?.eventTarget;
|
|
}
|
|
|
|
get parentMessageManager() {
|
|
// TODO bug 1595186: Replace use of parentMessageManager.
|
|
return this.messageManagerProxy?.messageManager;
|
|
}
|
|
|
|
shutdown() {
|
|
this.unload();
|
|
}
|
|
|
|
unload() {
|
|
if (this.unloaded) {
|
|
return;
|
|
}
|
|
|
|
this.messageManagerProxy?.dispose();
|
|
|
|
super.unload();
|
|
apiManager.emit("proxy-context-unload", this);
|
|
}
|
|
}
|
|
|
|
defineLazyGetter(ProxyContextParent.prototype, "apiCan", function () {
|
|
let obj = {};
|
|
let can = new CanOfAPIs(this, this.extension.apiManager, obj);
|
|
return can;
|
|
});
|
|
|
|
defineLazyGetter(ProxyContextParent.prototype, "apiObj", function () {
|
|
return this.apiCan.root;
|
|
});
|
|
|
|
defineLazyGetter(ProxyContextParent.prototype, "sandbox", function () {
|
|
// NOTE: the required Blob and URL globals are used in the ext-registerContentScript.js
|
|
// API module to convert JS and CSS data into blob URLs.
|
|
return Cu.Sandbox(this.principal, {
|
|
sandboxName: this.uri.spec,
|
|
wantGlobalProperties: ["Blob", "URL"],
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The parent side of proxied API context for extension content script
|
|
* running in ExtensionContent.jsm.
|
|
*/
|
|
class ContentScriptContextParent extends ProxyContextParent {}
|
|
|
|
/**
|
|
* The parent side of proxied API context for extension page, such as a
|
|
* background script, a tab page, or a popup, running in
|
|
* ExtensionChild.jsm.
|
|
*/
|
|
class ExtensionPageContextParent extends ProxyContextParent {
|
|
constructor(envType, extension, params, browsingContext) {
|
|
super(envType, extension, params, browsingContext, extension.principal);
|
|
|
|
this.viewType = params.viewType;
|
|
this.isTopContext = browsingContext.top === browsingContext;
|
|
|
|
this.extension.views.add(this);
|
|
|
|
extension.emit("extension-proxy-context-load", this);
|
|
}
|
|
|
|
// The window that contains this context. This may change due to moving tabs.
|
|
get appWindow() {
|
|
let win = this.xulBrowser.ownerGlobal;
|
|
return win.browsingContext.topChromeWindow;
|
|
}
|
|
|
|
get currentWindow() {
|
|
if (this.viewType !== "background") {
|
|
return this.appWindow;
|
|
}
|
|
}
|
|
|
|
get tabId() {
|
|
let { tabTracker } = apiManager.global;
|
|
let data = tabTracker.getBrowserData(this.xulBrowser);
|
|
if (data.tabId >= 0) {
|
|
return data.tabId;
|
|
}
|
|
}
|
|
|
|
onBrowserChange(browser) {
|
|
super.onBrowserChange(browser);
|
|
this.xulBrowser = browser;
|
|
}
|
|
|
|
unload() {
|
|
super.unload();
|
|
this.extension.views.delete(this);
|
|
}
|
|
|
|
shutdown() {
|
|
apiManager.emit("page-shutdown", this);
|
|
super.shutdown();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The parent side of proxied API context for devtools extension page, such as a
|
|
* devtools pages and panels running in ExtensionChild.jsm.
|
|
*/
|
|
class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
|
|
constructor(...params) {
|
|
super(...params);
|
|
|
|
// Set all attributes that are lazily defined to `null` here.
|
|
//
|
|
// Note that we can't do that for `this._devToolsToolbox` because it will
|
|
// be defined when calling our parent constructor and so would override it back to `null`.
|
|
this._devToolsCommands = null;
|
|
this._onNavigatedListeners = null;
|
|
|
|
this._onResourceAvailable = this._onResourceAvailable.bind(this);
|
|
}
|
|
|
|
set devToolsToolbox(toolbox) {
|
|
if (this._devToolsToolbox) {
|
|
throw new Error("Cannot set the context DevTools toolbox twice");
|
|
}
|
|
|
|
this._devToolsToolbox = toolbox;
|
|
}
|
|
|
|
get devToolsToolbox() {
|
|
return this._devToolsToolbox;
|
|
}
|
|
|
|
async addOnNavigatedListener(listener) {
|
|
if (!this._onNavigatedListeners) {
|
|
this._onNavigatedListeners = new Set();
|
|
|
|
await this.devToolsToolbox.resourceCommand.watchResources(
|
|
[this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
|
|
{
|
|
onAvailable: this._onResourceAvailable,
|
|
ignoreExistingResources: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
this._onNavigatedListeners.add(listener);
|
|
}
|
|
|
|
removeOnNavigatedListener(listener) {
|
|
if (this._onNavigatedListeners) {
|
|
this._onNavigatedListeners.delete(listener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The returned "commands" object, exposing modules implemented from devtools/shared/commands.
|
|
* Each attribute being a static interface to communicate with the server backend.
|
|
*
|
|
* @returns {Promise<object>}
|
|
*/
|
|
async getDevToolsCommands() {
|
|
// Ensure that we try to instantiate a commands only once,
|
|
// even if createCommandsForTabForWebExtension is async.
|
|
if (this._devToolsCommandsPromise) {
|
|
return this._devToolsCommandsPromise;
|
|
}
|
|
if (this._devToolsCommands) {
|
|
return this._devToolsCommands;
|
|
}
|
|
|
|
this._devToolsCommandsPromise = (async () => {
|
|
const commands =
|
|
await lazy.DevToolsShim.createCommandsForTabForWebExtension(
|
|
this.devToolsToolbox.commands.descriptorFront.localTab
|
|
);
|
|
await commands.targetCommand.startListening();
|
|
this._devToolsCommands = commands;
|
|
this._devToolsCommandsPromise = null;
|
|
return commands;
|
|
})();
|
|
return this._devToolsCommandsPromise;
|
|
}
|
|
|
|
unload() {
|
|
// Bail if the toolbox reference was already cleared.
|
|
if (!this.devToolsToolbox) {
|
|
return;
|
|
}
|
|
|
|
if (this._onNavigatedListeners) {
|
|
this.devToolsToolbox.resourceCommand.unwatchResources(
|
|
[this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
|
|
{ onAvailable: this._onResourceAvailable }
|
|
);
|
|
}
|
|
|
|
if (this._devToolsCommands) {
|
|
this._devToolsCommands.destroy();
|
|
this._devToolsCommands = null;
|
|
}
|
|
|
|
if (this._onNavigatedListeners) {
|
|
this._onNavigatedListeners.clear();
|
|
this._onNavigatedListeners = null;
|
|
}
|
|
|
|
this._devToolsToolbox = null;
|
|
|
|
super.unload();
|
|
}
|
|
|
|
async _onResourceAvailable(resources) {
|
|
for (const resource of resources) {
|
|
const { targetFront } = resource;
|
|
if (targetFront.isTopLevel && resource.name === "dom-complete") {
|
|
for (const listener of this._onNavigatedListeners) {
|
|
listener(targetFront.url);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The parent side of proxied API context for extension background service
|
|
* worker script.
|
|
*/
|
|
class BackgroundWorkerContextParent extends ProxyContextParent {
|
|
constructor(envType, extension, params) {
|
|
// TODO: split out from ProxyContextParent a base class that
|
|
// doesn't expect a browsingContext and one for contexts that are
|
|
// expected to have a browsingContext associated.
|
|
super(envType, extension, params, null, extension.principal);
|
|
|
|
this.viewType = params.viewType;
|
|
this.workerDescriptorId = params.workerDescriptorId;
|
|
|
|
this.extension.views.add(this);
|
|
|
|
extension.emit("extension-proxy-context-load", this);
|
|
}
|
|
}
|
|
|
|
ParentAPIManager = {
|
|
proxyContexts: new Map(),
|
|
|
|
init() {
|
|
// TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
|
|
Services.obs.addObserver(this, "message-manager-close");
|
|
|
|
this.conduit = new lazy.BroadcastConduit(this, {
|
|
id: "ParentAPIManager",
|
|
reportOnClosed: "childId",
|
|
recv: [
|
|
"CreateProxyContext",
|
|
"ContextLoaded",
|
|
"APICall",
|
|
"AddListener",
|
|
"RemoveListener",
|
|
],
|
|
send: ["CallResult"],
|
|
query: ["RunListener", "StreamFilterSuspendCancel"],
|
|
});
|
|
},
|
|
|
|
attachMessageManager(extension, processMessageManager) {
|
|
extension.parentMessageManager = processMessageManager;
|
|
},
|
|
|
|
async observe(subject, topic, data) {
|
|
if (topic === "message-manager-close") {
|
|
let mm = subject;
|
|
for (let [childId, context] of this.proxyContexts) {
|
|
if (context.parentMessageManager === mm) {
|
|
this.closeProxyContext(childId);
|
|
}
|
|
}
|
|
|
|
// Reset extension message managers when their child processes shut down.
|
|
for (let extension of GlobalManager.extensionMap.values()) {
|
|
if (extension.parentMessageManager === mm) {
|
|
extension.parentMessageManager = null;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
shutdownExtension(extensionId, reason) {
|
|
if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
|
|
apiManager._callHandlers([extensionId], "disable", "onDisable");
|
|
}
|
|
|
|
for (let [childId, context] of this.proxyContexts) {
|
|
if (context.extension.id == extensionId) {
|
|
context.shutdown();
|
|
this.proxyContexts.delete(childId);
|
|
}
|
|
}
|
|
},
|
|
|
|
queryStreamFilterSuspendCancel(childId) {
|
|
return this.conduit.queryStreamFilterSuspendCancel(childId);
|
|
},
|
|
|
|
recvCreateProxyContext(data, { actor, sender }) {
|
|
let { envType, extensionId, childId, principal } = data;
|
|
|
|
if (this.proxyContexts.has(childId)) {
|
|
throw new Error(
|
|
"A WebExtension context with the given ID already exists!"
|
|
);
|
|
}
|
|
|
|
let extension = GlobalManager.getExtension(extensionId);
|
|
if (!extension) {
|
|
throw new Error(`No WebExtension found with ID ${extensionId}`);
|
|
}
|
|
|
|
let context;
|
|
if (envType == "addon_parent" || envType == "devtools_parent") {
|
|
if (!sender.verified) {
|
|
throw new Error(`Bad sender context envType: ${sender.envType}`);
|
|
}
|
|
|
|
let isBackgroundWorker = false;
|
|
if (JSWindowActorParent.isInstance(actor)) {
|
|
const target = actor.browsingContext.top.embedderElement;
|
|
let processMessageManager =
|
|
target.messageManager.processMessageManager ||
|
|
Services.ppmm.getChildAt(0);
|
|
|
|
if (!extension.parentMessageManager) {
|
|
if (target.remoteType === extension.remoteType) {
|
|
this.attachMessageManager(extension, processMessageManager);
|
|
}
|
|
}
|
|
|
|
if (processMessageManager !== extension.parentMessageManager) {
|
|
throw new Error(
|
|
"Attempt to create privileged extension parent from incorrect child process"
|
|
);
|
|
}
|
|
} else if (JSProcessActorParent.isInstance(actor)) {
|
|
if (actor.manager.remoteType !== extension.remoteType) {
|
|
throw new Error(
|
|
"Attempt to create privileged extension parent from incorrect child process"
|
|
);
|
|
}
|
|
|
|
if (envType !== "addon_parent") {
|
|
throw new Error(
|
|
`Unexpected envType ${envType} on an extension process actor`
|
|
);
|
|
}
|
|
if (data.viewType !== "background_worker") {
|
|
throw new Error(
|
|
`Unexpected viewType ${data.viewType} on an extension process actor`
|
|
);
|
|
}
|
|
isBackgroundWorker = true;
|
|
} else {
|
|
// Unreacheable: JSWindowActorParent and JSProcessActorParent are the
|
|
// only actors.
|
|
throw new Error(
|
|
"Attempt to create privileged extension parent via incorrect actor"
|
|
);
|
|
}
|
|
|
|
if (isBackgroundWorker) {
|
|
context = new BackgroundWorkerContextParent(envType, extension, data);
|
|
} else if (envType == "addon_parent") {
|
|
context = new ExtensionPageContextParent(
|
|
envType,
|
|
extension,
|
|
data,
|
|
actor.browsingContext
|
|
);
|
|
} else if (envType == "devtools_parent") {
|
|
context = new DevToolsExtensionPageContextParent(
|
|
envType,
|
|
extension,
|
|
data,
|
|
actor.browsingContext
|
|
);
|
|
}
|
|
} else if (envType == "content_parent") {
|
|
// Note: actor is always a JSWindowActorParent, with a browsingContext.
|
|
context = new ContentScriptContextParent(
|
|
envType,
|
|
extension,
|
|
data,
|
|
actor.browsingContext,
|
|
principal
|
|
);
|
|
} else {
|
|
throw new Error(`Invalid WebExtension context envType: ${envType}`);
|
|
}
|
|
this.proxyContexts.set(childId, context);
|
|
},
|
|
|
|
recvContextLoaded(data, { actor, sender }) {
|
|
let context = this.getContextById(data.childId);
|
|
verifyActorForContext(actor, context);
|
|
const { extension } = context;
|
|
extension.emit("extension-proxy-context-load:completed", context);
|
|
},
|
|
|
|
recvConduitClosed(sender) {
|
|
this.closeProxyContext(sender.id);
|
|
},
|
|
|
|
closeProxyContext(childId) {
|
|
let context = this.proxyContexts.get(childId);
|
|
if (context) {
|
|
context.unload();
|
|
this.proxyContexts.delete(childId);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Call the given function and also log the call as appropriate
|
|
* (i.e., with activity logging and/or profiler markers)
|
|
*
|
|
* @param {BaseContext} context The context making this call.
|
|
* @param {object} data Additional data about the call.
|
|
* @param {Function} callable The actual implementation to invoke.
|
|
*/
|
|
async callAndLog(context, data, callable) {
|
|
let { id } = context.extension;
|
|
// If we were called via callParentAsyncFunction we don't want
|
|
// to log again, check for the flag.
|
|
const { alreadyLogged } = data.options || {};
|
|
if (!alreadyLogged) {
|
|
lazy.ExtensionActivityLog.log(
|
|
id,
|
|
context.viewType,
|
|
"api_call",
|
|
data.path,
|
|
{
|
|
args: data.args,
|
|
}
|
|
);
|
|
}
|
|
|
|
let start = Cu.now();
|
|
try {
|
|
return callable();
|
|
} finally {
|
|
ChromeUtils.addProfilerMarker(
|
|
"ExtensionParent",
|
|
{ startTime: start },
|
|
`${id}, api_call: ${data.path}`
|
|
);
|
|
}
|
|
},
|
|
|
|
async recvAPICall(data, { actor }) {
|
|
let context = this.getContextById(data.childId);
|
|
let target = actor.browsingContext?.top.embedderElement;
|
|
|
|
verifyActorForContext(actor, context);
|
|
|
|
let reply = result => {
|
|
if (target && !context.parentMessageManager) {
|
|
Services.console.logStringMessage(
|
|
"Cannot send function call result: other side closed connection " +
|
|
`(call data: ${uneval({ path: data.path, args: data.args })})`
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.conduit.sendCallResult(data.childId, {
|
|
childId: data.childId,
|
|
callId: data.callId,
|
|
path: data.path,
|
|
...result,
|
|
});
|
|
};
|
|
|
|
try {
|
|
let args = data.args;
|
|
let { isHandlingUserInput = false } = data.options || {};
|
|
let pendingBrowser = context.pendingEventBrowser;
|
|
let fun = await context.apiCan.asyncFindAPIPath(data.path);
|
|
let result = this.callAndLog(context, data, () => {
|
|
return context.withPendingBrowser(pendingBrowser, () =>
|
|
context.withCallContextData({ isHandlingUserInput }, () =>
|
|
fun(...args)
|
|
)
|
|
);
|
|
});
|
|
|
|
if (data.callId) {
|
|
result = result || Promise.resolve();
|
|
|
|
result.then(
|
|
result => {
|
|
result = result instanceof SpreadArgs ? [...result] : [result];
|
|
|
|
let holder = new StructuredCloneHolder(
|
|
`ExtensionParent/${context.extension.id}/recvAPICall/${data.path}`,
|
|
null,
|
|
result
|
|
);
|
|
|
|
reply({ result: holder });
|
|
},
|
|
error => {
|
|
error = context.normalizeError(error);
|
|
reply({
|
|
error: { message: error.message, fileName: error.fileName },
|
|
});
|
|
}
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (data.callId) {
|
|
let error = context.normalizeError(e);
|
|
reply({ error: { message: error.message } });
|
|
} else {
|
|
Cu.reportError(e);
|
|
}
|
|
}
|
|
},
|
|
|
|
async recvAddListener(data, { actor }) {
|
|
let context = this.getContextById(data.childId);
|
|
|
|
verifyActorForContext(actor, context);
|
|
|
|
let { childId, alreadyLogged = false } = data;
|
|
let handlingUserInput = false;
|
|
|
|
let listener = async (...listenerArgs) => {
|
|
let startTime = Cu.now();
|
|
// Extract urgentSend flag to avoid deserializing args holder later.
|
|
let urgentSend = false;
|
|
if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
|
|
urgentSend = listenerArgs[0].urgentSend;
|
|
delete listenerArgs[0].urgentSend;
|
|
}
|
|
let runListenerPromise = this.conduit.queryRunListener(childId, {
|
|
childId,
|
|
handlingUserInput,
|
|
listenerId: data.listenerId,
|
|
path: data.path,
|
|
urgentSend,
|
|
get args() {
|
|
return new StructuredCloneHolder(
|
|
`ExtensionParent/${context.extension.id}/recvAddListener/${data.path}`,
|
|
null,
|
|
listenerArgs
|
|
);
|
|
},
|
|
});
|
|
context.trackRunListenerPromise(runListenerPromise);
|
|
|
|
const result = await runListenerPromise;
|
|
let rv = result && result.deserialize(globalThis);
|
|
ChromeUtils.addProfilerMarker(
|
|
"ExtensionParent",
|
|
{ startTime },
|
|
`${context.extension.id}, api_event: ${data.path}`
|
|
);
|
|
lazy.ExtensionActivityLog.log(
|
|
context.extension.id,
|
|
context.viewType,
|
|
"api_event",
|
|
data.path,
|
|
{ args: listenerArgs, result: rv }
|
|
);
|
|
return rv;
|
|
};
|
|
|
|
context.listenerProxies.set(data.listenerId, listener);
|
|
|
|
let args = data.args;
|
|
let promise = context.apiCan.asyncFindAPIPath(data.path);
|
|
|
|
// Store pending listener additions so we can be sure they're all
|
|
// fully initialize before we consider extension startup complete.
|
|
if (context.isBackgroundContext && context.listenerPromises) {
|
|
const { listenerPromises } = context;
|
|
listenerPromises.add(promise);
|
|
let remove = () => {
|
|
listenerPromises.delete(promise);
|
|
};
|
|
promise.then(remove, remove);
|
|
}
|
|
|
|
let handler = await promise;
|
|
if (handler.setUserInput) {
|
|
handlingUserInput = true;
|
|
}
|
|
handler.addListener(listener, ...args);
|
|
if (!alreadyLogged) {
|
|
lazy.ExtensionActivityLog.log(
|
|
context.extension.id,
|
|
context.viewType,
|
|
"api_call",
|
|
`${data.path}.addListener`,
|
|
{ args }
|
|
);
|
|
}
|
|
},
|
|
|
|
async recvRemoveListener(data) {
|
|
let context = this.getContextById(data.childId);
|
|
let listener = context.listenerProxies.get(data.listenerId);
|
|
|
|
let handler = await context.apiCan.asyncFindAPIPath(data.path);
|
|
handler.removeListener(listener);
|
|
|
|
let { alreadyLogged = false } = data;
|
|
if (!alreadyLogged) {
|
|
lazy.ExtensionActivityLog.log(
|
|
context.extension.id,
|
|
context.viewType,
|
|
"api_call",
|
|
`${data.path}.removeListener`,
|
|
{ args: [] }
|
|
);
|
|
}
|
|
},
|
|
|
|
getContextById(childId) {
|
|
let context = this.proxyContexts.get(childId);
|
|
if (!context) {
|
|
throw new Error("WebExtension context not found!");
|
|
}
|
|
return context;
|
|
},
|
|
};
|
|
|
|
ParentAPIManager.init();
|
|
|
|
/**
|
|
* A hidden window which contains the extension pages that are not visible
|
|
* (i.e., background pages and devtools pages), and is also used by
|
|
* ExtensionDebuggingUtils to contain the browser elements used by the
|
|
* addon debugger to connect to the devtools actors running in the same
|
|
* process of the target extension (and be able to stay connected across
|
|
* the addon reloads).
|
|
*/
|
|
class HiddenXULWindow {
|
|
constructor() {
|
|
this._windowlessBrowser = null;
|
|
this.unloaded = false;
|
|
this.waitInitialized = this.initWindowlessBrowser();
|
|
}
|
|
|
|
shutdown() {
|
|
if (this.unloaded) {
|
|
throw new Error(
|
|
"Unable to shutdown an unloaded HiddenXULWindow instance"
|
|
);
|
|
}
|
|
|
|
this.unloaded = true;
|
|
|
|
this.waitInitialized = null;
|
|
|
|
if (!this._windowlessBrowser) {
|
|
Cu.reportError("HiddenXULWindow was shut down while it was loading.");
|
|
// initWindowlessBrowser will close windowlessBrowser when possible.
|
|
return;
|
|
}
|
|
|
|
this._windowlessBrowser.close();
|
|
this._windowlessBrowser = null;
|
|
}
|
|
|
|
get chromeDocument() {
|
|
return this._windowlessBrowser.document;
|
|
}
|
|
|
|
/**
|
|
* Private helper that create a HTMLDocument in a windowless browser.
|
|
*
|
|
* @returns {Promise<void>}
|
|
* A promise which resolves when the windowless browser is ready.
|
|
*/
|
|
async initWindowlessBrowser() {
|
|
if (this.waitInitialized) {
|
|
throw new Error("HiddenXULWindow already initialized");
|
|
}
|
|
|
|
// The invisible page is currently wrapped in a XUL window to fix an issue
|
|
// with using the canvas API from a background page (See Bug 1274775).
|
|
let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
|
|
|
|
// The windowless browser is a thin wrapper around a docShell that keeps
|
|
// its related resources alive. It implements nsIWebNavigation and
|
|
// forwards its methods to the underlying docShell. That .docShell
|
|
// needs `QueryInterface(nsIWebNavigation)` to give us access to the
|
|
// webNav methods that are already available on the windowless browser.
|
|
let chromeShell = windowlessBrowser.docShell;
|
|
chromeShell.QueryInterface(Ci.nsIWebNavigation);
|
|
|
|
if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
|
|
let attrs = chromeShell.getOriginAttributes();
|
|
attrs.privateBrowsingId = 1;
|
|
chromeShell.setOriginAttributes(attrs);
|
|
}
|
|
|
|
windowlessBrowser.browsingContext.useGlobalHistory = false;
|
|
chromeShell.loadURI(DUMMY_PAGE_URI, {
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
});
|
|
|
|
await promiseObserved(
|
|
"chrome-document-global-created",
|
|
win => win.document == chromeShell.document
|
|
);
|
|
await promiseDocumentLoaded(windowlessBrowser.document);
|
|
if (this.unloaded) {
|
|
windowlessBrowser.close();
|
|
return;
|
|
}
|
|
this._windowlessBrowser = windowlessBrowser;
|
|
}
|
|
|
|
/**
|
|
* Creates the browser XUL element that will contain the WebExtension Page.
|
|
*
|
|
* @param {object} xulAttributes
|
|
* An object that contains the xul attributes to set of the newly
|
|
* created browser XUL element.
|
|
*
|
|
* @returns {Promise<XULElement>}
|
|
* A Promise which resolves to the newly created browser XUL element.
|
|
*/
|
|
async createBrowserElement(xulAttributes) {
|
|
if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
|
|
throw new Error("missing mandatory xulAttributes parameter");
|
|
}
|
|
|
|
await this.waitInitialized;
|
|
|
|
const chromeDoc = this.chromeDocument;
|
|
|
|
const browser = chromeDoc.createXULElement("browser");
|
|
browser.setAttribute("type", "content");
|
|
browser.setAttribute("disableglobalhistory", "true");
|
|
browser.setAttribute("messagemanagergroup", "webext-browsers");
|
|
|
|
for (const [name, value] of Object.entries(xulAttributes)) {
|
|
if (value != null) {
|
|
browser.setAttribute(name, value);
|
|
}
|
|
}
|
|
|
|
let awaitFrameLoader = Promise.resolve();
|
|
|
|
if (browser.getAttribute("remote") === "true") {
|
|
awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
|
|
}
|
|
|
|
chromeDoc.documentElement.appendChild(browser);
|
|
|
|
// Forcibly flush layout so that we get a pres shell soon enough, see
|
|
// bug 1274775.
|
|
browser.getBoundingClientRect();
|
|
|
|
await awaitFrameLoader;
|
|
return browser;
|
|
}
|
|
}
|
|
|
|
const SharedWindow = {
|
|
_window: null,
|
|
_count: 0,
|
|
|
|
acquire() {
|
|
if (this._window == null) {
|
|
if (this._count != 0) {
|
|
throw new Error(
|
|
`Shared window already exists with count ${this._count}`
|
|
);
|
|
}
|
|
|
|
this._window = new HiddenXULWindow();
|
|
}
|
|
|
|
this._count++;
|
|
return this._window;
|
|
},
|
|
|
|
release() {
|
|
if (this._count < 1) {
|
|
throw new Error(`Releasing shared window with count ${this._count}`);
|
|
}
|
|
|
|
this._count--;
|
|
if (this._count == 0) {
|
|
this._window.shutdown();
|
|
this._window = null;
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* This is a base class used by the ext-backgroundPage and ext-devtools API implementations
|
|
* to inherits the shared boilerplate code needed to create a parent document for the hidden
|
|
* extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
|
|
* DevToolsPage classes.
|
|
*
|
|
* @param {Extension} extension
|
|
* The Extension which owns the hidden extension page created (used to decide
|
|
* if the hidden extension page parent doc is going to be a windowlessBrowser or
|
|
* a visible XUL window).
|
|
* @param {string} viewType
|
|
* The viewType of the WebExtension page that is going to be loaded
|
|
* in the created browser element (e.g. "background" or "devtools_page").
|
|
*/
|
|
class HiddenExtensionPage {
|
|
constructor(extension, viewType) {
|
|
if (!extension || !viewType) {
|
|
throw new Error("extension and viewType parameters are mandatory");
|
|
}
|
|
|
|
this.extension = extension;
|
|
this.viewType = viewType;
|
|
this.browser = null;
|
|
this.unloaded = false;
|
|
}
|
|
|
|
/**
|
|
* Destroy the created parent document.
|
|
*/
|
|
shutdown() {
|
|
if (this.unloaded) {
|
|
throw new Error(
|
|
"Unable to shutdown an unloaded HiddenExtensionPage instance"
|
|
);
|
|
}
|
|
|
|
this.unloaded = true;
|
|
|
|
if (this.browser) {
|
|
this._releaseBrowser();
|
|
}
|
|
}
|
|
|
|
_releaseBrowser() {
|
|
this.browser.remove();
|
|
this.browser = null;
|
|
SharedWindow.release();
|
|
}
|
|
|
|
/**
|
|
* Creates the browser XUL element that will contain the WebExtension Page.
|
|
*
|
|
* @returns {Promise<XULElement>}
|
|
* A Promise which resolves to the newly created browser XUL element.
|
|
*/
|
|
async createBrowserElement() {
|
|
if (this.browser) {
|
|
throw new Error("createBrowserElement called twice");
|
|
}
|
|
|
|
let window = SharedWindow.acquire();
|
|
try {
|
|
this.browser = await window.createBrowserElement({
|
|
"webextension-view-type": this.viewType,
|
|
remote: this.extension.remote ? "true" : null,
|
|
remoteType: this.extension.remoteType,
|
|
initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
|
|
});
|
|
} catch (e) {
|
|
SharedWindow.release();
|
|
throw e;
|
|
}
|
|
|
|
if (this.unloaded) {
|
|
this._releaseBrowser();
|
|
throw new Error("Extension shut down before browser element was created");
|
|
}
|
|
|
|
return this.browser;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This object provides utility functions needed by the devtools actors to
|
|
* be able to connect and debug an extension (which can run in the main or in
|
|
* a child extension process).
|
|
*/
|
|
const DebugUtils = {
|
|
// A lazily created hidden XUL window, which contains the browser elements
|
|
// which are used to connect the webextension patent actor to the extension process.
|
|
hiddenXULWindow: null,
|
|
|
|
// Map<extensionId, Promise<XULElement>>
|
|
debugBrowserPromises: new Map(),
|
|
// DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
|
|
debugActors: new DefaultWeakMap(() => new Set()),
|
|
|
|
_extensionUpdatedWatcher: null,
|
|
watchExtensionUpdated() {
|
|
if (!this._extensionUpdatedWatcher) {
|
|
// Watch the updated extension objects.
|
|
this._extensionUpdatedWatcher = async (evt, extension) => {
|
|
const browserPromise = this.debugBrowserPromises.get(extension.id);
|
|
if (browserPromise) {
|
|
const browser = await browserPromise;
|
|
if (
|
|
browser.isRemoteBrowser !== extension.remote &&
|
|
this.debugBrowserPromises.get(extension.id) === browserPromise
|
|
) {
|
|
// If the cached browser element is not anymore of the same
|
|
// remote type of the extension, remove it.
|
|
this.debugBrowserPromises.delete(extension.id);
|
|
browser.remove();
|
|
}
|
|
}
|
|
};
|
|
|
|
apiManager.on("ready", this._extensionUpdatedWatcher);
|
|
}
|
|
},
|
|
|
|
unwatchExtensionUpdated() {
|
|
if (this._extensionUpdatedWatcher) {
|
|
apiManager.off("ready", this._extensionUpdatedWatcher);
|
|
delete this._extensionUpdatedWatcher;
|
|
}
|
|
},
|
|
|
|
getExtensionManifestWarnings(id) {
|
|
const addon = GlobalManager.extensionMap.get(id);
|
|
if (addon) {
|
|
return addon.warnings;
|
|
}
|
|
return [];
|
|
},
|
|
|
|
/**
|
|
* Determine if the extension does have a non-persistent background script
|
|
* (either an event page or a background service worker):
|
|
*
|
|
* Based on this the DevTools client will determine if this extension should provide
|
|
* to the extension developers a button to forcefully terminate the background
|
|
* script.
|
|
*
|
|
* @param {string} addonId
|
|
* The id of the addon
|
|
*
|
|
* @returns {void|boolean}
|
|
* - undefined => does not apply (no background script in the manifest)
|
|
* - true => the background script is persistent.
|
|
* - false => the background script is an event page or a service worker.
|
|
*/
|
|
hasPersistentBackgroundScript(addonId) {
|
|
const policy = WebExtensionPolicy.getByID(addonId);
|
|
|
|
// The addon doesn't have any background script or we
|
|
// can't be sure yet.
|
|
if (
|
|
policy?.extension?.type !== "extension" ||
|
|
!policy?.extension?.manifest?.background
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return policy.extension.persistentBackground;
|
|
},
|
|
|
|
/**
|
|
* Determine if the extension background page is running.
|
|
*
|
|
* Based on this the DevTools client will show the status of the background
|
|
* script in about:debugging.
|
|
*
|
|
* @param {string} addonId
|
|
* The id of the addon
|
|
*
|
|
* @returns {void|boolean}
|
|
* - undefined => does not apply (no background script in the manifest)
|
|
* - true => the background script is running.
|
|
* - false => the background script is stopped.
|
|
*/
|
|
isBackgroundScriptRunning(addonId) {
|
|
const policy = WebExtensionPolicy.getByID(addonId);
|
|
|
|
// The addon doesn't have any background script or we
|
|
// can't be sure yet.
|
|
if (!(this.hasPersistentBackgroundScript(addonId) === false)) {
|
|
return undefined;
|
|
}
|
|
|
|
const views = policy?.extension?.views || [];
|
|
for (const view of views) {
|
|
if (
|
|
view.viewType === "background" ||
|
|
(view.viewType === "background_worker" && !view.unloaded)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
async terminateBackgroundScript(addonId) {
|
|
// Terminate the background if the extension does have
|
|
// a non-persistent background script (event page or background
|
|
// service worker).
|
|
if (this.hasPersistentBackgroundScript(addonId) === false) {
|
|
const policy = WebExtensionPolicy.getByID(addonId);
|
|
// When the event page is being terminated through the Devtools
|
|
// action, we should terminate it even if there are DevTools
|
|
// toolboxes attached to the extension.
|
|
return policy.extension.terminateBackground({
|
|
ignoreDevToolsAttached: true,
|
|
});
|
|
}
|
|
throw Error(`Unable to terminate background script for ${addonId}`);
|
|
},
|
|
|
|
/**
|
|
* Determine whether a devtools toolbox attached to the extension.
|
|
*
|
|
* This method is called by the background page idle timeout handler,
|
|
* to inhibit terminating the event page when idle while the extension
|
|
* developer is debugging the extension through the Addon Debugging window
|
|
* (similarly to how service workers are kept alive while the devtools are
|
|
* attached).
|
|
*
|
|
* @param {string} id
|
|
* The id of the extension.
|
|
*
|
|
* @returns {boolean}
|
|
* true when a devtools toolbox is attached to an extension with
|
|
* the given id, false otherwise.
|
|
*/
|
|
hasDevToolsAttached(id) {
|
|
return this.debugBrowserPromises.has(id);
|
|
},
|
|
|
|
/**
|
|
* Retrieve a XUL browser element which has been configured to be able to connect
|
|
* the devtools actor with the process where the extension is running.
|
|
*
|
|
* @param {WebExtensionParentActor} webExtensionParentActor
|
|
* The devtools actor that is retrieving the browser element.
|
|
*
|
|
* @returns {Promise<XULElement>}
|
|
* A promise which resolves to the configured browser XUL element.
|
|
*/
|
|
async getExtensionProcessBrowser(webExtensionParentActor) {
|
|
const extensionId = webExtensionParentActor.addonId;
|
|
const extension = GlobalManager.getExtension(extensionId);
|
|
if (!extension) {
|
|
throw new Error(`Extension not found: ${extensionId}`);
|
|
}
|
|
|
|
const createBrowser = () => {
|
|
if (!this.hiddenXULWindow) {
|
|
this.hiddenXULWindow = new HiddenXULWindow();
|
|
this.watchExtensionUpdated();
|
|
}
|
|
|
|
return this.hiddenXULWindow.createBrowserElement({
|
|
"webextension-addon-debug-target": extensionId,
|
|
remote: extension.remote ? "true" : null,
|
|
remoteType: extension.remoteType,
|
|
initialBrowsingContextGroupId: extension.browsingContextGroupId,
|
|
});
|
|
};
|
|
|
|
let browserPromise = this.debugBrowserPromises.get(extensionId);
|
|
|
|
// Create a new promise if there is no cached one in the map.
|
|
if (!browserPromise) {
|
|
browserPromise = createBrowser();
|
|
this.debugBrowserPromises.set(extensionId, browserPromise);
|
|
browserPromise.then(browser => {
|
|
browserPromise.browser = browser;
|
|
});
|
|
browserPromise.catch(e => {
|
|
Cu.reportError(e);
|
|
this.debugBrowserPromises.delete(extensionId);
|
|
});
|
|
}
|
|
|
|
this.debugActors.get(browserPromise).add(webExtensionParentActor);
|
|
|
|
return browserPromise;
|
|
},
|
|
|
|
getFrameLoader(extensionId) {
|
|
let promise = this.debugBrowserPromises.get(extensionId);
|
|
return promise && promise.browser && promise.browser.frameLoader;
|
|
},
|
|
|
|
/**
|
|
* Given the devtools actor that has retrieved an addon debug browser element,
|
|
* it destroys the XUL browser element, and it also destroy the hidden XUL window
|
|
* if it is not currently needed.
|
|
*
|
|
* @param {WebExtensionParentActor} webExtensionParentActor
|
|
* The devtools actor that has retrieved an addon debug browser element.
|
|
*/
|
|
async releaseExtensionProcessBrowser(webExtensionParentActor) {
|
|
const extensionId = webExtensionParentActor.addonId;
|
|
const browserPromise = this.debugBrowserPromises.get(extensionId);
|
|
|
|
if (browserPromise) {
|
|
const actorsSet = this.debugActors.get(browserPromise);
|
|
actorsSet.delete(webExtensionParentActor);
|
|
if (actorsSet.size === 0) {
|
|
this.debugActors.delete(browserPromise);
|
|
this.debugBrowserPromises.delete(extensionId);
|
|
await browserPromise.then(browser => browser.remove());
|
|
}
|
|
}
|
|
|
|
if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
|
|
this.hiddenXULWindow.shutdown();
|
|
this.hiddenXULWindow = null;
|
|
this.unwatchExtensionUpdated();
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Returns a Promise which resolves with the message data when the given message
|
|
* was received by the message manager. The promise is rejected if the message
|
|
* manager was closed before a message was received.
|
|
*
|
|
* @param {MessageListenerManager} messageManager
|
|
* The message manager on which to listen for messages.
|
|
* @param {string} messageName
|
|
* The message to listen for.
|
|
* @returns {Promise<*>}
|
|
*/
|
|
function promiseMessageFromChild(messageManager, messageName) {
|
|
return new Promise((resolve, reject) => {
|
|
let unregister;
|
|
function listener(message) {
|
|
unregister();
|
|
resolve(message.data);
|
|
}
|
|
function observer(subject, topic, data) {
|
|
if (subject === messageManager) {
|
|
unregister();
|
|
reject(
|
|
new Error(
|
|
`Message manager was disconnected before receiving ${messageName}`
|
|
)
|
|
);
|
|
}
|
|
}
|
|
unregister = () => {
|
|
Services.obs.removeObserver(observer, "message-manager-close");
|
|
messageManager.removeMessageListener(messageName, listener);
|
|
};
|
|
messageManager.addMessageListener(messageName, listener);
|
|
Services.obs.addObserver(observer, "message-manager-close");
|
|
});
|
|
}
|
|
|
|
// This should be called before browser.loadURI is invoked.
|
|
async function promiseBackgroundViewLoaded(browser) {
|
|
let { childId } = await promiseMessageFromChild(
|
|
browser.messageManager,
|
|
"Extension:BackgroundViewLoaded"
|
|
);
|
|
if (childId) {
|
|
return ParentAPIManager.getContextById(childId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
|
|
* to be called for every ExtensionProxyContext created for an extension page given
|
|
* its related extension, viewType and browser element (both the top level context and any context
|
|
* created for the extension urls running into its iframe descendants).
|
|
*
|
|
* @param {object} params
|
|
* @param {object} params.extension
|
|
* The Extension on which we are going to listen for the newly created ExtensionProxyContext.
|
|
* @param {string} params.viewType
|
|
* The viewType of the WebExtension page that we are watching (e.g. "background" or
|
|
* "devtools_page").
|
|
* @param {XULElement} params.browser
|
|
* The browser element of the WebExtension page that we are watching.
|
|
* @param {Function} onExtensionProxyContextLoaded
|
|
* The callback that is called when a new context has been loaded (as `callback(context)`);
|
|
*
|
|
* @returns {Function}
|
|
* Unsubscribe the listener.
|
|
*/
|
|
function watchExtensionProxyContextLoad(
|
|
{ extension, viewType, browser },
|
|
onExtensionProxyContextLoaded
|
|
) {
|
|
if (typeof onExtensionProxyContextLoaded !== "function") {
|
|
throw new Error("Missing onExtensionProxyContextLoaded handler");
|
|
}
|
|
|
|
const listener = (event, context) => {
|
|
if (context.viewType == viewType && context.xulBrowser == browser) {
|
|
onExtensionProxyContextLoaded(context);
|
|
}
|
|
};
|
|
|
|
extension.on("extension-proxy-context-load", listener);
|
|
|
|
return () => {
|
|
extension.off("extension-proxy-context-load", listener);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This helper is used to subscribe a listener (e.g. in the ext-backgroundPage)
|
|
* to be called for every ExtensionProxyContext created for an extension
|
|
* background service worker given its related extension.
|
|
*
|
|
* @param {object} params
|
|
* @param {object} params.extension
|
|
* The Extension on which we are going to listen for the newly created ExtensionProxyContext.
|
|
* @param {Function} onExtensionWorkerContextLoaded
|
|
* The callback that is called when the worker script has been fully loaded (as `callback(context)`);
|
|
*
|
|
* @returns {Function}
|
|
* Unsubscribe the listener.
|
|
*/
|
|
function watchExtensionWorkerContextLoaded(
|
|
{ extension },
|
|
onExtensionWorkerContextLoaded
|
|
) {
|
|
if (typeof onExtensionWorkerContextLoaded !== "function") {
|
|
throw new Error("Missing onExtensionWorkerContextLoaded handler");
|
|
}
|
|
|
|
const listener = (event, context) => {
|
|
if (context.viewType == "background_worker") {
|
|
onExtensionWorkerContextLoaded(context);
|
|
}
|
|
};
|
|
|
|
extension.on("extension-proxy-context-load:completed", listener);
|
|
|
|
return () => {
|
|
extension.off("extension-proxy-context-load:completed", listener);
|
|
};
|
|
}
|
|
|
|
// Manages icon details for toolbar buttons in the |pageAction| and
|
|
// |browserAction| APIs.
|
|
let IconDetails = {
|
|
DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
|
|
|
|
// WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
|
|
iconCache: new DefaultWeakMap(() => {
|
|
return new DefaultMap(() => new DefaultMap(() => new Map()));
|
|
}),
|
|
|
|
// Normalizes the various acceptable input formats into an object
|
|
// with icon size as key and icon URL as value.
|
|
//
|
|
// If a context is specified (function is called from an extension):
|
|
// Throws an error if an invalid icon size was provided or the
|
|
// extension is not allowed to load the specified resources.
|
|
//
|
|
// If no context is specified, instead of throwing an error, this
|
|
// function simply logs a warning message.
|
|
normalize(details, extension, context = null) {
|
|
if (!details.imageData && details.path != null) {
|
|
// Pick a cache key for the icon paths. If the path is a string,
|
|
// use it directly. Otherwise, stringify the path object.
|
|
let key = details.path;
|
|
if (typeof key !== "string") {
|
|
key = uneval(key);
|
|
}
|
|
|
|
let icons = this.iconCache
|
|
.get(extension)
|
|
.get(context && context.uri.spec)
|
|
.get(details.iconType);
|
|
|
|
let icon = icons.get(key);
|
|
if (!icon) {
|
|
icon = this._normalize(details, extension, context);
|
|
icons.set(key, icon);
|
|
}
|
|
return icon;
|
|
}
|
|
|
|
return this._normalize(details, extension, context);
|
|
},
|
|
|
|
_normalize(details, extension, context = null) {
|
|
let result = {};
|
|
|
|
try {
|
|
let { imageData, path, themeIcons } = details;
|
|
|
|
if (imageData) {
|
|
if (typeof imageData == "string") {
|
|
imageData = { 19: imageData };
|
|
}
|
|
|
|
for (let size of Object.keys(imageData)) {
|
|
result[size] = imageData[size];
|
|
}
|
|
}
|
|
|
|
let baseURI = context ? context.uri : extension.baseURI;
|
|
|
|
if (path != null) {
|
|
if (typeof path != "object") {
|
|
path = { 19: path };
|
|
}
|
|
|
|
for (let size of Object.keys(path)) {
|
|
let url = path[size];
|
|
if (url) {
|
|
url = baseURI.resolve(path[size]);
|
|
|
|
// The Chrome documentation specifies these parameters as
|
|
// relative paths. We currently accept absolute URLs as well,
|
|
// which means we need to check that the extension is allowed
|
|
// to load them. This will throw an error if it's not allowed.
|
|
this._checkURL(url, extension);
|
|
}
|
|
result[size] = url || this.DEFAULT_ICON;
|
|
}
|
|
}
|
|
|
|
if (themeIcons) {
|
|
themeIcons.forEach(({ size, light, dark }) => {
|
|
let lightURL = baseURI.resolve(light);
|
|
let darkURL = baseURI.resolve(dark);
|
|
|
|
this._checkURL(lightURL, extension);
|
|
this._checkURL(darkURL, extension);
|
|
|
|
let defaultURL = result[size] || result[19]; // always fallback to default first
|
|
result[size] = {
|
|
default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
|
|
light: lightURL,
|
|
dark: darkURL,
|
|
};
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Function is called from extension code, delegate error.
|
|
if (context) {
|
|
throw e;
|
|
}
|
|
// If there's no context, it's because we're handling this
|
|
// as a manifest directive. Log a warning rather than
|
|
// raising an error.
|
|
extension.manifestError(`Invalid icon data: ${e}`);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
// Checks if the extension is allowed to load the given URL with the specified principal.
|
|
// This will throw an error if the URL is not allowed.
|
|
_checkURL(url, extension) {
|
|
if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
|
|
throw new ExtensionError(`Illegal URL ${url}`);
|
|
}
|
|
},
|
|
|
|
// Returns the appropriate icon URL for the given icons object and the
|
|
// screen resolution of the given window.
|
|
getPreferredIcon(icons, extension = null, size = 16) {
|
|
const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
|
|
|
|
let bestSize = null;
|
|
if (icons[size]) {
|
|
bestSize = size;
|
|
} else if (icons[2 * size]) {
|
|
bestSize = 2 * size;
|
|
} else {
|
|
let sizes = Object.keys(icons)
|
|
.map(key => parseInt(key, 10))
|
|
.sort((a, b) => a - b);
|
|
|
|
bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
|
|
}
|
|
|
|
if (bestSize) {
|
|
return { size: bestSize, icon: icons[bestSize] || DEFAULT };
|
|
}
|
|
|
|
return { size, icon: DEFAULT };
|
|
},
|
|
|
|
// These URLs should already be properly escaped, but make doubly sure CSS
|
|
// string escape characters are escaped here, since they could lead to a
|
|
// sandbox break.
|
|
escapeUrl(url) {
|
|
return url.replace(/[\\\s"]/g, encodeURIComponent);
|
|
},
|
|
};
|
|
|
|
// A cache to support faster initialization of extensions at browser startup.
|
|
// All cached data is removed when the browser is updated.
|
|
// Extension-specific data is removed when the add-on is updated.
|
|
StartupCache = {
|
|
STORE_NAMES: Object.freeze([
|
|
"general",
|
|
"locales",
|
|
"manifests",
|
|
"other",
|
|
"permissions",
|
|
"schemas",
|
|
"menus",
|
|
]),
|
|
|
|
_ensureDirectoryPromise: null,
|
|
_saveTask: null,
|
|
|
|
_ensureDirectory() {
|
|
if (this._ensureDirectoryPromise === null) {
|
|
this._ensureDirectoryPromise = IOUtils.makeDirectory(
|
|
PathUtils.parent(this.file),
|
|
{
|
|
ignoreExisting: true,
|
|
createAncestors: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
return this._ensureDirectoryPromise;
|
|
},
|
|
|
|
// When the application version changes, this file is removed by
|
|
// RemoveComponentRegistries in nsAppRunner.cpp.
|
|
file: PathUtils.join(
|
|
Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
|
|
"startupCache",
|
|
"webext.sc.lz4"
|
|
),
|
|
|
|
async _saveNow() {
|
|
let data = new Uint8Array(lazy.aomStartup.encodeBlob(this._data));
|
|
await this._ensureDirectoryPromise;
|
|
await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
|
|
Services.telemetry.scalarSet(
|
|
"extensions.startupCache.write_byteLength",
|
|
data.byteLength
|
|
);
|
|
},
|
|
|
|
save() {
|
|
this._ensureDirectory();
|
|
|
|
if (!this._saveTask) {
|
|
this._saveTask = new lazy.DeferredTask(() => this._saveNow(), 5000);
|
|
|
|
IOUtils.profileBeforeChange.addBlocker(
|
|
"Flush WebExtension StartupCache",
|
|
async () => {
|
|
await this._saveTask.finalize();
|
|
this._saveTask = null;
|
|
}
|
|
);
|
|
}
|
|
|
|
return this._saveTask.arm();
|
|
},
|
|
|
|
_data: null,
|
|
async _readData() {
|
|
let result = new Map();
|
|
try {
|
|
Glean.extensions.startupCacheLoadTime.start();
|
|
let { buffer } = await IOUtils.read(this.file);
|
|
|
|
result = lazy.aomStartup.decodeBlob(buffer);
|
|
Glean.extensions.startupCacheLoadTime.stop();
|
|
} catch (e) {
|
|
Glean.extensions.startupCacheLoadTime.cancel();
|
|
if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
|
|
Cu.reportError(e);
|
|
}
|
|
|
|
Services.telemetry.keyedScalarAdd(
|
|
"extensions.startupCache.read_errors",
|
|
lazy.getErrorNameForTelemetry(e),
|
|
1
|
|
);
|
|
}
|
|
|
|
this._data = result;
|
|
return result;
|
|
},
|
|
|
|
get dataPromise() {
|
|
if (!this._dataPromise) {
|
|
this._dataPromise = this._readData();
|
|
}
|
|
return this._dataPromise;
|
|
},
|
|
|
|
clearAddonData(id) {
|
|
return Promise.all([
|
|
this.general.delete(id),
|
|
this.locales.delete(id),
|
|
this.manifests.delete(id),
|
|
this.permissions.delete(id),
|
|
this.menus.delete(id),
|
|
]).catch(e => {
|
|
// Ignore the error. It happens when we try to flush the add-on
|
|
// data after the AddonManager has flushed the entire startup cache.
|
|
});
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (topic === "startupcache-invalidate") {
|
|
this._data = new Map();
|
|
this._dataPromise = Promise.resolve(this._data);
|
|
}
|
|
},
|
|
|
|
get(extension, path, createFunc) {
|
|
return this.general.get(
|
|
[extension.id, extension.version, ...path],
|
|
createFunc
|
|
);
|
|
},
|
|
|
|
delete(extension, path) {
|
|
return this.general.delete([extension.id, extension.version, ...path]);
|
|
},
|
|
};
|
|
|
|
Services.obs.addObserver(StartupCache, "startupcache-invalidate");
|
|
|
|
class CacheStore {
|
|
constructor(storeName) {
|
|
this.storeName = storeName;
|
|
}
|
|
|
|
async getStore(path = null) {
|
|
let data = await StartupCache.dataPromise;
|
|
|
|
let store = data.get(this.storeName);
|
|
if (!store) {
|
|
store = new Map();
|
|
data.set(this.storeName, store);
|
|
}
|
|
|
|
let key = path;
|
|
if (Array.isArray(path)) {
|
|
for (let elem of path.slice(0, -1)) {
|
|
let next = store.get(elem);
|
|
if (!next) {
|
|
next = new Map();
|
|
store.set(elem, next);
|
|
}
|
|
store = next;
|
|
}
|
|
key = path[path.length - 1];
|
|
}
|
|
|
|
return [store, key];
|
|
}
|
|
|
|
async get(path, createFunc) {
|
|
let [store, key] = await this.getStore(path);
|
|
|
|
let result = store.get(key);
|
|
|
|
if (result === undefined) {
|
|
result = await createFunc(path);
|
|
store.set(key, result);
|
|
StartupCache.save();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async set(path, value) {
|
|
let [store, key] = await this.getStore(path);
|
|
|
|
store.set(key, value);
|
|
StartupCache.save();
|
|
}
|
|
|
|
async getAll() {
|
|
let [store] = await this.getStore();
|
|
|
|
return new Map(store);
|
|
}
|
|
|
|
async delete(path) {
|
|
let [store, key] = await this.getStore(path);
|
|
|
|
if (store.delete(key)) {
|
|
StartupCache.save();
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let name of StartupCache.STORE_NAMES) {
|
|
StartupCache[name] = new CacheStore(name);
|
|
}
|
|
|
|
export var ExtensionParent = {
|
|
GlobalManager,
|
|
HiddenExtensionPage,
|
|
IconDetails,
|
|
ParentAPIManager,
|
|
StartupCache,
|
|
WebExtensionPolicy,
|
|
apiManager,
|
|
promiseBackgroundViewLoaded,
|
|
watchExtensionProxyContextLoad,
|
|
watchExtensionWorkerContextLoaded,
|
|
DebugUtils,
|
|
};
|
|
|
|
// browserPaintedPromise and browserStartupPromise are promises that
|
|
// resolve after the first browser window is painted and after browser
|
|
// windows have been restored, respectively. Alternatively,
|
|
// browserStartupPromise also resolves from the extensions-late-startup
|
|
// notification sent by Firefox Reality on desktop platforms, because it
|
|
// doesn't support SessionStore.
|
|
// _resetStartupPromises should only be called from outside this file in tests.
|
|
ExtensionParent._resetStartupPromises = () => {
|
|
ExtensionParent.browserPaintedPromise = promiseObserved(
|
|
"browser-delayed-startup-finished"
|
|
).then(() => {});
|
|
ExtensionParent.browserStartupPromise = Promise.race([
|
|
promiseObserved("sessionstore-windows-restored"),
|
|
promiseObserved("extensions-late-startup"),
|
|
]).then(() => {});
|
|
};
|
|
ExtensionParent._resetStartupPromises();
|
|
|
|
ChromeUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
|
|
return Object.freeze({
|
|
os: (function () {
|
|
let os = AppConstants.platform;
|
|
if (os == "macosx") {
|
|
os = "mac";
|
|
}
|
|
return os;
|
|
})(),
|
|
arch: (function () {
|
|
let abi = Services.appinfo.XPCOMABI;
|
|
let [arch] = abi.split("-");
|
|
if (arch == "x86") {
|
|
arch = "x86-32";
|
|
} else if (arch == "x86_64") {
|
|
arch = "x86-64";
|
|
}
|
|
return arch;
|
|
})(),
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Retreives the browser_style stylesheets needed for extension popups and sidebars.
|
|
*
|
|
* @returns {Array<string>} an array of stylesheets needed for the current platform.
|
|
*/
|
|
ChromeUtils.defineLazyGetter(ExtensionParent, "extensionStylesheets", () => {
|
|
let stylesheets = ["chrome://browser/content/extension.css"];
|
|
|
|
if (AppConstants.platform === "macosx") {
|
|
stylesheets.push("chrome://browser/content/extension-mac.css");
|
|
}
|
|
|
|
return stylesheets;
|
|
});
|