Newer
Older
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */

Kris Maglione
committed
/* 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";
/**
* 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.
*/
/* exported ExtensionParent */

Florian Quèze
committed
var EXPORTED_SYMBOLS = ["ExtensionParent"];

Kris Maglione
committed
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);

Kris Maglione
committed
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
AppConstants: "resource://gre/modules/AppConstants.jsm",
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",

Tomislav Jovanovic
committed
BroadcastConduit: "resource://gre/modules/ConduitsParent.jsm",
DeferredTask: "resource://gre/modules/DeferredTask.jsm",
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",

Bob Silverberg
committed
ExtensionData: "resource://gre/modules/Extension.jsm",
ExtensionActivityLog: "resource://gre/modules/ExtensionActivityLog.jsm",
GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.jsm",
MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.jsm",
NativeApp: "resource://gre/modules/NativeMessaging.jsm",
PerformanceCounters: "resource://gre/modules/PerformanceCounters.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
Schemas: "resource://gre/modules/Schemas.jsm",

Luca Greco
committed
getErrorNameForTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm",
});
XPCOMUtils.defineLazyServiceGetters(this, {
aomStartup: [
"@mozilla.org/addons/addon-manager-startup;1",
"amIAddonManagerStartup",
],
});

Matthew Wein
committed
// We're using the pref to avoid loading PerformanceCounters.jsm for nothing.
XPCOMUtils.defineLazyPreferenceGetter(
this,
"gTimingEnabled",
"extensions.webextensions.enablePerformanceCounters",
false
);
const { ExtensionCommon } = ChromeUtils.import(
"resource://gre/modules/ExtensionCommon.jsm"
);
const { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm"
);

Kris Maglione
committed
var {
BaseContext,

Kris Maglione
committed
CanOfAPIs,

Kris Maglione
committed
SchemaAPIManager,
SpreadArgs,
defineLazyGetter,

Kris Maglione
committed
} = ExtensionCommon;
var {

Kris Maglione
committed
DefaultMap,
DefaultWeakMap,
ExtensionError,
promiseDocumentLoaded,
promiseEvent,
promiseObserved,

Kris Maglione
committed
} = ExtensionUtils;

Tomislav Jovanovic
committed
const ERROR_NO_RECEIVERS =
"Could not establish connection. Receiving end does not exist.";

Kris Maglione
committed
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";

Kris Maglione
committed
const CATEGORY_EXTENSION_MODULES = "webextension-modules";

Kris Maglione
committed
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
let schemaURLs = new Set();
schemaURLs.add("chrome://extensions/content/schemas/experiments.json");

Kris Maglione
committed
let GlobalManager;
let ParentAPIManager;

Kris Maglione
committed
let StartupCache;

Kris Maglione
committed
function verifyActorForContext(actor, context) {

Kagami Sascha Rosylight
committed
if (JSWindowActorParent.isInstance(actor)) {
let target = actor.browsingContext.top.embedderElement;
if (context.parentMessageManager !== target.messageManager) {
throw new Error("Got message on unexpected message manager");
}

Kagami Sascha Rosylight
committed
} else if (JSProcessActorParent.isInstance(actor)) {
if (actor.manager.remoteType !== context.extension.remoteType) {
throw new Error("Got message from unexpected process");
}
}
}

Kris Maglione
committed
// This object loads the ext-*.js scripts that define the extension API.
let apiManager = new (class extends SchemaAPIManager {

Kris Maglione
committed
constructor() {

Kris Maglione
committed
super("main", Schemas);

Kris Maglione
committed
this.initialized = null;

Kris Maglione
committed

Bob Silverberg
committed
/* eslint-disable mozilla/balanced-listeners */
this.on("startup", (e, extension) => {

Kris Maglione
committed
return extension.apiManager.onStartup(extension);

Kris Maglione
committed
});

Bob Silverberg
committed
this.on("update", async (e, { id, resourceURI, isPrivileged }) => {

Bob Silverberg
committed
let modules = this.eventModules.get("update");
if (modules.size == 0) {
return;
}
let extension = new ExtensionData(resourceURI, isPrivileged);

Bob Silverberg
committed
await extension.loadManifest();
return Promise.all(
Array.from(modules).map(async apiName => {
let module = await this.asyncLoadModule(apiName);
module.onUpdate(id, extension.manifest);
})
);

Bob Silverberg
committed
});
this.on("uninstall", (e, { id }) => {

Bob Silverberg
committed
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);
})
);

Bob Silverberg
committed
});
/* eslint-enable mozilla/balanced-listeners */
// Handle any changes that happened during startup
let disabledIds = AddonManager.getStartupChanges(
AddonManager.STARTUP_CHANGE_DISABLED
);

