/* 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. */ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/MessageChannel.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { ExtensionChild: "resource://gre/modules/ExtensionChild.jsm", ExtensionContent: "resource://gre/modules/ExtensionContent.jsm", ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm", }); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole()); const { DefaultWeakMap, getInnerWindowID, } = ExtensionUtils; // We need to avoid touching Services.appinfo here in order to prevent // the wrong version from being cached during xpcshell test startup. const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT; function parseScriptOptions(options) { return { allFrames: options.all_frames, matchAboutBlank: options.match_about_blank, frameID: options.frame_id, runAt: options.run_at, matches: new MatchPatternSet(options.matches), excludeMatches: new MatchPatternSet(options.exclude_matches || []), includeGlobs: options.include_globs && options.include_globs.map(glob => new MatchGlob(glob)), excludeGlobs: options.exclude_globs && options.exclude_globs.map(glob => new MatchGlob(glob)), jsPaths: options.js || [], cssPaths: options.css || [], }; } var extensions = new DefaultWeakMap(policy => { let data = policy.initData; if (data.serialize) { // We have an actual Extension rather than serialized extension // data, so serialize it now to make sure we have consistent inputs // between parent and child processes. data = data.serialize(); } let extension = new ExtensionChild.BrowserExtensionContent(data); extension.policy = policy; return extension; }); var contentScripts = new DefaultWeakMap(matcher => { return new ExtensionContent.Script(extensions.get(matcher.extension), matcher); }); function getMessageManager(window) { let docShell = window.document.docShell.QueryInterface(Ci.nsIInterfaceRequestor); try { return docShell.getInterface(Ci.nsIContentFrameMessageManager); } catch (e) { // Some windows don't support this interface (hidden window). return null; } } var DocumentManager; var ExtensionManager; class ExtensionGlobal { constructor(global) { this.global = global; this.global.addMessageListener("Extension:SetFrameData", this); this.frameData = null; MessageChannel.addListener(global, "Extension:Capture", this); MessageChannel.addListener(global, "Extension:DetectLanguage", this); MessageChannel.addListener(global, "Extension:Execute", 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; } switch (messageName) { case "Extension:Capture": return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options); case "Extension:DetectLanguage": return ExtensionContent.handleDetectLanguage(this.global, target); case "Extension:Execute": let policy = WebExtensionPolicy.getByID(recipient.extensionId); let matcher = new WebExtensionContentScript(policy, parseScriptOptions(data.options)); Object.assign(matcher, { wantReturnValue: data.options.wantReturnValue, removeCSS: data.options.remove_css, cssOrigin: data.options.css_origin, cssCode: data.options.cssCode, jsCode: data.options.jsCode, }); let script = contentScripts.get(matcher); return ExtensionContent.handleExtensionExecute(this.global, target, data.options, script); case "WebNavigation:GetFrame": return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options); case "WebNavigation:GetAllFrames": return ExtensionContent.handleWebNavigationGetAllFrames(this.global); } } } // Responsible for creating ExtensionContexts and injecting content // scripts into them when new documents are created. DocumentManager = { globals: new Map(), // Initialize listeners that we need regardless of whether extensions are // enabled. earlyInit() { Services.obs.addObserver(this, "tab-content-frameloader-created"); // eslint-disable-line mozilla/balanced-listeners }, // Initialize a frame script global which extension contexts may be loaded // into. initGlobal(global) { this.globals.set(global, new ExtensionGlobal(global)); // eslint-disable-next-line mozilla/balanced-listeners global.addEventListener("unload", () => { this.globals.delete(global); }); }, initExtension(extension) { this.injectExtensionScripts(extension); }, // Listeners observe(subject, topic, data) { if (topic == "tab-content-frameloader-created") { this.initGlobal(subject); } }, // Script loading injectExtensionScripts(extension) { for (let window of this.enumerateWindows()) { let runAt = {document_start: [], document_end: [], document_idle: []}; for (let script of extension.contentScripts) { if (script.matchesWindow(window)) { runAt[script.runAt].push(script); } } let inject = matcher => contentScripts.get(matcher).injectInto(window); let injectAll = matchers => Promise.all(matchers.map(inject)); // Intentionally using `.then` instead of `await`, we only need to // chain injecting other scripts into *this* window, not all windows. injectAll(runAt.document_start) .then(() => injectAll(runAt.document_end)) .then(() => injectAll(runAt.document_idle)); } }, /** * Checks that all parent frames for the given withdow either have the * same add-on ID, or are special chrome-privileged documents such as * about:addons or developer tools panels. * * @param {Window} window * The window to check. * @param {string} addonId * The add-on ID to check. * @returns {boolean} */ checkParentFrames(window, addonId) { while (window.parent !== window) { window = window.parent; let principal = window.document.nodePrincipal; if (Services.scriptSecurityManager.isSystemPrincipal(principal)) { // The add-on manager is a special case, since it contains extension // options pages in same-type frames. if (window.location.href === "about:addons") { return true; } } if (principal.addonId !== addonId) { return false; } } return true; }, loadInto(policy, window) { let extension = extensions.get(policy); if (WebExtensionPolicy.isExtensionProcess && this.checkParentFrames(window, policy.id)) { // We're in a top-level extension frame, or a sub-frame thereof, // in the extension process. Inject the full extension page API. ExtensionPageChild.initExtensionContext(extension, window); } else { // We're in a content sub-frame or not in the extension process. // Only inject a minimal content script API. ExtensionContent.initExtensionContext(extension, window); } }, // Helpers * enumerateWindows(docShell) { if (docShell) { let enum_ = docShell.getDocShellEnumerator(docShell.typeContent, docShell.ENUMERATE_FORWARDS); for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) { yield docShell.getInterface(Ci.nsIDOMWindow); } } else { for (let global of this.globals.keys()) { yield* this.enumerateWindows(global.docShell); } } }, }; ExtensionManager = { init() { MessageChannel.setupMessageManagers([Services.cpmm]); Services.cpmm.addMessageListener("Extension:Startup", this); Services.cpmm.addMessageListener("Extension:Shutdown", this); Services.cpmm.addMessageListener("Extension:FlushJarCache", this); let procData = Services.cpmm.initialProcessData || {}; for (let data of procData["Extension:Extensions"] || []) { this.initExtension(data); } if (isContentProcess) { // Make sure we handle new schema data until Schemas.jsm is loaded. if (!procData["Extension:Schemas"]) { procData["Extension:Schemas"] = new Map(); } this.schemaJSON = procData["Extension:Schemas"]; Services.cpmm.addMessageListener("Schema:Add", this); } }, initExtensionPolicy(extension) { let policy = WebExtensionPolicy.getByID(extension.id); if (!policy) { let localizeCallback, allowedOrigins, webAccessibleResources; if (extension.localize) { // We have a real Extension object. localizeCallback = extension.localize.bind(extension); allowedOrigins = extension.whiteListedHosts; webAccessibleResources = extension.webAccessibleResources; } else { // We have serialized extension data; localizeCallback = str => extensions.get(policy).localize(str); allowedOrigins = new MatchPatternSet(extension.whiteListedHosts); webAccessibleResources = extension.webAccessibleResources.map(host => new MatchGlob(host)); } policy = new WebExtensionPolicy({ id: extension.id, mozExtensionHostname: extension.uuid, name: extension.name, baseURL: extension.resourceURL, permissions: Array.from(extension.permissions), allowedOrigins, webAccessibleResources, contentSecurityPolicy: extension.manifest.content_security_policy, localizeCallback, backgroundScripts: (extension.manifest.background && extension.manifest.background.scripts), contentScripts: extension.contentScripts.map(parseScriptOptions), }); policy.active = true; policy.initData = extension; } return policy; }, initExtension(data) { let policy = this.initExtensionPolicy(data); DocumentManager.initExtension(policy); }, receiveMessage({name, data}) { switch (name) { case "Extension:Startup": { this.initExtension(data); Services.cpmm.sendAsyncMessage("Extension:StartupComplete"); 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; } } Services.cpmm.sendAsyncMessage("Extension:ShutdownComplete"); break; } case "Extension:FlushJarCache": { ExtensionUtils.flushJarCache(data.path); Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete"); break; } case "Schema:Add": { // If we're given a Map, the ordering of the initial items // matters, so swap with our current data to make sure its // entries appear first. if (typeof data.get === "function") { [this.schemaJSON, data] = [data, this.schemaJSON]; Services.cpmm.initialProcessData["Extension:Schemas"] = this.schemaJSON; } for (let [url, schema] of data) { this.schemaJSON.set(url, schema); } break; } } }, }; function ExtensionProcessScript() { if (!ExtensionProcessScript.singleton) { ExtensionProcessScript.singleton = this; } return ExtensionProcessScript.singleton; } ExtensionProcessScript.singleton = null; ExtensionProcessScript.prototype = { classID: Components.ID("{21f9819e-4cdf-49f9-85a0-850af91a5058}"), QueryInterface: XPCOMUtils.generateQI([Ci.mozIExtensionProcessScript]), get wrappedJSObject() { return this; }, getFrameData(global, force) { let extGlobal = DocumentManager.globals.get(global); return extGlobal && extGlobal.getFrameData(force); }, initExtension(extension) { return ExtensionManager.initExtensionPolicy(extension); }, initExtensionDocument(policy, doc) { if (DocumentManager.globals.has(getMessageManager(doc.defaultView))) { DocumentManager.loadInto(policy, doc.defaultView); } }, preloadContentScript(contentScript) { contentScripts.get(contentScript).preload(); }, loadContentScript(contentScript, window) { if (DocumentManager.globals.has(getMessageManager(window))) { contentScripts.get(contentScript).injectInto(window); } }, }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ExtensionProcessScript]); DocumentManager.earlyInit(); ExtensionManager.init();