/* 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 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. */ var EXPORTED_SYMBOLS = ["ExtensionProcessScript"]; const { MessageChannel } = ChromeUtils.import( "resource://gre/modules/MessageChannel.jsm" ); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); XPCOMUtils.defineLazyModuleGetters(this, { AppConstants: "resource://gre/modules/AppConstants.jsm", ExtensionChild: "resource://gre/modules/ExtensionChild.jsm", ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm", ExtensionContent: "resource://gre/modules/ExtensionContent.jsm", ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm", }); const { ExtensionUtils } = ChromeUtils.import( "resource://gre/modules/ExtensionUtils.jsm" ); XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole() ); const { DefaultWeakMap, getInnerWindowID } = 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 XPCOMUtils.defineLazyGetter(this, "isContentProcess", () => { return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; }); XPCOMUtils.defineLazyGetter(this, "isContentScriptProcess", () => { return ( isContentProcess || !WebExtensionPolicy.useRemoteWebExtensions || // Thunderbird still loads some content in the parent process. AppConstants.MOZ_APP_NAME == "thunderbird" ); }); var extensions = new DefaultWeakMap(policy => { return new ExtensionChild.BrowserExtensionContent(policy); }); var pendingExtensions = new Map(); var ExtensionManager; class ExtensionGlobal { constructor(global) { this.global = global; this.global.addMessageListener("Extension:SetFrameData", this); this.frameData = null; MessageChannel.addListener(global, "Extension:DetectLanguage", this); MessageChannel.addListener(global, "WebNavigation:GetFrame", this); MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this); } get messageFilterStrict() { return { innerWindowID: getInnerWindowID(this.global.content), }; } getFrameData(force = false) { if (!this.frameData && force) { this.frameData = this.global.sendSyncMessage( "Extension:GetTabAndWindowId" )[0]; } return this.frameData; } receiveMessage({ target, messageName, recipient, data, name }) { switch (name) { case "Extension:SetFrameData": if (this.frameData) { Object.assign(this.frameData, data); } else { this.frameData = data; } if (data.viewType && WebExtensionPolicy.isExtensionProcess) { ExtensionPageChild.expectViewLoad(this.global, data.viewType); } return; } // SetFrameData does not have a recipient extension, or it would be // an extension process. Anything following this point must have // a recipient extension, so check access to the window. let policy = WebExtensionPolicy.getByID(recipient.extensionId); if (!policy.canAccessWindow(this.global.content)) { throw new Error("Extension cannot access window"); } return ExtensionContent.receiveMessage( this.global, messageName, target, data, recipient ); } } ExtensionManager = { // WeakMap> registeredContentScripts: new DefaultWeakMap(policy => new Map()), globals: new WeakMap(), init() { MessageChannel.setupMessageManagers([Services.cpmm]); Services.cpmm.addMessageListener("Extension:Startup", this); Services.cpmm.addMessageListener("Extension:Shutdown", this); Services.cpmm.addMessageListener("Extension:FlushJarCache", this); Services.cpmm.addMessageListener("Extension:RegisterContentScript", this); Services.cpmm.addMessageListener( "Extension:UnregisterContentScripts", this ); // eslint-disable-next-line mozilla/balanced-listeners Services.obs.addObserver( global => this.globals.set(global, new ExtensionGlobal(global)), "tab-content-frameloader-created" ); 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") || {}); } policy = new WebExtensionPolicy({ id: extension.id, mozExtensionHostname: extension.uuid, name: extension.name, baseURL: extension.resourceURL, isPrivileged: extension.isPrivileged, permissions: extension.permissions, allowedOrigins: extension.allowedOrigins, webAccessibleResources: extension.webAccessibleResources, manifestVersion: extension.manifestVersion, extensionPageCSP: extension.extensionPageCSP, localizeCallback, backgroundScripts, backgroundWorkerScript, 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 (isContentProcess) { policy.active = false; } } break; } case "Extension:FlushJarCache": ExtensionUtils.flushJarCache(data.path); break; case "Extension:RegisterContentScript": { let policy = WebExtensionPolicy.getByID(data.id); if (policy) { const registeredContentScripts = this.registeredContentScripts.get( policy ); const type = "userScriptOptions" in data.options ? "userScript" : "contentScript"; if (registeredContentScripts.has(data.scriptId)) { Cu.reportError( new Error( `Registering ${type} ${data.scriptId} on ${data.id} more than once` ) ); } else { const script = new WebExtensionContentScript( policy, data.options ); // If the script is a userScript, add the additional userScriptOptions // property to the WebExtensionContentScript instance. if (type === "userScript") { script.userScriptOptions = data.options.userScriptOptions; } policy.registerContentScript(script); registeredContentScripts.set(data.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; } } } catch (e) { Cu.reportError(e); } Services.cpmm.sendAsyncMessage(`${name}Complete`); }, }; var ExtensionProcessScript = { extensions, getFrameData(global, force) { let extGlobal = ExtensionManager.globals.get(global); return extGlobal && extGlobal.getFrameData(force); }, initExtension(extension) { return ExtensionManager.initExtensionPolicy(extension); }, initExtensionDocument(policy, doc, privileged) { let extension = extensions.get(policy); if (privileged) { ExtensionPageChild.initExtensionContext(extension, doc.defaultView); } else { ExtensionContent.initExtensionContext(extension, doc.defaultView); } }, getExtensionChild(id) { let policy = WebExtensionPolicy.getByID(id); if (policy) { return extensions.get(policy); } }, preloadContentScript(contentScript) { if (isContentScriptProcess) { ExtensionContent.contentScripts.get(contentScript).preload(); } }, loadContentScript(contentScript, window) { return ExtensionContent.contentScripts .get(contentScript) .injectInto(window); }, }; ExtensionManager.init();