diff options
Diffstat (limited to 'toolkit/components/extensions/ExtensionProcessScript.sys.mjs')
-rw-r--r-- | toolkit/components/extensions/ExtensionProcessScript.sys.mjs | 525 |
1 files changed, 525 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ExtensionProcessScript.sys.mjs b/toolkit/components/extensions/ExtensionProcessScript.sys.mjs new file mode 100644 index 0000000000..2fcf113a88 --- /dev/null +++ b/toolkit/components/extensions/ExtensionProcessScript.sys.mjs @@ -0,0 +1,525 @@ +/* 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 script contains the minimum, skeleton content process code that we need + * in order to lazily load other extension modules when they are first + * necessary. Anything which is not likely to be needed immediately, or shortly + * after startup, in *every* browser process live outside of this file. + */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionChild: "resource://gre/modules/ExtensionChild.sys.mjs", + ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs", + ExtensionContent: "resource://gre/modules/ExtensionContent.sys.mjs", + ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.sys.mjs", + ExtensionWorkerChild: "resource://gre/modules/ExtensionWorkerChild.sys.mjs", + Schemas: "resource://gre/modules/Schemas.sys.mjs", +}); + +import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; + +const { DefaultWeakMap } = ExtensionUtils; + +const { sharedData } = Services.cpmm; + +function getData(extension, key = "") { + return sharedData.get(`extension/${extension.id}/${key}`); +} + +// We need to avoid touching Services.appinfo here in order to prevent +// the wrong version from being cached during xpcshell test startup. +// eslint-disable-next-line mozilla/use-services +ChromeUtils.defineLazyGetter(lazy, "isContentProcess", () => { + return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; +}); + +ChromeUtils.defineLazyGetter(lazy, "isContentScriptProcess", () => { + return ( + lazy.isContentProcess || + !WebExtensionPolicy.useRemoteWebExtensions || + // Thunderbird still loads some content in the parent process. + AppConstants.MOZ_APP_NAME == "thunderbird" + ); +}); + +var extensions = new DefaultWeakMap(policy => { + return new lazy.ExtensionChild.BrowserExtensionContent(policy); +}); + +var pendingExtensions = new Map(); + +var ExtensionManager; + +ExtensionManager = { + // WeakMap<WebExtensionPolicy, Map<number, WebExtensionContentScript>> + registeredContentScripts: new DefaultWeakMap(policy => new Map()), + + init() { + Services.cpmm.addMessageListener("Extension:Startup", this); + Services.cpmm.addMessageListener("Extension:Shutdown", this); + Services.cpmm.addMessageListener("Extension:FlushJarCache", this); + Services.cpmm.addMessageListener("Extension:RegisterContentScripts", this); + Services.cpmm.addMessageListener( + "Extension:UnregisterContentScripts", + this + ); + Services.cpmm.addMessageListener("Extension:UpdateContentScripts", this); + Services.cpmm.addMessageListener("Extension:UpdatePermissions", this); + Services.cpmm.addMessageListener("Extension:UpdateIgnoreQuarantine", this); + + this.updateStubExtensions(); + + for (let id of sharedData.get("extensions/activeIDs") || []) { + this.initExtension(getData({ id })); + } + }, + + initStubPolicy(id, data) { + let resolveReadyPromise; + let readyPromise = new Promise(resolve => { + resolveReadyPromise = resolve; + }); + + let policy = new WebExtensionPolicy({ + id, + localizeCallback() {}, + readyPromise, + allowedOrigins: new MatchPatternSet([]), + ...data, + }); + + try { + policy.active = true; + + pendingExtensions.set(id, { policy, resolveReadyPromise }); + } catch (e) { + Cu.reportError(e); + } + }, + + updateStubExtensions() { + for (let [id, data] of sharedData.get("extensions/pending") || []) { + if (!pendingExtensions.has(id)) { + this.initStubPolicy(id, data); + } + } + }, + + initExtensionPolicy(extension) { + let policy = WebExtensionPolicy.getByID(extension.id); + if (!policy || pendingExtensions.has(extension.id)) { + let localizeCallback; + if (extension.localize) { + // We have a real Extension object. + localizeCallback = extension.localize.bind(extension); + } else { + // We have serialized extension data; + localizeCallback = str => extensions.get(policy).localize(str); + } + + let { backgroundScripts } = extension; + if (!backgroundScripts && WebExtensionPolicy.isExtensionProcess) { + ({ backgroundScripts } = getData(extension, "extendedData") || {}); + } + + let { backgroundWorkerScript } = extension; + if (!backgroundWorkerScript && WebExtensionPolicy.isExtensionProcess) { + ({ backgroundWorkerScript } = getData(extension, "extendedData") || {}); + } + + let { backgroundTypeModule } = extension; + if ( + backgroundTypeModule == null && + WebExtensionPolicy.isExtensionProcess + ) { + ({ backgroundTypeModule } = getData(extension, "extendedData") || {}); + } + + policy = new WebExtensionPolicy({ + id: extension.id, + mozExtensionHostname: extension.uuid, + name: extension.name, + type: extension.type, + baseURL: extension.resourceURL, + + isPrivileged: extension.isPrivileged, + ignoreQuarantine: extension.ignoreQuarantine, + temporarilyInstalled: extension.temporarilyInstalled, + permissions: extension.permissions, + allowedOrigins: extension.allowedOrigins, + webAccessibleResources: extension.webAccessibleResources, + + manifestVersion: extension.manifestVersion, + extensionPageCSP: extension.extensionPageCSP, + + localizeCallback, + + backgroundScripts, + backgroundWorkerScript, + backgroundTypeModule, + + contentScripts: extension.contentScripts, + }); + + policy.debugName = `${JSON.stringify(policy.name)} (ID: ${ + policy.id + }, ${policy.getURL()})`; + + // Register any existent dynamically registered content script for the extension + // when a content process is started for the first time (which also cover + // a content process that crashed and it has been recreated). + const registeredContentScripts = + this.registeredContentScripts.get(policy); + + for (let [scriptId, options] of getData(extension, "contentScripts") || + []) { + const script = new WebExtensionContentScript(policy, options); + + // If the script is a userScript, add the additional userScriptOptions + // property to the WebExtensionContentScript instance. + if ("userScriptOptions" in options) { + script.userScriptOptions = options.userScriptOptions; + } + + policy.registerContentScript(script); + registeredContentScripts.set(scriptId, script); + } + + let stub = pendingExtensions.get(extension.id); + if (stub) { + pendingExtensions.delete(extension.id); + stub.policy.active = false; + stub.resolveReadyPromise(policy); + } + + policy.active = true; + policy.instanceId = extension.instanceId; + policy.optionalPermissions = extension.optionalPermissions; + } + return policy; + }, + + initExtension(data) { + if (typeof data === "string") { + data = getData({ id: data }); + } + let policy = this.initExtensionPolicy(data); + + policy.injectContentScripts(); + }, + + handleEvent(event) { + if ( + event.type === "change" && + event.changedKeys.includes("extensions/pending") + ) { + this.updateStubExtensions(); + } + }, + + receiveMessage({ name, data }) { + try { + switch (name) { + case "Extension:Startup": + this.initExtension(data); + break; + + case "Extension:Shutdown": { + let policy = WebExtensionPolicy.getByID(data.id); + if (policy) { + if (extensions.has(policy)) { + extensions.get(policy).shutdown(); + } + + if (lazy.isContentProcess) { + policy.active = false; + } + } + break; + } + + case "Extension:FlushJarCache": + ExtensionUtils.flushJarCache(data.path); + break; + + case "Extension:RegisterContentScripts": { + let policy = WebExtensionPolicy.getByID(data.id); + + if (policy) { + const registeredContentScripts = + this.registeredContentScripts.get(policy); + + for (const { scriptId, options } of data.scripts) { + const type = + "userScriptOptions" in options ? "userScript" : "contentScript"; + + if (registeredContentScripts.has(scriptId)) { + Cu.reportError( + new Error( + `Registering ${type} ${scriptId} on ${data.id} more than once` + ) + ); + } else { + const script = new WebExtensionContentScript(policy, options); + + // If the script is a userScript, add the additional + // userScriptOptions property to the WebExtensionContentScript + // instance. + if (type === "userScript") { + script.userScriptOptions = options.userScriptOptions; + } + + policy.registerContentScript(script); + registeredContentScripts.set(scriptId, script); + } + } + } + break; + } + + case "Extension:UnregisterContentScripts": { + let policy = WebExtensionPolicy.getByID(data.id); + + if (policy) { + const registeredContentScripts = + this.registeredContentScripts.get(policy); + + for (const scriptId of data.scriptIds) { + const script = registeredContentScripts.get(scriptId); + if (script) { + policy.unregisterContentScript(script); + registeredContentScripts.delete(scriptId); + } + } + } + break; + } + + case "Extension:UpdateContentScripts": { + let policy = WebExtensionPolicy.getByID(data.id); + + if (policy) { + const registeredContentScripts = + this.registeredContentScripts.get(policy); + + for (const { scriptId, options } of data.scripts) { + const oldScript = registeredContentScripts.get(scriptId); + const newScript = new WebExtensionContentScript(policy, options); + + policy.unregisterContentScript(oldScript); + policy.registerContentScript(newScript); + registeredContentScripts.set(scriptId, newScript); + } + } + break; + } + + case "Extension:UpdatePermissions": { + let policy = WebExtensionPolicy.getByID(data.id); + if (!policy) { + break; + } + // In the parent process, Extension.jsm updates the policy. + if (lazy.isContentProcess) { + lazy.ExtensionCommon.updateAllowedOrigins( + policy, + data.origins, + data.add + ); + + if (data.permissions.length) { + let perms = new Set(policy.permissions); + for (let perm of data.permissions) { + if (data.add) { + perms.add(perm); + } else { + perms.delete(perm); + } + } + policy.permissions = perms; + } + } + + if (data.permissions.length && extensions.has(policy)) { + // Notify ChildApiManager of permission changes. + extensions.get(policy).emit("update-permissions"); + } + break; + } + + case "Extension:UpdateIgnoreQuarantine": { + let policy = WebExtensionPolicy.getByID(data.id); + if (policy?.active) { + policy.ignoreQuarantine = data.ignoreQuarantine; + } + break; + } + } + } catch (e) { + Cu.reportError(e); + } + Services.cpmm.sendAsyncMessage(`${name}Complete`); + }, +}; + +export var ExtensionProcessScript = { + extensions, + + initExtension(extension) { + return ExtensionManager.initExtensionPolicy(extension); + }, + + initExtensionDocument(policy, doc, privileged) { + let extension = extensions.get(policy); + if (privileged) { + lazy.ExtensionPageChild.initExtensionContext(extension, doc.defaultView); + } else { + lazy.ExtensionContent.initExtensionContext(extension, doc.defaultView); + } + }, + + getExtensionChild(id) { + let policy = WebExtensionPolicy.getByID(id); + if (policy) { + return extensions.get(policy); + } + }, + + preloadContentScript(contentScript) { + if (lazy.isContentScriptProcess) { + lazy.ExtensionContent.contentScripts.get(contentScript).preload(); + } + }, + + loadContentScript(contentScript, window) { + return lazy.ExtensionContent.contentScripts + .get(contentScript) + .injectInto(window); + }, +}; + +export var ExtensionAPIRequestHandler = { + initExtensionWorker(policy, serviceWorkerInfo) { + let extension = extensions.get(policy); + + if (!extension) { + throw new Error(`Extension instance not found for addon ${policy.id}`); + } + + lazy.ExtensionWorkerChild.initExtensionWorkerContext( + extension, + serviceWorkerInfo + ); + }, + + onExtensionWorkerLoaded(policy, serviceWorkerDescriptorId) { + lazy.ExtensionWorkerChild.notifyExtensionWorkerContextLoaded( + serviceWorkerDescriptorId, + policy + ); + }, + + onExtensionWorkerDestroyed(policy, serviceWorkerDescriptorId) { + lazy.ExtensionWorkerChild.destroyExtensionWorkerContext( + serviceWorkerDescriptorId + ); + }, + + handleAPIRequest(policy, request) { + let context; + + try { + let extension = extensions.get(policy); + + if (!extension) { + throw new Error(`Extension instance not found for addon ${policy.id}`); + } + + context = this.getExtensionContextForAPIRequest({ + extension, + request, + }); + + if (!context) { + throw new Error( + `Extension context not found for API request: ${request}` + ); + } + + // Add a property to the request object for the normalizedArgs. + request.normalizedArgs = this.validateAndNormalizeRequestArgs({ + context, + request, + }); + + return context.childManager.handleWebIDLAPIRequest(request); + } catch (error) { + // Propagate errors related to parameter validation when the error object + // belongs to the extension context that initiated the call. + if (context?.Error && error instanceof context.Error) { + return { + type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR, + value: error, + }; + } + // Do not propagate errors that are not meant to be accessible to the + // extension, report it to the console and just throw the generic + // "An unexpected error occurred". + Cu.reportError(error); + return { + type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR, + value: new Error("An unexpected error occurred"), + }; + } + }, + + getExtensionContextForAPIRequest({ extension, request }) { + if (request.serviceWorkerInfo) { + return lazy.ExtensionWorkerChild.getExtensionWorkerContext( + extension, + request.serviceWorkerInfo + ); + } + + return null; + }, + + validateAndNormalizeRequestArgs({ context, request }) { + if ( + !lazy.Schemas.checkPermissions(request.apiNamespace, context.extension) + ) { + throw new context.Error( + `Not enough privileges to access ${request.apiNamespace}` + ); + } + if (request.requestType === "getProperty") { + return []; + } + + if (request.apiObjectType) { + // skip parameter validation on request targeting an api object, + // even the JS-based implementation of the API objects are not + // going through the same kind of Schema based validation that + // the API namespaces methods and events go through. + // + // TODO(Bug 1728535): validate and normalize also this request arguments + // as a low priority follow up. + return request.args; + } + + // Validate and normalize parameters, set the normalized args on the + // mozIExtensionAPIRequest normalizedArgs property. + return lazy.Schemas.checkWebIDLRequestParameters( + context.childManager, + request + ); + }, +}; + +ExtensionManager.init(); |