monikamaheshwari
committed
if (disabledIds.length) {
this._callHandlers(disabledIds, "disable", "onDisable");
}
let uninstalledIds = AddonManager.getStartupChanges(
AddonManager.STARTUP_CHANGE_UNINSTALLED
);

monikamaheshwari
committed
if (uninstalledIds.length) {
this._callHandlers(uninstalledIds, "uninstall", "onUninstall");
}

Kris Maglione
committed
}

Kris Maglione
committed
getModuleJSONURLs() {
return Array.from(
Services.catMan.enumerateCategory(CATEGORY_EXTENSION_MODULES),
({ value }) => value
);

Kris Maglione
committed
}

Kris Maglione
committed
// 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())
);

Kris Maglione
committed
let scriptURLs = [];
for (let { value } of Services.catMan.enumerateCategory(
CATEGORY_EXTENSION_SCRIPTS
)) {

Kris Maglione
committed
scriptURLs.push(value);

Kris Maglione
committed
}

Kris Maglione
committed
let promise = (async () => {
let scripts = await Promise.all(
scriptURLs.map(url => ChromeUtils.compileScript(url))
);

Kris Maglione
committed
this.initModuleData(await modulesPromise);

Kris Maglione
committed
this.initGlobal();

Kris Maglione
committed
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.

Kris Maglione
committed
return Schemas.load(BASE_SCHEMA).then(() => {

Kris Maglione
committed
let promises = [];
for (let { value } of Services.catMan.enumerateCategory(
CATEGORY_EXTENSION_SCHEMAS
)) {

Kris Maglione
committed
promises.push(Schemas.load(value));

Kris Maglione
committed
}
for (let [url, { content }] of this.schemaURLs) {

Kris Maglione
committed
promises.push(Schemas.load(url, content));

Kris Maglione
committed
}
for (let url of schemaURLs) {
promises.push(Schemas.load(url));
}

Kris Maglione
committed
return Promise.all(promises).then(() => {
Schemas.updateSharedSchemas();
});

Kris Maglione
committed
});

Kris Maglione
committed
})();

Kris Maglione
committed

Tomislav Jovanovic
committed
Services.mm.addMessageListener("Extension:GetFrameData", this);

Kris Maglione
committed

Kris Maglione
committed
this.initialized = promise;
return this.initialized;
}

Tomislav Jovanovic
committed
receiveMessage({ target }) {
let data = GlobalManager.frameData.get(target) || {};
Object.assign(data, this.global.tabTracker.getBrowserData(target));
return data;

Kris Maglione
committed
}
// 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)));
}

Shane Caraveo
committed
if (event === "enabling") {
promises.push(...ids.map(id => this.emit("enabling", id)));
}
AsyncShutdown.profileBeforeChange.addBlocker(
`Extension API ${event} handlers for ${ids.join(",")}`,
Promise.all(promises)
);

Kris Maglione
committed

