diff options
Diffstat (limited to 'toolkit/components/extensions/ExtensionChild.sys.mjs')
-rw-r--r-- | toolkit/components/extensions/ExtensionChild.sys.mjs | 1040 |
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, +}; |