1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 var EXPORTED_SYMBOLS = ["ExtensionContent", "ExtensionContentChild"];
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11 const { XPCOMUtils } = ChromeUtils.import(
12 "resource://gre/modules/XPCOMUtils.jsm"
15 XPCOMUtils.defineLazyModuleGetters(this, {
16 AppConstants: "resource://gre/modules/AppConstants.jsm",
17 ExtensionProcessScript: "resource://gre/modules/ExtensionProcessScript.jsm",
18 ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm",
19 LanguageDetector: "resource:///modules/translation/LanguageDetector.jsm",
20 MessageChannel: "resource://gre/modules/MessageChannel.jsm",
21 Schemas: "resource://gre/modules/Schemas.jsm",
22 WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
25 XPCOMUtils.defineLazyServiceGetter(
28 "@mozilla.org/content/style-sheet-service;1",
29 "nsIStyleSheetService"
32 const Timer = Components.Constructor(
33 "@mozilla.org/timer;1",
38 const { ExtensionChild, ExtensionActivityLogChild } = ChromeUtils.import(
39 "resource://gre/modules/ExtensionChild.jsm"
41 const { ExtensionCommon } = ChromeUtils.import(
42 "resource://gre/modules/ExtensionCommon.jsm"
44 const { ExtensionUtils } = ChromeUtils.import(
45 "resource://gre/modules/ExtensionUtils.jsm"
48 XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
55 promiseDocumentLoaded,
64 runSafeSyncWithoutClone,
67 const { BrowserExtensionContent, ChildAPIManager, Messenger } = ExtensionChild;
69 XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
71 XPCOMUtils.defineLazyGetter(this, "isContentScriptProcess", () => {
73 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT ||
74 !WebExtensionPolicy.useRemoteWebExtensions ||
75 // Thunderbird still loads some content in the parent process.
76 AppConstants.MOZ_APP_NAME == "thunderbird"
82 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
84 var apiManager = new (class extends SchemaAPIManager {
86 super("content", Schemas);
87 this.initialized = false;
91 if (!this.initialized) {
92 this.initialized = true;
94 for (let { value } of Services.catMan.enumerateCategory(
95 CATEGORY_EXTENSION_SCRIPTS_CONTENT
97 this.loadScript(value);
103 const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
104 const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
106 const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
107 const CSSCODE_EXPIRY_TIMEOUT_MS = 10 * 60 * 1000;
109 const scriptCaches = new WeakSet();
110 const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
112 class CacheMap extends DefaultMap {
113 constructor(timeout, getter, extension) {
116 this.expiryTimeout = timeout;
118 scriptCaches.add(this);
120 // This ensures that all the cached scripts and stylesheets are deleted
121 // from the cache and the xpi is no longer actively used.
122 // See Bug 1435100 for rationale.
123 extension.once("shutdown", () => {
129 let promise = super.get(url);
131 promise.lastUsed = Date.now();
133 promise.timer.cancel();
135 promise.timer = Timer(
136 this.delete.bind(this, url),
138 Ci.nsITimer.TYPE_ONE_SHOT
146 super.get(url).timer.cancel();
152 clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) {
153 let now = Date.now();
154 for (let [url, promise] of this.entries()) {
155 // Delete the entry if expired or if clear has been called with timeout -1
156 // (which is used to force the cache to clear all the entries, e.g. when the
157 // extension is shutting down).
158 if (timeout === -1 || now - promise.lastUsed >= timeout) {
165 class ScriptCache extends CacheMap {
166 constructor(options, extension) {
167 super(SCRIPT_EXPIRY_TIMEOUT_MS, null, extension);
168 this.options = options;
171 defaultConstructor(url) {
172 let promise = ChromeUtils.compileScript(url, this.options);
173 promise.then(script => {
174 promise.script = script;
181 * Shared base class for the two specialized CSS caches:
182 * CSSCache (for the "url"-based stylesheets) and CSSCodeCache
183 * (for the stylesheet defined by plain CSS content as a string).
185 class BaseCSSCache extends CacheMap {
186 constructor(expiryTimeout, defaultConstructor, extension) {
187 super(expiryTimeout, defaultConstructor, extension);
190 addDocument(key, document) {
191 sheetCacheDocuments.get(this.get(key)).add(document);
194 deleteDocument(key, document) {
195 sheetCacheDocuments.get(this.get(key)).delete(document);
200 let promise = this.get(key);
202 // Never remove a sheet from the cache if it's still being used by a
203 // document. Rule processors can be shared between documents with the
204 // same preloaded sheet, so we only lose by removing them while they're
206 let docs = ChromeUtils.nondeterministicGetWeakSetKeys(
207 sheetCacheDocuments.get(promise)
219 * Cache of the preloaded stylesheet defined by url.
221 class CSSCache extends BaseCSSCache {
222 constructor(sheetType, extension) {
224 CSS_EXPIRY_TIMEOUT_MS,
226 let uri = Services.io.newURI(url);
227 return styleSheetService
228 .preloadSheetAsync(uri, sheetType)
230 return { url, sheet };
239 * Cache of the preloaded stylesheet defined by plain CSS content as a string,
240 * the key of the cached stylesheet is the hash of its "CSSCode" string.
242 class CSSCodeCache extends BaseCSSCache {
243 constructor(sheetType, extension) {
245 CSSCODE_EXPIRY_TIMEOUT_MS,
247 if (!this.has(hash)) {
248 // Do not allow the getter to be used to lazily create the cached stylesheet,
249 // the cached CSSCode stylesheet has to be explicitly set.
251 "Unexistent cached cssCode stylesheet: " + Error().stack
255 return super.get(hash);
260 // Store the preferred sheetType (used to preload the expected stylesheet type in
261 // the addCSSCode method).
262 this.sheetType = sheetType;
265 addCSSCode(hash, cssCode) {
266 if (this.has(hash)) {
267 // This cssCode have been already cached, no need to create it again.
270 // The `webext=style` portion is added metadata to help us distinguish
271 // different kinds of data URL loads that are triggered with the
272 // SystemPrincipal. It shall be removed with bug 1699425.
273 const uri = Services.io.newURI(
274 "data:text/css;extension=style;charset=utf-8," +
275 encodeURIComponent(cssCode)
277 const value = styleSheetService
278 .preloadSheetAsync(uri, this.sheetType)
280 return { sheet, uri };
283 super.set(hash, value);
288 BrowserExtensionContent.prototype,
291 return new ScriptCache({ hasReturnValue: false }, this);
296 BrowserExtensionContent.prototype,
299 return new ScriptCache({ hasReturnValue: true }, this);
303 defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", function() {
304 return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET, this);
307 defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", function() {
308 return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
311 // These two caches are similar to the above but specialized to cache the cssCode
312 // using an hash computed from the cssCode string as the key (instead of the generated data
313 // URI which can be pretty long for bigger injected cssCode).
314 defineLazyGetter(BrowserExtensionContent.prototype, "userCSSCode", function() {
315 return new CSSCodeCache(Ci.nsIStyleSheetService.USER_SHEET, this);
319 BrowserExtensionContent.prototype,
322 return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
326 // Represents a content script.
329 * @param {BrowserExtensionContent} extension
330 * @param {WebExtensionContentScript|object} matcher
331 * An object with a "matchesWindowGlobal" method and content script
332 * execution details. This is usually a plain WebExtensionContentScript
333 * except when the script is run via `tabs.executeScript`. In this
334 * case, the object may have some extra properties:
335 * wantReturnValue, removeCSS, cssOrigin, jsCode
337 constructor(extension, matcher) {
338 this.scriptType = "content_script";
339 this.extension = extension;
340 this.matcher = matcher;
342 this.runAt = this.matcher.runAt;
343 this.js = this.matcher.jsPaths;
344 this.css = this.matcher.cssPaths.slice();
345 this.cssCodeHash = null;
347 this.removeCSS = this.matcher.removeCSS;
348 this.cssOrigin = this.matcher.cssOrigin;
351 extension[this.cssOrigin === "user" ? "userCSS" : "authorCSS"];
353 extension[this.cssOrigin === "user" ? "userCSSCode" : "authorCSSCode"];
355 extension[matcher.wantReturnValue ? "dynamicScripts" : "staticScripts"];
357 if (matcher.wantReturnValue) {
358 this.compileScripts();
363 get requiresCleanup() {
364 return !this.removeCSS && (!!this.css.length || this.cssCodeHash);
367 async addCSSCode(cssCode) {
372 // Store the hash of the cssCode.
373 const buffer = await crypto.subtle.digest(
375 new TextEncoder().encode(cssCode)
377 this.cssCodeHash = String.fromCharCode(...new Uint16Array(buffer));
379 // Cache and preload the cssCode stylesheet.
380 this.cssCodeCache.addCSSCode(this.cssCodeHash, cssCode);
384 return this.js.map(url => this.scriptCache.get(url));
388 return this.css.map(url => this.cssCache.get(url));
393 this.compileScripts();
397 if (this.requiresCleanup) {
399 let { windowUtils } = window;
402 this.cssOrigin === "user"
403 ? windowUtils.USER_SHEET
404 : windowUtils.AUTHOR_SHEET;
406 for (let url of this.css) {
407 this.cssCache.deleteDocument(url, window.document);
409 if (!window.closed) {
410 runSafeSyncWithoutClone(
411 windowUtils.removeSheetUsingURIString,
418 const { cssCodeHash } = this;
420 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
421 if (!window.closed) {
422 this.cssCodeCache.get(cssCodeHash).then(({ uri }) => {
423 runSafeSyncWithoutClone(windowUtils.removeSheet, uri, type);
426 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
430 // Clear any sheets that were kept alive past their timeout as
431 // a result of living in this document.
432 this.cssCodeCache.clear(CSSCODE_EXPIRY_TIMEOUT_MS);
433 this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
437 matchesWindowGlobal(windowGlobal) {
438 return this.matcher.matchesWindowGlobal(windowGlobal);
441 async injectInto(window) {
442 if (!isContentScriptProcess) {
446 let context = this.extension.getContext(window);
447 for (let script of this.matcher.jsPaths) {
448 context.logActivity(this.scriptType, script, {
449 url: window.location.href,
454 if (this.runAt === "document_end") {
455 await promiseDocumentReady(window.document);
456 } else if (this.runAt === "document_idle") {
458 promiseDocumentIdle(window),
459 promiseDocumentLoaded(window.document),
463 return this.inject(context);
465 return Promise.reject(context.normalizeError(e));
470 * Tries to inject this script into the given window and sandbox, if
471 * there are pending operations for the window's current load state.
473 * @param {BaseContext} context
474 * The content script context into which to inject the scripts.
475 * @returns {Promise<any>}
476 * Resolves to the last value in the evaluated script, when
477 * execution is complete.
479 async inject(context) {
480 DocumentManager.lazyInit();
481 if (this.requiresCleanup) {
482 context.addScript(this);
485 const { cssCodeHash } = this;
488 if (this.css.length || cssCodeHash) {
489 let window = context.contentWindow;
490 let { windowUtils } = window;
493 this.cssOrigin === "user"
494 ? windowUtils.USER_SHEET
495 : windowUtils.AUTHOR_SHEET;
497 if (this.removeCSS) {
498 for (let url of this.css) {
499 this.cssCache.deleteDocument(url, window.document);
501 runSafeSyncWithoutClone(
502 windowUtils.removeSheetUsingURIString,
508 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
509 const { uri } = await this.cssCodeCache.get(cssCodeHash);
510 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
512 runSafeSyncWithoutClone(windowUtils.removeSheet, uri, type);
515 cssPromise = Promise.all(this.loadCSS()).then(sheets => {
516 let window = context.contentWindow;
521 for (let { url, sheet } of sheets) {
522 this.cssCache.addDocument(url, window.document);
524 runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
529 cssPromise = cssPromise.then(async () => {
530 const { sheet } = await this.cssCodeCache.get(cssCodeHash);
531 this.cssCodeCache.addDocument(cssCodeHash, window.document);
533 runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
537 // We're loading stylesheets via the stylesheet service, which means
538 // that the normal mechanism for blocking layout and onload for pending
539 // stylesheets aren't in effect (since there's no document to block). So
540 // we need to do something custom here, similar to what we do for
541 // scripts. Blocking parsing is overkill, since we really just want to
542 // block layout and onload. But we have an API to do the former and not
543 // the latter, so we do it that way. This hopefully isn't a performance
544 // problem since there are no network loads involved, and since we cache
545 // the stylesheets on first load. We should fix this up if it does becomes
547 if (this.css.length) {
548 context.contentWindow.document.blockParsing(cssPromise, {
549 blockScriptCreated: false,
555 let scripts = this.getCompiledScripts(context);
556 if (scripts instanceof Promise) {
557 scripts = await scripts;
560 // Make sure we've injected any related CSS before we run content scripts.
565 const { extension } = context;
567 // The evaluations below may throw, in which case the promise will be
568 // automatically rejected.
569 ExtensionTelemetry.contentScriptInjection.stopwatchStart(
574 for (let script of scripts) {
575 result = script.executeInGlobal(context.cloneScope);
578 if (this.matcher.jsCode) {
579 result = Cu.evalInSandbox(
588 ExtensionTelemetry.contentScriptInjection.stopwatchFinish(
598 * Get the compiled scripts (if they are already precompiled and cached) or a promise which resolves
599 * to the precompiled scripts (once they have been compiled and cached).
601 * @param {BaseContext} context
602 * The document to block the parsing on, if the scripts are not yet precompiled and cached.
604 * @returns {Array<PreloadedScript> | Promise<Array<PreloadedScript>>}
605 * Returns an array of preloaded scripts if they are already available, or a promise which
606 * resolves to the array of the preloaded scripts once they are precompiled and cached.
608 getCompiledScripts(context) {
609 let scriptPromises = this.compileScripts();
610 let scripts = scriptPromises.map(promise => promise.script);
612 // If not all scripts are already available in the cache, block
613 // parsing and wait all promises to resolve.
614 if (!scripts.every(script => script)) {
615 let promise = Promise.all(scriptPromises);
617 // If we're supposed to inject at the start of the document load,
618 // and we haven't already missed that point, block further parsing
619 // until the scripts have been loaded.
620 const { document } = context.contentWindow;
622 this.runAt === "document_start" &&
623 document.readyState !== "complete"
625 document.blockParsing(promise, { blockScriptCreated: false });
635 // Represents a user script.
636 class UserScript extends Script {
638 * @param {BrowserExtensionContent} extension
639 * @param {WebExtensionContentScript|object} matcher
640 * An object with a "matchesWindowGlobal" method and content script
643 constructor(extension, matcher) {
644 super(extension, matcher);
645 this.scriptType = "user_script";
647 // This is an opaque object that the extension provides, it is associated to
648 // the particular userScript and it is passed as a parameter to the custom
649 // userScripts APIs defined by the extension.
650 this.scriptMetadata = matcher.userScriptOptions.scriptMetadata;
652 extension.manifest.user_scripts &&
653 extension.manifest.user_scripts.api_script;
655 // Add the apiScript to the js scripts to compile.
656 if (this.apiScriptURL) {
657 this.js = [this.apiScriptURL].concat(this.js);
660 // WeakMap<ContentScriptContextChild, Sandbox>
661 this.sandboxes = new DefaultWeakMap(context => {
662 return this.createSandbox(context);
666 async inject(context) {
667 const { extension } = context;
669 DocumentManager.lazyInit();
671 let scripts = this.getCompiledScripts(context);
672 if (scripts instanceof Promise) {
673 scripts = await scripts;
676 let apiScript, sandboxScripts;
678 if (this.apiScriptURL) {
679 [apiScript, ...sandboxScripts] = scripts;
681 sandboxScripts = scripts;
684 // Load and execute the API script once per context.
686 context.executeAPIScript(apiScript);
689 // The evaluations below may throw, in which case the promise will be
690 // automatically rejected.
691 ExtensionTelemetry.userScriptInjection.stopwatchStart(extension, context);
693 let userScriptSandbox = this.sandboxes.get(context);
695 context.callOnClose({
697 // Destroy the userScript sandbox when the related ContentScriptContextChild instance
699 this.sandboxes.delete(context);
700 Cu.nukeSandbox(userScriptSandbox);
704 // Notify listeners subscribed to the userScripts.onBeforeScript API event,
705 // to allow extension API script to provide its custom APIs to the userScript.
707 context.userScriptsEvents.emit(
714 for (let script of sandboxScripts) {
715 script.executeInGlobal(userScriptSandbox);
718 ExtensionTelemetry.userScriptInjection.stopwatchFinish(
725 createSandbox(context) {
726 const { contentWindow } = context;
727 const contentPrincipal = contentWindow.document.nodePrincipal;
728 const ssm = Services.scriptSecurityManager;
731 if (contentPrincipal.isSystemPrincipal) {
732 principal = ssm.createNullPrincipal(contentPrincipal.originAttributes);
734 principal = [contentPrincipal];
737 const sandbox = Cu.Sandbox(principal, {
738 sandboxName: `User Script registered by ${this.extension.policy.debugName}`,
739 sandboxPrototype: contentWindow,
740 sameZoneAs: contentWindow,
742 wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
743 originAttributes: contentPrincipal.originAttributes,
745 "inner-window-id": context.innerWindowID,
746 addonId: this.extension.policy.id,
754 var contentScripts = new DefaultWeakMap(matcher => {
755 const extension = ExtensionProcessScript.extensions.get(matcher.extension);
757 if ("userScriptOptions" in matcher) {
758 return new UserScript(extension, matcher);
761 return new Script(extension, matcher);
765 * An execution context for semi-privileged extension content scripts.
767 * This is the child side of the ContentScriptContextParent class
768 * defined in ExtensionParent.jsm.
770 class ContentScriptContextChild extends BaseContext {
771 constructor(extension, contentWindow) {
772 super("content_child", extension);
774 this.setContentWindow(contentWindow);
776 let frameId = WebNavigationFrames.getFrameId(contentWindow);
777 this.frameId = frameId;
779 this.browsingContextId = contentWindow.docShell.browsingContext.id;
783 let contentPrincipal = contentWindow.document.nodePrincipal;
784 let ssm = Services.scriptSecurityManager;
786 // Copy origin attributes from the content window origin attributes to
787 // preserve the user context id.
788 let attrs = contentPrincipal.originAttributes;
789 let extensionPrincipal = ssm.createContentPrincipal(
790 this.extension.baseURI,
794 this.isExtensionPage = contentPrincipal.equals(extensionPrincipal);
796 if (this.isExtensionPage) {
797 // This is an iframe with content script API enabled and its principal
798 // should be the contentWindow itself. We create a sandbox with the
799 // contentWindow as principal and with X-rays disabled because it
800 // enables us to create the APIs object in this sandbox object and then
801 // copying it into the iframe's window. See bug 1214658.
802 this.sandbox = Cu.Sandbox(contentWindow, {
803 sandboxName: `Web-Accessible Extension Page ${extension.policy.debugName}`,
804 sandboxPrototype: contentWindow,
805 sameZoneAs: contentWindow,
807 isWebExtensionContentScript: true,
811 if (contentPrincipal.isSystemPrincipal) {
812 // Make sure we don't hand out the system principal by accident.
813 // Also make sure that the null principal has the right origin attributes.
814 principal = ssm.createNullPrincipal(attrs);
816 principal = [contentPrincipal, extensionPrincipal];
818 // This metadata is required by the Developer Tools, in order for
819 // the content script to be associated with both the extension and
820 // the tab holding the content page.
822 "inner-window-id": this.innerWindowID,
823 addonId: extensionPrincipal.addonId,
826 this.sandbox = Cu.Sandbox(principal, {
828 sandboxName: `Content Script ${extension.policy.debugName}`,
829 sandboxPrototype: contentWindow,
830 sameZoneAs: contentWindow,
832 isWebExtensionContentScript: true,
833 wantExportHelpers: true,
834 wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
835 originAttributes: attrs,
838 // Preserve a copy of the original Error and Promise globals from the sandbox object,
839 // which are used in the WebExtensions internals (before any content script code had
840 // any chance to redefine them).
841 this.cloneScopePromise = this.sandbox.Promise;
842 this.cloneScopeError = this.sandbox.Error;
844 // Preserve a copy of the original window's XMLHttpRequest and fetch
845 // in a content object (fetch is manually binded to the window
846 // to prevent it from raising a TypeError because content object is not
851 XMLHttpRequest: window.XMLHttpRequest,
852 fetch: window.fetch.bind(window),
853 WebSocket: window.WebSocket,
857 window.XMLHttpRequest = XMLHttpRequest;
858 window.fetch = fetch;
859 window.WebSocket = WebSocket;
865 Object.defineProperty(this, "principal", {
866 value: Cu.getObjectPrincipal(this.sandbox),
871 this.url = contentWindow.location.href;
873 defineLazyGetter(this, "chromeObj", () => {
874 let chromeObj = Cu.createObjectIn(this.sandbox);
876 this.childManager.inject(chromeObj);
880 Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
881 Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
883 // Keep track if the userScript API script has been already executed in this context
884 // (e.g. because there are more then one UserScripts that match the related webpage
885 // and so the UserScript apiScript has already been executed).
886 this.hasUserScriptAPIs = false;
888 // A lazy created EventEmitter related to userScripts-specific events.
889 defineLazyGetter(this, "userScriptsEvents", () => {
890 return new ExtensionCommon.EventEmitter();
895 if (!this.isExtensionPage) {
896 throw new Error("Cannot inject extension API into non-extension window");
899 // This is an iframe with content script API enabled (See Bug 1214658)
900 Schemas.exportLazyGetter(
905 Schemas.exportLazyGetter(
912 async logActivity(type, name, data) {
913 ExtensionActivityLogChild.log(this, type, name, data);
920 async executeAPIScript(apiScript) {
921 // Execute the UserScript apiScript only once per context (e.g. more then one UserScripts
922 // match the same webpage and the apiScript has already been executed).
923 if (apiScript && !this.hasUserScriptAPIs) {
924 this.hasUserScriptAPIs = true;
925 apiScript.executeInGlobal(this.cloneScope);
930 if (script.requiresCleanup) {
931 this.scripts.push(script);
938 // Cleanup the scripts even if the contentWindow have been destroyed.
939 for (let script of this.scripts) {
940 script.cleanup(this.contentWindow);
943 if (this.contentWindow) {
944 // Overwrite the content script APIs with an empty object if the APIs objects are still
945 // defined in the content window (See Bug 1214658).
946 if (this.isExtensionPage) {
947 Cu.createObjectIn(this.contentWindow, { defineAs: "browser" });
948 Cu.createObjectIn(this.contentWindow, { defineAs: "chrome" });
951 Cu.nukeSandbox(this.sandbox);
957 defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
958 return new Messenger(this);
962 ContentScriptContextChild.prototype,
965 apiManager.lazyInit();
968 let can = new CanOfAPIs(this, apiManager, localApis);
970 let childManager = new ChildAPIManager(this, this.messageManager, can, {
971 envType: "content_parent",
975 this.callOnClose(childManager);
981 // Responsible for creating ExtensionContexts and injecting content
982 // scripts into them when new documents are created.
984 // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
990 if (this.initialized) {
993 this.initialized = true;
995 Services.obs.addObserver(this, "inner-window-destroyed");
996 Services.obs.addObserver(this, "memory-pressure");
1000 Services.obs.removeObserver(this, "inner-window-destroyed");
1001 Services.obs.removeObserver(this, "memory-pressure");
1005 "inner-window-destroyed"(subject, topic, data) {
1006 let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
1008 MessageChannel.abortResponses({ innerWindowID: windowId });
1010 // Close any existent content-script context for the destroyed window.
1011 if (this.contexts.has(windowId)) {
1012 let extensions = this.contexts.get(windowId);
1013 for (let context of extensions.values()) {
1017 this.contexts.delete(windowId);
1020 "memory-pressure"(subject, topic, data) {
1021 let timeout = data === "heap-minimize" ? 0 : undefined;
1023 for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(
1026 cache.clear(timeout);
1031 observe(subject, topic, data) {
1032 this.observers[topic].call(this, subject, topic, data);
1035 shutdownExtension(extension) {
1036 for (let extensions of this.contexts.values()) {
1037 let context = extensions.get(extension);
1040 extensions.delete(extension);
1045 getContexts(window) {
1046 let winId = getInnerWindowID(window);
1048 let extensions = this.contexts.get(winId);
1050 extensions = new Map();
1051 this.contexts.set(winId, extensions);
1057 // For test use only.
1058 getContext(extensionId, window) {
1059 for (let [extension, context] of this.getContexts(window)) {
1060 if (extension.id === extensionId) {
1066 getContentScriptGlobals(window) {
1067 let extensions = this.contexts.get(getInnerWindowID(window));
1070 return Array.from(extensions.values(), ctx => ctx.sandbox);
1076 initExtensionContext(extension, window) {
1077 extension.getContext(window).injectAPI();
1081 var ExtensionContent = {
1082 BrowserExtensionContent,
1086 shutdownExtension(extension) {
1087 DocumentManager.shutdownExtension(extension);
1090 // This helper is exported to be integrated in the devtools RDP actors,
1091 // that can use it to retrieve the existent WebExtensions ContentScripts
1092 // of a target window and be able to show the ContentScripts source in the
1093 // DevTools Debugger panel.
1094 getContentScriptGlobals(window) {
1095 return DocumentManager.getContentScriptGlobals(window);
1098 initExtensionContext(extension, window) {
1099 DocumentManager.initExtensionContext(extension, window);
1102 getContext(extension, window) {
1103 let extensions = DocumentManager.getContexts(window);
1105 let context = extensions.get(extension);
1107 context = new ContentScriptContextChild(extension, window);
1108 extensions.set(extension, context);
1113 handleDetectLanguage(global, target) {
1114 let doc = target.content.document;
1116 return promiseDocumentReady(doc).then(() => {
1117 let elem = doc.documentElement;
1120 elem.getAttribute("xml:lang") ||
1121 elem.getAttribute("lang") ||
1122 doc.contentLanguage ||
1125 // We only want the last element of the TLD here.
1126 // Only country codes have any effect on the results, but other
1127 // values cause no harm.
1128 let tld = doc.location.hostname.match(/[a-z]*$/)[0];
1130 // The CLD2 library used by the language detector is capable of
1131 // analyzing raw HTML. Unfortunately, that takes much more memory,
1132 // and since it's hosted by emscripten, and therefore can't shrink
1133 // its heap after it's grown, it has a performance cost.
1134 // So we send plain text instead.
1135 let encoder = Cu.createDocumentEncoder("text/plain");
1139 Ci.nsIDocumentEncoder.SkipInvisibleContent
1141 let text = encoder.encodeToStringWithMaxLength(60 * 1024);
1143 let encoding = doc.characterSet;
1145 return LanguageDetector.detectLanguage({
1150 }).then(result => (result.language === "un" ? "und" : result.language));
1154 // Used to executeScript, insertCSS and removeCSS.
1155 async handleActorExecute({ options, windows }) {
1156 let policy = WebExtensionPolicy.getByID(options.extensionId);
1157 let matcher = new WebExtensionContentScript(policy, options);
1159 Object.assign(matcher, {
1160 wantReturnValue: options.wantReturnValue,
1161 removeCSS: options.removeCSS,
1162 cssOrigin: options.cssOrigin,
1163 jsCode: options.jsCode,
1165 let script = contentScripts.get(matcher);
1167 // Add the cssCode to the script, so that it can be converted into a cached URL.
1168 await script.addCSSCode(options.cssCode);
1169 delete options.cssCode;
1171 const executeInWin = innerId => {
1172 let wg = WindowGlobalChild.getByInnerWindowId(innerId);
1173 if (wg?.isCurrentGlobal && script.matchesWindowGlobal(wg)) {
1174 return script.injectInto(wg.browsingContext.window);
1178 let all = Promise.all(windows.map(executeInWin).filter(p => p));
1179 let result = await all.catch(e => Promise.reject({ message: e.message }));
1182 // Check if the result can be structured-cloned before sending back.
1183 return Cu.cloneInto(result, this);
1185 let path = options.jsPaths.slice(-1)[0] ?? "<anonymous code>";
1186 let message = `Script '${path}' result is non-structured-clonable data`;
1187 return Promise.reject({ message, fileName: path });
1191 async receiveMessage(global, name, target) {
1192 if (name === "Extension:DetectLanguage") {
1193 return this.handleDetectLanguage(global, target);
1199 *enumerateWindows(docShell) {
1200 let docShells = docShell.getAllDocShellsInSubtree(
1201 docShell.typeContent,
1202 docShell.ENUMERATE_FORWARDS
1205 for (let docShell of docShells) {
1207 yield docShell.domWindow;
1209 // This can fail if the docShell is being destroyed, so just
1210 // ignore the error.
1217 * Child side of the ExtensionContent process actor, handles some tabs.* APIs.
1219 class ExtensionContentChild extends JSProcessActorChild {
1220 receiveMessage({ name, data }) {
1221 if (!isContentScriptProcess) {
1226 return ExtensionContent.handleActorExecute(data);