summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/ExtensionChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/ExtensionChild.sys.mjs')
-rw-r--r--toolkit/components/extensions/ExtensionChild.sys.mjs1040
1 files changed, 1040 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ExtensionChild.sys.mjs b/toolkit/components/extensions/ExtensionChild.sys.mjs
new file mode 100644
index 0000000000..c095d03aeb
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionChild.sys.mjs
@@ -0,0 +1,1040 @@
+/* -*- 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 file handles addon logic that is independent of the chrome process and
+ * may run in all web content and extension processes.
+ *
+ * Don't put contentscript logic here, use ExtensionContent.jsm instead.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "finalizationService",
+ "@mozilla.org/toolkit/finalizationwitness;1",
+ "nsIFinalizationWitnessService"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ExtensionContent: "resource://gre/modules/ExtensionContent.sys.mjs",
+ ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.sys.mjs",
+ ExtensionProcessScript:
+ "resource://gre/modules/ExtensionProcessScript.sys.mjs",
+ NativeApp: "resource://gre/modules/NativeMessaging.sys.mjs",
+ PerformanceCounters: "resource://gre/modules/PerformanceCounters.sys.mjs",
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+});
+
+// We're using the pref to avoid loading PerformanceCounters.jsm for nothing.
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "gTimingEnabled",
+ "extensions.webextensions.enablePerformanceCounters",
+ false
+);
+import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
+import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
+
+const { DefaultMap, ExtensionError, LimitedSet, getUniqueId } = ExtensionUtils;
+
+const {
+ defineLazyGetter,
+ EventEmitter,
+ EventManager,
+ LocalAPIImplementation,
+ LocaleData,
+ NoCloneSpreadArgs,
+ SchemaAPIInterface,
+ withHandlingUserInput,
+} = ExtensionCommon;
+
+const { sharedData } = Services.cpmm;
+
+const MSG_SET_ENABLED = "Extension:ActivityLog:SetEnabled";
+const MSG_LOG = "Extension:ActivityLog:DoLog";
+
+export const ExtensionActivityLogChild = {
+ _initialized: false,
+ enabledExtensions: new Set(),
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ Services.cpmm.addMessageListener(MSG_SET_ENABLED, this);
+
+ this.enabledExtensions = new Set(
+ Services.cpmm.sharedData.get("extensions/logging")
+ );
+ },
+
+ receiveMessage({ name, data }) {
+ if (name === MSG_SET_ENABLED) {
+ if (data.value) {
+ this.enabledExtensions.add(data.id);
+ } else {
+ this.enabledExtensions.delete(data.id);
+ }
+ }
+ },
+
+ async log(context, type, name, data) {
+ this.init();
+ let { id } = context.extension;
+ if (this.enabledExtensions.has(id)) {
+ this._sendActivity({
+ timeStamp: Date.now(),
+ id,
+ viewType: context.viewType,
+ type,
+ name,
+ data,
+ browsingContextId: context.browsingContextId,
+ });
+ }
+ },
+
+ _sendActivity(data) {
+ Services.cpmm.sendAsyncMessage(MSG_LOG, data);
+ },
+};
+
+// A helper to allow us to distinguish trusted errors from unsanitized errors.
+// Extensions can create plain objects with arbitrary properties (such as
+// mozWebExtLocation), but not create instances of ExtensionErrorHolder.
+class ExtensionErrorHolder {
+ constructor(trustedErrorObject) {
+ this.trustedErrorObject = trustedErrorObject;
+ }
+}
+
+/**
+ * A finalization witness helper that wraps a sendMessage response and
+ * guarantees to either get the promise resolved, or rejected when the
+ * wrapped promise goes out of scope.
+ */
+const StrongPromise = {
+ stillAlive: new Map(),
+
+ wrap(promise, location) {
+ let id = String(getUniqueId());
+ let witness = lazy.finalizationService.make(
+ "extensions-onMessage-witness",
+ id
+ );
+
+ return new Promise((resolve, reject) => {
+ this.stillAlive.set(id, { reject, location });
+ promise.then(resolve, reject).finally(() => {
+ this.stillAlive.delete(id);
+ witness.forget();
+ });
+ });
+ },
+
+ observe(subject, topic, id) {
+ let message = "Promised response from onMessage listener went out of scope";
+ let { reject, location } = this.stillAlive.get(id);
+ reject(new ExtensionErrorHolder({ message, mozWebExtLocation: location }));
+ this.stillAlive.delete(id);
+ },
+};
+Services.obs.addObserver(StrongPromise, "extensions-onMessage-witness");
+
+// Simple single-event emitter-like helper, exposes the EventManager api.
+class SimpleEventAPI extends EventManager {
+ constructor(context, name) {
+ let fires = new Set();
+ let register = fire => {
+ fires.add(fire);
+ fire.location = context.getCaller();
+ return () => fires.delete(fire);
+ };
+ super({ context, name, register });
+ this.fires = fires;
+ }
+ emit(...args) {
+ return [...this.fires].map(fire => fire.asyncWithoutClone(...args));
+ }
+}
+
+// runtime.OnMessage event helper, handles custom async/sendResponse logic.
+class MessageEvent extends SimpleEventAPI {
+ emit(holder, sender) {
+ if (!this.fires.size || !this.context.active) {
+ return { received: false };
+ }
+
+ sender = Cu.cloneInto(sender, this.context.cloneScope);
+ let message = holder.deserialize(this.context.cloneScope);
+
+ let responses = [...this.fires]
+ .map(fire => this.wrapResponse(fire, message, sender))
+ .filter(x => x !== undefined);
+
+ return !responses.length
+ ? { received: true, response: false }
+ : Promise.race(responses).then(
+ value => ({ response: true, value }),
+ error => Promise.reject(this.unwrapOrSanitizeError(error))
+ );
+ }
+
+ unwrapOrSanitizeError(error) {
+ if (error instanceof ExtensionErrorHolder) {
+ return error.trustedErrorObject;
+ }
+ // If not a wrapped error, sanitize it and convert to ExtensionError, so
+ // that context.normalizeError will use the error message.
+ return new ExtensionError(error?.message ?? "An unexpected error occurred");
+ }
+
+ wrapResponse(fire, message, sender) {
+ let response, sendResponse;
+ let promise = new Promise(resolve => {
+ sendResponse = Cu.exportFunction(value => {
+ resolve(value);
+ response = promise;
+ }, this.context.cloneScope);
+ });
+
+ let result;
+ try {
+ result = fire.raw(message, sender, sendResponse);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ if (
+ result &&
+ typeof result === "object" &&
+ Cu.getClassName(result, true) === "Promise" &&
+ this.context.principal.subsumes(Cu.getObjectPrincipal(result))
+ ) {
+ return StrongPromise.wrap(result, fire.location);
+ } else if (result === true) {
+ return StrongPromise.wrap(promise, fire.location);
+ }
+ return response;
+ }
+}
+
+function holdMessage(name, anonymizedName, data, native = null) {
+ if (native && AppConstants.platform !== "android") {
+ data = lazy.NativeApp.encodeMessage(native.context, data);
+ }
+ return new StructuredCloneHolder(name, anonymizedName, data);
+}
+
+// Implements the runtime.Port extension API object.
+class Port {
+ /**
+ * @param {BaseContext} context The context that owns this port.
+ * @param {number} portId Uniquely identifies this port's channel.
+ * @param {string} name Arbitrary port name as defined by the addon.
+ * @param {boolean} native Is this a Port for native messaging.
+ * @param {object} sender The `Port.sender` property.
+ */
+ constructor(context, portId, name, native, sender) {
+ this.context = context;
+ this.name = name;
+ this.sender = sender;
+ this.holdMessage = native
+ ? (name, anonymizedName, data) =>
+ holdMessage(name, anonymizedName, data, this)
+ : holdMessage;
+ this.conduit = context.openConduit(this, {
+ portId,
+ native,
+ source: !sender,
+ recv: ["PortMessage", "PortDisconnect"],
+ send: ["PortMessage"],
+ });
+ this.initEventManagers();
+ }
+
+ initEventManagers() {
+ const { context } = this;
+ this.onMessage = new SimpleEventAPI(context, "Port.onMessage");
+ this.onDisconnect = new SimpleEventAPI(context, "Port.onDisconnect");
+ }
+
+ getAPI() {
+ // Public Port object handed to extensions from `connect()` and `onConnect`.
+ return {
+ name: this.name,
+ sender: this.sender,
+ error: null,
+ onMessage: this.onMessage.api(),
+ onDisconnect: this.onDisconnect.api(),
+ postMessage: this.sendPortMessage.bind(this),
+ disconnect: () => this.conduit.close(),
+ };
+ }
+
+ recvPortMessage({ holder }) {
+ this.onMessage.emit(holder.deserialize(this.api), this.api);
+ }
+
+ recvPortDisconnect({ error = null }) {
+ this.conduit.close();
+ if (this.context.active) {
+ this.api.error = error && this.context.normalizeError(error);
+ this.onDisconnect.emit(this.api);
+ }
+ }
+
+ sendPortMessage(json) {
+ if (this.conduit.actor) {
+ return this.conduit.sendPortMessage({
+ holder: this.holdMessage(
+ `Port/${this.context.extension.id}/sendPortMessage/${this.name}`,
+ `Port/${this.context.extension.id}/sendPortMessage/<anonymized>`,
+ json
+ ),
+ });
+ }
+ throw new this.context.Error("Attempt to postMessage on disconnected port");
+ }
+}
+
+defineLazyGetter(Port.prototype, "api", function () {
+ let api = this.getAPI();
+ return Cu.cloneInto(api, this.context.cloneScope, { cloneFunctions: true });
+});
+
+/**
+ * Each extension context gets its own Messenger object. It handles the
+ * basics of sendMessage, onMessage, connect and onConnect.
+ */
+class Messenger {
+ constructor(context) {
+ this.context = context;
+ this.conduit = context.openConduit(this, {
+ childId: context.childManager.id,
+ query: ["NativeMessage", "RuntimeMessage", "PortConnect"],
+ recv: ["RuntimeMessage", "PortConnect"],
+ });
+ this.initEventManagers();
+ }
+
+ initEventManagers() {
+ const { context } = this;
+ this.onConnect = new SimpleEventAPI(context, "runtime.onConnect");
+ this.onConnectEx = new SimpleEventAPI(context, "runtime.onConnectExternal");
+ this.onMessage = new MessageEvent(context, "runtime.onMessage");
+ this.onMessageEx = new MessageEvent(context, "runtime.onMessageExternal");
+ }
+
+ sendNativeMessage(nativeApp, json) {
+ let holder = holdMessage(
+ `Messenger/${this.context.extension.id}/sendNativeMessage/${nativeApp}`,
+ null,
+ json,
+ this
+ );
+ return this.conduit.queryNativeMessage({ nativeApp, holder });
+ }
+
+ sendRuntimeMessage({ extensionId, message, callback, ...args }) {
+ let response = this.conduit.queryRuntimeMessage({
+ extensionId: extensionId || this.context.extension.id,
+ holder: holdMessage(
+ `Messenger/${this.context.extension.id}/sendRuntimeMessage`,
+ null,
+ message
+ ),
+ ...args,
+ });
+ // If |response| is a rejected promise, the value will be sanitized by
+ // wrapPromise, according to the rules of context.normalizeError.
+ return this.context.wrapPromise(response, callback);
+ }
+
+ connect({ name, native, ...args }) {
+ let portId = getUniqueId();
+ let port = new Port(this.context, portId, name, !!native);
+ this.conduit
+ .queryPortConnect({ portId, name, native, ...args })
+ .catch(error => port.recvPortDisconnect({ error }));
+ return port.api;
+ }
+
+ recvPortConnect({ extensionId, portId, name, sender }) {
+ let event = sender.id === extensionId ? this.onConnect : this.onConnectEx;
+ if (this.context.active && event.fires.size) {
+ let port = new Port(this.context, portId, name, false, sender);
+ return event.emit(port.api).length;
+ }
+ }
+
+ recvRuntimeMessage({ extensionId, holder, sender }) {
+ let event = sender.id === extensionId ? this.onMessage : this.onMessageEx;
+ return event.emit(holder, sender);
+ }
+}
+
+// For test use only.
+var ExtensionManager = {
+ extensions: new Map(),
+};
+
+// Represents a browser extension in the content process.
+class BrowserExtensionContent extends EventEmitter {
+ constructor(policy) {
+ super();
+
+ this.policy = policy;
+ // Set a weak reference to this instance on the WebExtensionPolicy expando properties
+ // (because it makes it easier to reach the extension instance from the policy object
+ // without leaking it due to a circular dependency keeping it alive).
+ this.policy.weakExtension = Cu.getWeakReference(this);
+
+ this.instanceId = policy.instanceId;
+ this.optionalPermissions = policy.optionalPermissions;
+
+ if (WebExtensionPolicy.isExtensionProcess) {
+ // Keep in sync with serializeExtended in Extension.jsm
+ let ed = this.getSharedData("extendedData");
+ this.backgroundScripts = ed.backgroundScripts;
+ this.backgroundWorkerScript = ed.backgroundWorkerScript;
+ this.childModules = ed.childModules;
+ this.dependencies = ed.dependencies;
+ this.persistentBackground = ed.persistentBackground;
+ this.schemaURLs = ed.schemaURLs;
+ }
+
+ this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
+ Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+ this.apiManager = this.getAPIManager();
+
+ this._manifest = null;
+ this._localeData = null;
+
+ this.baseURI = Services.io.newURI(`moz-extension://${this.uuid}/`);
+ this.baseURL = this.baseURI.spec;
+
+ this.principal = Services.scriptSecurityManager.createContentPrincipal(
+ this.baseURI,
+ {}
+ );
+
+ // Only used in addon processes.
+ this.blockedParsingDocuments = new WeakSet();
+ this.views = new Set();
+
+ // Only used for devtools views.
+ this.devtoolsViews = new Set();
+
+ ExtensionManager.extensions.set(this.id, this);
+ }
+
+ get id() {
+ return this.policy.id;
+ }
+
+ get uuid() {
+ return this.policy.mozExtensionHostname;
+ }
+
+ get permissions() {
+ return new Set(this.policy.permissions);
+ }
+
+ get allowedOrigins() {
+ return this.policy.allowedOrigins;
+ }
+
+ getSharedData(key, value) {
+ return sharedData.get(`extension/${this.id}/${key}`);
+ }
+
+ get localeData() {
+ if (!this._localeData) {
+ this._localeData = new LocaleData(this.getSharedData("locales"));
+ }
+ return this._localeData;
+ }
+
+ get manifest() {
+ if (!this._manifest) {
+ this._manifest = this.getSharedData("manifest");
+ }
+ return this._manifest;
+ }
+
+ get manifestVersion() {
+ return this.manifest.manifest_version;
+ }
+
+ get privateBrowsingAllowed() {
+ return this.policy.privateBrowsingAllowed;
+ }
+
+ canAccessWindow(window) {
+ return this.policy.canAccessWindow(window);
+ }
+
+ getAPIManager() {
+ let apiManagers = [lazy.ExtensionPageChild.apiManager];
+
+ if (this.dependencies) {
+ for (let id of this.dependencies) {
+ let extension = lazy.ExtensionProcessScript.getExtensionChild(id);
+ if (extension) {
+ apiManagers.push(extension.experimentAPIManager);
+ }
+ }
+ }
+
+ if (this.childModules) {
+ this.experimentAPIManager = new ExtensionCommon.LazyAPIManager(
+ "addon",
+ this.childModules,
+ this.schemaURLs
+ );
+
+ apiManagers.push(this.experimentAPIManager);
+ }
+
+ if (apiManagers.length == 1) {
+ return apiManagers[0];
+ }
+
+ return new ExtensionCommon.MultiAPIManager("addon", apiManagers.reverse());
+ }
+
+ shutdown() {
+ ExtensionManager.extensions.delete(this.id);
+ lazy.ExtensionContent.shutdownExtension(this);
+ Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+ this.emit("shutdown");
+ }
+
+ getContext(window) {
+ return lazy.ExtensionContent.getContext(this, window);
+ }
+
+ emit(event, ...args) {
+ Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, { event, args });
+ super.emit(event, ...args);
+ }
+
+ // TODO(Bug 1768471): consider folding this back into emit if we will change it to
+ // return a value as EventEmitter and Extension emit methods do.
+ emitLocalWithResult(event, ...args) {
+ return super.emit(event, ...args);
+ }
+
+ receiveMessage({ name, data }) {
+ if (name === this.MESSAGE_EMIT_EVENT) {
+ super.emit(data.event, ...data.args);
+ }
+ }
+
+ localizeMessage(...args) {
+ return this.localeData.localizeMessage(...args);
+ }
+
+ localize(...args) {
+ return this.localeData.localize(...args);
+ }
+
+ hasPermission(perm) {
+ // If the permission is a "manifest property" permission, we check if the extension
+ // does have the required property in its manifest.
+ let manifest_ = "manifest:";
+ if (perm.startsWith(manifest_)) {
+ // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
+ let value = this.manifest;
+ for (let prop of perm.substr(manifest_.length).split(".")) {
+ if (!value) {
+ break;
+ }
+ value = value[prop];
+ }
+
+ return value != null;
+ }
+ return this.permissions.has(perm);
+ }
+
+ trackBlockedParsingDocument(doc) {
+ this.blockedParsingDocuments.add(doc);
+ }
+
+ untrackBlockedParsingDocument(doc) {
+ this.blockedParsingDocuments.delete(doc);
+ }
+
+ hasContextBlockedParsingDocument(extContext) {
+ return this.blockedParsingDocuments.has(extContext.contentWindow?.document);
+ }
+}
+
+/**
+ * An object that runs an remote implementation of an API.
+ */
+class ProxyAPIImplementation extends SchemaAPIInterface {
+ /**
+ * @param {string} namespace The full path to the namespace that contains the
+ * `name` member. This may contain dots, e.g. "storage.local".
+ * @param {string} name The name of the method or property.
+ * @param {ChildAPIManager} childApiManager The owner of this implementation.
+ * @param {boolean} alreadyLogged Whether the child already logged the event.
+ */
+ constructor(namespace, name, childApiManager, alreadyLogged = false) {
+ super();
+ this.path = `${namespace}.${name}`;
+ this.childApiManager = childApiManager;
+ this.alreadyLogged = alreadyLogged;
+ }
+
+ revoke() {
+ let map = this.childApiManager.listeners.get(this.path);
+ for (let listener of map.listeners.keys()) {
+ this.removeListener(listener);
+ }
+
+ this.path = null;
+ this.childApiManager = null;
+ }
+
+ callFunctionNoReturn(args) {
+ this.childApiManager.callParentFunctionNoReturn(this.path, args);
+ }
+
+ callAsyncFunction(args, callback, requireUserInput) {
+ const context = this.childApiManager.context;
+ const isHandlingUserInput =
+ context.contentWindow?.windowUtils?.isHandlingUserInput;
+ if (requireUserInput) {
+ if (!isHandlingUserInput) {
+ let err = new context.cloneScope.Error(
+ `${this.path} may only be called from a user input handler`
+ );
+ return context.wrapPromise(Promise.reject(err), callback);
+ }
+ }
+ return this.childApiManager.callParentAsyncFunction(
+ this.path,
+ args,
+ callback,
+ {
+ alreadyLogged: this.alreadyLogged,
+ isHandlingUserInput,
+ }
+ );
+ }
+
+ addListener(listener, args) {
+ let map = this.childApiManager.listeners.get(this.path);
+
+ if (map.listeners.has(listener)) {
+ // TODO: Called with different args?
+ return;
+ }
+
+ let id = getUniqueId();
+
+ map.ids.set(id, listener);
+ map.listeners.set(listener, id);
+
+ this.childApiManager.conduit.sendAddListener({
+ childId: this.childApiManager.id,
+ listenerId: id,
+ path: this.path,
+ args,
+ alreadyLogged: this.alreadyLogged,
+ });
+ }
+
+ removeListener(listener) {
+ let map = this.childApiManager.listeners.get(this.path);
+
+ if (!map.listeners.has(listener)) {
+ return;
+ }
+
+ let id = map.listeners.get(listener);
+ map.listeners.delete(listener);
+ map.ids.delete(id);
+ map.removedIds.add(id);
+
+ this.childApiManager.conduit.sendRemoveListener({
+ childId: this.childApiManager.id,
+ listenerId: id,
+ path: this.path,
+ alreadyLogged: this.alreadyLogged,
+ });
+ }
+
+ hasListener(listener) {
+ let map = this.childApiManager.listeners.get(this.path);
+ return map.listeners.has(listener);
+ }
+}
+
+class ChildLocalAPIImplementation extends LocalAPIImplementation {
+ constructor(pathObj, namespace, name, childApiManager) {
+ super(pathObj, name, childApiManager.context);
+ this.childApiManagerId = childApiManager.id;
+ this.fullname = `${namespace}.${name}`;
+ }
+
+ /**
+ * Call the given function and also log the call as appropriate
+ * (i.e., with PerformanceCounters and/or activity logging)
+ *
+ * @param {Function} callable The actual implementation to invoke.
+ * @param {Array} args Arguments to the function call.
+ * @returns {any} The return result of callable.
+ */
+ callAndLog(callable, args) {
+ this.context.logActivity("api_call", this.fullname, { args });
+ let start = Cu.now();
+ try {
+ return callable();
+ } finally {
+ ChromeUtils.addProfilerMarker(
+ "ExtensionChild",
+ { startTime: start },
+ `${this.context.extension.id}, api_call: ${this.fullname}`
+ );
+ if (lazy.gTimingEnabled) {
+ let end = Cu.now() * 1000;
+ lazy.PerformanceCounters.storeExecutionTime(
+ this.context.extension.id,
+ this.name,
+ end - start * 1000,
+ this.childApiManagerId
+ );
+ }
+ }
+ }
+
+ callFunction(args) {
+ return this.callAndLog(() => super.callFunction(args), args);
+ }
+
+ callFunctionNoReturn(args) {
+ return this.callAndLog(() => super.callFunctionNoReturn(args), args);
+ }
+
+ callAsyncFunction(args, callback, requireUserInput) {
+ return this.callAndLog(
+ () => super.callAsyncFunction(args, callback, requireUserInput),
+ args
+ );
+ }
+}
+
+// We create one instance of this class for every extension context that
+// needs to use remote APIs. It uses the the JSWindowActor and
+// JSProcessActor Conduits actors (see ConduitsChild.jsm) to communicate
+// with the ParentAPIManager singleton in ExtensionParent.jsm.
+// It handles asynchronous function calls as well as event listeners.
+class ChildAPIManager {
+ constructor(context, messageManager, localAPICan, contextData) {
+ this.context = context;
+ this.messageManager = messageManager;
+ this.url = contextData.url;
+
+ // The root namespace of all locally implemented APIs. If an extension calls
+ // an API that does not exist in this object, then the implementation is
+ // delegated to the ParentAPIManager.
+ this.localApis = localAPICan.root;
+ this.apiCan = localAPICan;
+ this.schema = this.apiCan.apiManager.schema;
+
+ this.id = `${context.extension.id}.${context.contextId}`;
+
+ this.conduit = context.openConduit(this, {
+ childId: this.id,
+ send: [
+ "CreateProxyContext",
+ "ContextLoaded",
+ "APICall",
+ "AddListener",
+ "RemoveListener",
+ ],
+ recv: ["CallResult", "RunListener", "StreamFilterSuspendCancel"],
+ });
+
+ this.conduit.sendCreateProxyContext({
+ childId: this.id,
+ extensionId: context.extension.id,
+ principal: context.principal,
+ ...contextData,
+ });
+
+ this.listeners = new DefaultMap(() => ({
+ ids: new Map(),
+ listeners: new Map(),
+ removedIds: new LimitedSet(10),
+ }));
+
+ // Map[callId -> Deferred]
+ this.callPromises = new Map();
+
+ this.permissionsChangedCallbacks = new Set();
+ this.updatePermissions = null;
+ if (this.context.extension.optionalPermissions.length) {
+ this.updatePermissions = () => {
+ for (let callback of this.permissionsChangedCallbacks) {
+ try {
+ callback();
+ } catch (err) {
+ Cu.reportError(err);
+ }
+ }
+ };
+ this.context.extension.on("update-permissions", this.updatePermissions);
+ }
+ }
+
+ inject(obj) {
+ this.schema.inject(obj, this);
+ }
+
+ recvCallResult(data) {
+ let deferred = this.callPromises.get(data.callId);
+ this.callPromises.delete(data.callId);
+ if ("error" in data) {
+ deferred.reject(data.error);
+ } else {
+ let result = data.result.deserialize(this.context.cloneScope);
+
+ deferred.resolve(new NoCloneSpreadArgs(result));
+ }
+ }
+
+ recvRunListener(data) {
+ let map = this.listeners.get(data.path);
+ let listener = map.ids.get(data.listenerId);
+
+ if (listener) {
+ if (!this.context.active) {
+ Services.console.logStringMessage(
+ `Ignored listener for inactive context at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n`
+ );
+ return;
+ }
+
+ let args = data.args.deserialize(this.context.cloneScope);
+ let fire = () => this.context.applySafeWithoutClone(listener, args);
+ return Promise.resolve(
+ data.handlingUserInput
+ ? withHandlingUserInput(this.context.contentWindow, fire)
+ : fire()
+ ).then(result => {
+ if (result !== undefined) {
+ return new StructuredCloneHolder(
+ `ChildAPIManager/${this.context.extension.id}/${data.path}`,
+ null,
+ result,
+ this.context.cloneScope
+ );
+ }
+ return result;
+ });
+ }
+ if (!map.removedIds.has(data.listenerId)) {
+ Services.console.logStringMessage(
+ `Unknown listener at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n`
+ );
+ }
+ }
+
+ async recvStreamFilterSuspendCancel() {
+ const promise = this.context.extension.emitLocalWithResult(
+ "internal:stream-filter-suspend-cancel"
+ );
+ // if all listeners throws emitLocalWithResult returns undefined.
+ if (!promise) {
+ return false;
+ }
+
+ return promise.then(results =>
+ results.some(hasActiveStreamFilter => hasActiveStreamFilter === true)
+ );
+ }
+
+ /**
+ * Call a function in the parent process and ignores its return value.
+ *
+ * @param {string} path The full name of the method, e.g. "tabs.create".
+ * @param {Array} args The parameters for the function.
+ */
+ callParentFunctionNoReturn(path, args) {
+ this.conduit.sendAPICall({ childId: this.id, path, args });
+ }
+
+ /**
+ * Calls a function in the parent process and returns its result
+ * asynchronously.
+ *
+ * @param {string} path The full name of the method, e.g. "tabs.create".
+ * @param {Array} args The parameters for the function.
+ * @param {function(*)} [callback] The callback to be called when the function
+ * completes.
+ * @param {object} [options] Extra options.
+ * @returns {Promise|undefined} Must be void if `callback` is set, and a
+ * promise otherwise. The promise is resolved when the function completes.
+ */
+ callParentAsyncFunction(path, args, callback, options = {}) {
+ let callId = getUniqueId();
+ let deferred = lazy.PromiseUtils.defer();
+ this.callPromises.set(callId, deferred);
+
+ let {
+ // Any child api that calls into a parent function will have already
+ // logged the api_call. Flag it so the parent doesn't log again.
+ alreadyLogged = true,
+ // Propagating the isHAndlingUserInput flag to the API call handler
+ // executed on the parent process side.
+ isHandlingUserInput = false,
+ } = options;
+
+ // TODO: conduit.queryAPICall()
+ this.conduit.sendAPICall({
+ childId: this.id,
+ callId,
+ path,
+ args,
+ options: { alreadyLogged, isHandlingUserInput },
+ });
+ return this.context.wrapPromise(deferred.promise, callback);
+ }
+
+ /**
+ * Create a proxy for an event in the parent process. The returned event
+ * object shares its internal state with other instances. For instance, if
+ * `removeListener` is used on a listener that was added on another object
+ * through `addListener`, then the event is unregistered.
+ *
+ * @param {string} path The full name of the event, e.g. "tabs.onCreated".
+ * @returns {object} An object with the addListener, removeListener and
+ * hasListener methods. See SchemaAPIInterface for documentation.
+ */
+ getParentEvent(path) {
+ path = path.split(".");
+
+ let name = path.pop();
+ let namespace = path.join(".");
+
+ let impl = new ProxyAPIImplementation(namespace, name, this, true);
+ return {
+ addListener: (listener, ...args) => impl.addListener(listener, args),
+ removeListener: listener => impl.removeListener(listener),
+ hasListener: listener => impl.hasListener(listener),
+ };
+ }
+
+ close() {
+ // Reports CONDUIT_CLOSED on the parent side.
+ this.conduit.close();
+
+ if (this.updatePermissions) {
+ this.context.extension.off("update-permissions", this.updatePermissions);
+ }
+ }
+
+ get cloneScope() {
+ return this.context.cloneScope;
+ }
+
+ get principal() {
+ return this.context.principal;
+ }
+
+ get manifestVersion() {
+ return this.context.manifestVersion;
+ }
+
+ shouldInject(namespace, name, allowedContexts) {
+ // Do not generate content script APIs, unless explicitly allowed.
+ if (
+ this.context.envType === "content_child" &&
+ !allowedContexts.includes("content")
+ ) {
+ return false;
+ }
+
+ // Do not generate devtools APIs, unless explicitly allowed.
+ if (
+ this.context.envType === "devtools_child" &&
+ !allowedContexts.includes("devtools")
+ ) {
+ return false;
+ }
+
+ // Do not generate devtools APIs, unless explicitly allowed.
+ if (
+ this.context.envType !== "devtools_child" &&
+ allowedContexts.includes("devtools_only")
+ ) {
+ return false;
+ }
+
+ // Do not generate content_only APIs, unless explicitly allowed.
+ if (
+ this.context.envType !== "content_child" &&
+ allowedContexts.includes("content_only")
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ getImplementation(namespace, name) {
+ this.apiCan.findAPIPath(`${namespace}.${name}`);
+ let obj = this.apiCan.findAPIPath(namespace);
+
+ if (obj && name in obj) {
+ return new ChildLocalAPIImplementation(obj, namespace, name, this);
+ }
+
+ return this.getFallbackImplementation(namespace, name);
+ }
+
+ getFallbackImplementation(namespace, name) {
+ // No local API found, defer implementation to the parent.
+ return new ProxyAPIImplementation(namespace, name, this);
+ }
+
+ hasPermission(permission) {
+ return this.context.extension.hasPermission(permission);
+ }
+
+ isPermissionRevokable(permission) {
+ return this.context.extension.optionalPermissions.includes(permission);
+ }
+
+ setPermissionsChangedCallback(callback) {
+ this.permissionsChangedCallbacks.add(callback);
+ }
+}
+
+export var ExtensionChild = {
+ BrowserExtensionContent,
+ ChildAPIManager,
+ ChildLocalAPIImplementation,
+ MessageEvent,
+ Messenger,
+ Port,
+ ProxyAPIImplementation,
+ SimpleEventAPI,
+};