Tomislav Jovanovic
committed
// Receives messages related to the extension messaging API and forwards them
// to relevant child messengers. Also handles Native messaging and GeckoView.
const ProxyMessenger = {

Tomislav Jovanovic
committed
/**
* @typedef {object} ParentPort
* @prop {function(StructuredCloneHolder)} onPortMessage
* @prop {function()} onPortDisconnect
*/
/** @type Map<number, ParentPort> */
ports: new Map(),
_torRuntimeMessageListeners: [],

Tomislav Jovanovic
committed
init() {

Tomislav Jovanovic
committed
this.conduit = new BroadcastConduit(ProxyMessenger, {
id: "ProxyMessenger",
reportOnClosed: "portId",

Tomislav Jovanovic
committed
recv: ["PortConnect", "PortMessage", "NativeMessage", "RuntimeMessage"],
cast: ["PortConnect", "PortMessage", "PortDisconnect", "RuntimeMessage"],

Tomislav Jovanovic
committed
});
},
openNative(nativeApp, sender) {
let context = ParentAPIManager.getContextById(sender.childId);

Tomislav Jovanovic
committed
if (context.extension.hasPermission("geckoViewAddons")) {

Agi Sferro
committed
return new GeckoViewConnection(

Tomislav Jovanovic
committed
this.getSender(context.extension, sender),

Tomislav Jovanovic
committed
sender.actor.browsingContext.top.embedderElement,

Agi Sferro
committed
nativeApp,

Tomislav Jovanovic
committed
context.extension.hasPermission("nativeMessagingFromContent")

Agi Sferro
committed
);

Tomislav Jovanovic
committed
} else if (sender.verified) {
return new NativeApp(context, nativeApp);
}
sender = this.getSender(context.extension, sender);

Tomislav Jovanovic
committed
throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
},
recvNativeMessage({ nativeApp, holder }, { sender }) {
return this.openNative(nativeApp, sender).sendMessage(holder);
},

Tomislav Jovanovic
committed
getSender(extension, source) {

Tomislav Jovanovic
committed
let sender = {
contextId: source.id,
id: source.extensionId,
envType: source.envType,

Tomislav Jovanovic
committed
url: source.url,

Tomislav Jovanovic
committed
};

Kagami Sascha Rosylight
committed
if (JSWindowActorParent.isInstance(source.actor)) {

Luca Greco
committed
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();

Rob Wu
committed
// frameId is documented to only be set if sender.tab is set.
sender.frameId = source.frameId;

Luca Greco
committed
}

Tomislav Jovanovic
committed
}

Tomislav Jovanovic
committed

Tomislav Jovanovic
committed
return sender;
},

Tomislav Jovanovic
committed
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") {

Tomislav Jovanovic
committed
// No receivers in discarded tabs, so bail early to keep the browser lazy.
throw new ExtensionError(ERROR_NO_RECEIVERS);
}

Tomislav Jovanovic
committed
let browser = tab.linkedBrowser || tab.browser;
return browser.browsingContext.id;
},

Tomislav Jovanovic
committed
// 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);

Luca Greco
committed
if (!extension) {
return Promise.reject({ message: ERROR_NO_RECEIVERS });
}

Tomislav Jovanovic
committed
await extension.wakeupBackground?.();

Tomislav Jovanovic
committed
arg.sender = this.getSender(extension, sender);

Tomislav Jovanovic
committed
arg.topBC = arg.tabId && this.getTopBrowsingContextId(arg.tabId);
return arg.tabId ? "tab" : "messenger";
},

Tomislav Jovanovic
committed
async recvRuntimeMessage(arg, { sender }) {
// We need to listen to some extension messages in Tor Browser
for (const listener of this._torRuntimeMessageListeners) {
listener(arg);
}

Tomislav Jovanovic
committed
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;

Tomislav Jovanovic
committed
},

Tomislav Jovanovic
committed
async recvPortConnect(arg, { sender }) {
if (arg.native) {
let port = this.openNative(arg.name, sender).onConnect(arg.portId, this);
this.ports.set(arg.portId, port);
return;
}

Tomislav Jovanovic
committed
// 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)));

Tomislav Jovanovic
committed
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) {
return this.ports.get(sender.portId).onPortMessage(holder);

Tomislav Jovanovic
committed
}
await this.ports.get(sender.portId);
this.sendPortMessage(sender.portId, holder, !sender.source);

Tomislav Jovanovic
committed
},
recvConduitClosed(sender) {
let app = this.ports.get(sender.portId);
if (this.ports.delete(sender.portId) && sender.native) {
return app.onPortDisconnect();
}
this.sendPortDisconnect(sender.portId, null, !sender.source);

Tomislav Jovanovic
committed
},
sendPortMessage(portId, holder, source = true) {
this.conduit.castPortMessage("port", { portId, source, holder });

Tomislav Jovanovic
committed
},
sendPortDisconnect(portId, error, source = true) {
this.conduit.castPortDisconnect("port", { portId, source, error });

Tomislav Jovanovic
committed
this.ports.delete(portId);
},
};

Tomislav Jovanovic
committed
ProxyMessenger.init();

Kris Maglione
committed
// 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,

Tomislav Jovanovic
committed
/** @type {WeakMap<Browser, object>} Extension Context init data. */
frameData: new WeakMap(),

Kris Maglione
committed
init(extension) {
if (this.extensionMap.size == 0) {
apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = true;
Services.ppmm.addMessageListener(
"Extension:SendPerformanceCounter",
this
);

Kris Maglione
committed
}
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;
Services.ppmm.removeMessageListener(
"Extension:SendPerformanceCounter",
this
);
}
},
async receiveMessage({ name, data }) {
switch (name) {
case "Extension:SendPerformanceCounter":
PerformanceCounters.merge(data.counters);
break;

Kris Maglione
committed
}
},

Tomislav Jovanovic
committed
_onExtensionBrowser(type, browser, data = {}) {
data.viewType = browser.getAttribute("webextension-view-type");
if (data.viewType) {
GlobalManager.frameData.set(browser, data);

Kris Maglione
committed
}

Kris Maglione
committed
},
getExtension(extensionId) {
return this.extensionMap.get(extensionId);
},
};

Kris Maglione
committed
/**
* The proxied parent side of a context in ExtensionChild.jsm, for the
* parent side of a proxied API.
*/
class ProxyContextParent extends BaseContext {

Kris Maglione
committed
constructor(envType, extension, params, xulBrowser, principal) {
super(envType, extension);
this.childId = params.childId;
this.uri = Services.io.newURI(params.url);

Kris Maglione
committed
this.incognito = params.incognito;

Kris Maglione
committed
this.listenerPromises = new Set();

Kris Maglione
committed
// 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.
this.messageManagerProxy =
xulBrowser && new MessageManagerProxy(xulBrowser);

Kris Maglione
committed
Object.defineProperty(this, "principal", {
value: principal,
enumerable: true,
configurable: true,

Kris Maglione
committed
});
this.listenerProxies = new Map();

Andrew Swan
committed
this.pendingEventBrowser = null;

Luca Greco
committed
this.callContextData = null;

Andrew Swan
committed

Kris Maglione
committed
apiManager.emit("proxy-context-load", this);
}

Luca Greco
committed
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
/**
* 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) {

Andrew Swan
committed
let savedBrowser = this.pendingEventBrowser;
this.pendingEventBrowser = browser;
try {
let result = await callable();
return result;

Andrew Swan
committed
} 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.
}

Kris Maglione
committed
get cloneScope() {
return this.sandbox;
}

Kris Maglione
committed
applySafe(callback, args) {

Kris Maglione
committed
// There's no need to clone when calling listeners for a proxied
// context.

Kris Maglione
committed
return this.applySafeWithoutClone(callback, args);

Kris Maglione
committed
}

Kris Maglione
committed
get xulBrowser() {
return this.messageManagerProxy?.eventTarget;

Kris Maglione
committed
}

Kris Maglione
committed

Kris Maglione
committed
get parentMessageManager() {
return this.messageManagerProxy?.messageManager;

Kris Maglione
committed
}
shutdown() {
this.unload();
}
unload() {
if (this.unloaded) {
return;
}
this.messageManagerProxy?.dispose();

Kris Maglione
committed
super.unload();
apiManager.emit("proxy-context-unload", this);
}
}

Kris Maglione
committed
defineLazyGetter(ProxyContextParent.prototype, "apiCan", function() {

Kris Maglione
committed
let obj = {};

Kris Maglione
committed
let can = new CanOfAPIs(this, this.extension.apiManager, obj);

Kris Maglione
committed
return can;
});
defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
return this.apiCan.root;

Kris Maglione
committed
});

Kris Maglione
committed
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"],
});

Kris Maglione
committed
});

Kris Maglione
committed
/**
* The parent side of proxied API context for extension content script
* running in ExtensionContent.jsm.
*/
class ContentScriptContextParent extends ProxyContextParent {}

Kris Maglione
committed
/**
* 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 {

Kris Maglione
committed
constructor(envType, extension, params, xulBrowser) {
super(envType, extension, params, xulBrowser, extension.principal);
this.viewType = params.viewType;

Luca Greco
committed

Kris Maglione
committed
this.extension.views.add(this);

Luca Greco
committed
extension.emit("extension-proxy-context-load", this);

Kris Maglione
committed
}
// The window that contains this context. This may change due to moving tabs.

Brendan Dahl
committed
get appWindow() {
let win = this.xulBrowser.ownerGlobal;

Kris Maglione
committed
return win.browsingContext.topChromeWindow;

Kris Maglione
committed
}

Kris Maglione
committed
get currentWindow() {
if (this.viewType !== "background") {

Brendan Dahl
committed
return this.appWindow;

Kris Maglione
committed
}
}

Kris Maglione
committed
get tabId() {
let { tabTracker } = apiManager.global;

Kris Maglione
committed
let data = tabTracker.getBrowserData(this.xulBrowser);
if (data.tabId >= 0) {
return data.tabId;

Kris Maglione
committed
}
}
onBrowserChange(browser) {
super.onBrowserChange(browser);
this.xulBrowser = browser;
}
unload() {
super.unload();
this.extension.views.delete(this);
}

Kris Maglione
committed
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);

Daisuke Akatsuka
committed

Alexandre Poirot
committed
// 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;

Daisuke Akatsuka
committed
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;
}

Daisuke Akatsuka
committed
async addOnNavigatedListener(listener) {
if (!this._onNavigatedListeners) {
this._onNavigatedListeners = new Set();

Alexandre Poirot
committed
await this.devToolsToolbox.resourceCommand.watchResources(
[this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],

Daisuke Akatsuka
committed
{
onAvailable: this._onResourceAvailable,
ignoreExistingResources: true,
}
);
}
this._onNavigatedListeners.add(listener);
}
removeOnNavigatedListener(listener) {
if (this._onNavigatedListeners) {
this._onNavigatedListeners.delete(listener);
}
}
/**

Alexandre Poirot
committed
* 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>}

Alexandre Poirot
committed
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;
}

Alexandre Poirot
committed
this._devToolsCommandsPromise = (async () => {
const commands = await DevToolsShim.createCommandsForTabForWebExtension(
this.devToolsToolbox.descriptorFront.localTab
);
await commands.targetCommand.startListening();
this._devToolsCommands = commands;
this._devToolsCommandsPromise = null;
return commands;
})();
return this._devToolsCommandsPromise;
}

Nicolas Chevobbe
committed
unload() {
// Bail if the toolbox reference was already cleared.
if (!this.devToolsToolbox) {
return;
}

Daisuke Akatsuka
committed
if (this._onNavigatedListeners) {

Alexandre Poirot
committed
this.devToolsToolbox.resourceCommand.unwatchResources(
[this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],

Daisuke Akatsuka
committed
{ onAvailable: this._onResourceAvailable }
);
}

Alexandre Poirot
committed
if (this._devToolsCommands) {
this._devToolsCommands.destroy();
this._devToolsCommands = null;
}

Daisuke Akatsuka
committed
if (this._onNavigatedListeners) {
this._onNavigatedListeners.clear();
this._onNavigatedListeners = null;
}
this._devToolsToolbox = null;

Nicolas Chevobbe
committed
super.unload();
}

Alexandre Poirot
committed
async _onResourceAvailable(resources) {
for (const resource of resources) {
const { targetFront } = resource;
if (targetFront.isTopLevel && resource.name === "dom-complete") {
const url = targetFront.localTab.linkedBrowser.currentURI.spec;
for (const listener of this._onNavigatedListeners) {
listener(url);
}

Daisuke Akatsuka
committed
}
}
}
}
/**
* 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 xulBrowser and one for contexts that are
// expected to have a xulBrowser 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);
}
}

Kris Maglione
committed
ParentAPIManager = {
proxyContexts: new Map(),
init() {
// TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
Services.obs.addObserver(this, "message-manager-close");

Kris Maglione
committed

Tomislav Jovanovic
committed
this.conduit = new BroadcastConduit(this, {
id: "ParentAPIManager",
reportOnClosed: "childId",

Luca Greco
committed
recv: [
"CreateProxyContext",
"ContextLoaded",
"APICall",
"AddListener",
"RemoveListener",
],

Tomislav Jovanovic
committed
send: ["CallResult"],
query: ["RunListener", "StreamFilterSuspendCancel"],

Tomislav Jovanovic
committed
});

Kris Maglione
committed
},

Kris Maglione
committed
attachMessageManager(extension, processMessageManager) {
extension.parentMessageManager = processMessageManager;
},
async observe(subject, topic, data) {

Kris Maglione
committed
if (topic === "message-manager-close") {
let mm = subject;
for (let [childId, context] of this.proxyContexts) {

Kris Maglione
committed
if (context.parentMessageManager === mm) {

Kris Maglione
committed
this.closeProxyContext(childId);
}
}

Kris Maglione
committed
// Reset extension message managers when their child processes shut down.
for (let extension of GlobalManager.extensionMap.values()) {
if (extension.parentMessageManager === mm) {
extension.parentMessageManager = null;
}
}

Kris Maglione
committed
}
},
shutdownExtension(extensionId, reason) {
if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
apiManager._callHandlers([extensionId], "disable", "onDisable");
}

Kris Maglione
committed
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);
},

Tomislav Jovanovic
committed
recvCreateProxyContext(data, { actor, sender }) {
let { envType, extensionId, childId, principal } = data;
let target = actor.browsingContext?.top.embedderElement;

Tomislav Jovanovic
committed

Kris Maglione
committed
if (this.proxyContexts.has(childId)) {
throw new Error(
"A WebExtension context with the given ID already exists!"
);

Kris Maglione
committed
}
let extension = GlobalManager.getExtension(extensionId);
if (!extension) {
throw new Error(`No WebExtension found with ID ${extensionId}`);
}
let context;

Luca Greco
committed
if (envType == "addon_parent" || envType == "devtools_parent") {

Tomislav Jovanovic
committed
if (!sender.verified) {
throw new Error(`Bad sender context envType: ${sender.envType}`);
}

Kagami Sascha Rosylight
committed
if (JSWindowActorParent.isInstance(actor)) {
let processMessageManager =
target.messageManager.processMessageManager ||
Services.ppmm.getChildAt(0);

Kris Maglione
committed
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"
);
}

Kagami Sascha Rosylight
committed
} 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`
);
}

Kris Maglione
committed
}
if (envType == "addon_parent" && data.viewType === "background_worker") {
context = new BackgroundWorkerContextParent(envType, extension, data);
} else if (envType == "addon_parent") {
context = new ExtensionPageContextParent(
envType,
extension,
data,
target
);
} else if (envType == "devtools_parent") {
context = new DevToolsExtensionPageContextParent(
envType,
extension,
data,
target
);
}

Kris Maglione
committed
} else if (envType == "content_parent") {
context = new ContentScriptContextParent(
envType,
extension,
data,
target,
principal
);

Kris Maglione
committed
} else {
throw new Error(`Invalid WebExtension context envType: ${envType}`);
}
this.proxyContexts.set(childId, context);
},

Luca Greco
committed
recvContextLoaded(data, { actor, sender }) {
let context = this.getContextById(data.childId);
verifyActorForContext(actor, context);
const { extension } = context;
extension.emit("extension-proxy-context-load:completed", context);
},

Tomislav Jovanovic
committed
recvConduitClosed(sender) {
this.closeProxyContext(sender.id);
},

Kris Maglione
committed
closeProxyContext(childId) {
let context = this.proxyContexts.get(childId);
if (context) {
context.unload();
this.proxyContexts.delete(childId);
}
},
async retrievePerformanceCounters() {
// getting the parent counters
return PerformanceCounters.getData();
/**
* Call the given function and also log the call as appropriate
* (i.e., with PerformanceCounters and/or activity logging)
*
* @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) {
ExtensionActivityLog.log(id, context.viewType, "api_call", data.path, {
args: data.args,
});

Florian Quèze
committed
let start = Cu.now();
try {
return callable();
} finally {

Florian Quèze
committed
ChromeUtils.addProfilerMarker(
"ExtensionParent",
{ startTime: start },
`${id}, api_call: ${data.path}`
);
if (gTimingEnabled) {
let end = Cu.now() * 1000;