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/. */
7 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
12 ChromeUtils.defineESModuleGetters(lazy, {
13 ExtensionProcessScript:
14 "resource://gre/modules/ExtensionProcessScript.sys.mjs",
15 ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
17 "resource://gre/modules/translation/LanguageDetector.sys.mjs",
18 Schemas: "resource://gre/modules/Schemas.sys.mjs",
19 WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs",
22 XPCOMUtils.defineLazyServiceGetter(
25 "@mozilla.org/content/style-sheet-service;1",
26 "nsIStyleSheetService"
29 const Timer = Components.Constructor(
30 "@mozilla.org/timer;1",
35 const ScriptError = Components.Constructor(
36 "@mozilla.org/scripterror;1",
43 ExtensionActivityLogChild,
44 } from "resource://gre/modules/ExtensionChild.sys.mjs";
45 import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
46 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
53 promiseDocumentLoaded,
63 runSafeSyncWithoutClone,
66 const { BrowserExtensionContent, ChildAPIManager, Messenger } = ExtensionChild;
68 ChromeUtils.defineLazyGetter(lazy, "isContentScriptProcess", () => {
70 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT ||
71 !WebExtensionPolicy.useRemoteWebExtensions ||
72 // Thunderbird still loads some content in the parent process.
73 AppConstants.MOZ_APP_NAME == "thunderbird"
79 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
81 var apiManager = new (class extends SchemaAPIManager {
83 super("content", lazy.Schemas);
84 this.initialized = false;
88 if (!this.initialized) {
89 this.initialized = true;
91 for (let { value } of Services.catMan.enumerateCategory(
92 CATEGORY_EXTENSION_SCRIPTS_CONTENT
94 this.loadScript(value);
100 const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
101 const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
103 const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
104 const CSSCODE_EXPIRY_TIMEOUT_MS = 10 * 60 * 1000;
106 const scriptCaches = new WeakSet();
107 const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
109 class CacheMap extends DefaultMap {
110 constructor(timeout, getter, extension) {
113 this.expiryTimeout = timeout;
115 scriptCaches.add(this);
117 // This ensures that all the cached scripts and stylesheets are deleted
118 // from the cache and the xpi is no longer actively used.
119 // See Bug 1435100 for rationale.
120 extension.once("shutdown", () => {
126 let promise = super.get(url);
128 promise.lastUsed = Date.now();
130 promise.timer.cancel();
132 promise.timer = Timer(
133 this.delete.bind(this, url),
135 Ci.nsITimer.TYPE_ONE_SHOT
143 super.get(url).timer.cancel();
146 return super.delete(url);
149 clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) {
150 let now = Date.now();
151 for (let [url, promise] of this.entries()) {
152 // Delete the entry if expired or if clear has been called with timeout -1
153 // (which is used to force the cache to clear all the entries, e.g. when the
154 // extension is shutting down).
155 if (timeout === -1 || now - promise.lastUsed >= timeout) {
162 class ScriptCache extends CacheMap {
163 constructor(options, extension) {
165 SCRIPT_EXPIRY_TIMEOUT_MS,
167 let promise = ChromeUtils.compileScript(url, options);
168 promise.then(script => {
169 promise.script = script;
179 * Shared base class for the two specialized CSS caches:
180 * CSSCache (for the "url"-based stylesheets) and CSSCodeCache
181 * (for the stylesheet defined by plain CSS content as a string).
183 class BaseCSSCache extends CacheMap {
184 constructor(expiryTimeout, defaultConstructor, extension) {
185 super(expiryTimeout, defaultConstructor, extension);
188 addDocument(key, document) {
189 sheetCacheDocuments.get(this.get(key)).add(document);
192 deleteDocument(key, document) {
193 sheetCacheDocuments.get(this.get(key)).delete(document);
198 let promise = this.get(key);
200 // Never remove a sheet from the cache if it's still being used by a
201 // document. Rule processors can be shared between documents with the
202 // same preloaded sheet, so we only lose by removing them while they're
204 let docs = ChromeUtils.nondeterministicGetWeakSetKeys(
205 sheetCacheDocuments.get(promise)
212 return super.delete(key);
217 * Cache of the preloaded stylesheet defined by url.
219 class CSSCache extends BaseCSSCache {
220 constructor(sheetType, extension) {
222 CSS_EXPIRY_TIMEOUT_MS,
224 let uri = Services.io.newURI(url);
225 return lazy.styleSheetService
226 .preloadSheetAsync(uri, sheetType)
228 return { url, sheet };
237 * Cache of the preloaded stylesheet defined by plain CSS content as a string,
238 * the key of the cached stylesheet is the hash of its "CSSCode" string.
240 class CSSCodeCache extends BaseCSSCache {
241 constructor(sheetType, extension) {
243 CSSCODE_EXPIRY_TIMEOUT_MS,
245 if (!this.has(hash)) {
246 // Do not allow the getter to be used to lazily create the cached stylesheet,
247 // the cached CSSCode stylesheet has to be explicitly set.
249 "Unexistent cached cssCode stylesheet: " + Error().stack
253 return super.get(hash);
258 // Store the preferred sheetType (used to preload the expected stylesheet type in
259 // the addCSSCode method).
260 this.sheetType = sheetType;
263 addCSSCode(hash, cssCode) {
264 if (this.has(hash)) {
265 // This cssCode have been already cached, no need to create it again.
268 // The `webext=style` portion is added metadata to help us distinguish
269 // different kinds of data URL loads that are triggered with the
270 // SystemPrincipal. It shall be removed with bug 1699425.
271 const uri = Services.io.newURI(
272 "data:text/css;extension=style;charset=utf-8," +
273 encodeURIComponent(cssCode)
275 const value = lazy.styleSheetService
276 .preloadSheetAsync(uri, this.sheetType)
278 return { sheet, uri };
281 super.set(hash, value);
286 BrowserExtensionContent.prototype,
289 return new ScriptCache({ hasReturnValue: false }, this);
294 BrowserExtensionContent.prototype,
297 return new ScriptCache({ hasReturnValue: true }, this);
301 defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", function () {
302 return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET, this);
305 defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", function () {
306 return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
309 // These two caches are similar to the above but specialized to cache the cssCode
310 // using an hash computed from the cssCode string as the key (instead of the generated data
311 // URI which can be pretty long for bigger injected cssCode).
312 defineLazyGetter(BrowserExtensionContent.prototype, "userCSSCode", function () {
313 return new CSSCodeCache(Ci.nsIStyleSheetService.USER_SHEET, this);
317 BrowserExtensionContent.prototype,
320 return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
324 // Represents a content script.
327 * @param {BrowserExtensionContent} extension
328 * @param {WebExtensionContentScript|object} matcher
329 * An object with a "matchesWindowGlobal" method and content script
330 * execution details. This is usually a plain WebExtensionContentScript
331 * except when the script is run via `tabs.executeScript`. In this
332 * case, the object may have some extra properties:
333 * wantReturnValue, removeCSS, cssOrigin, jsCode
335 constructor(extension, matcher) {
336 this.scriptType = "content_script";
337 this.extension = extension;
338 this.matcher = matcher;
340 this.runAt = this.matcher.runAt;
341 this.js = this.matcher.jsPaths;
342 this.css = this.matcher.cssPaths.slice();
343 this.cssCodeHash = null;
345 this.removeCSS = this.matcher.removeCSS;
346 this.cssOrigin = this.matcher.cssOrigin;
349 extension[this.cssOrigin === "user" ? "userCSS" : "authorCSS"];
351 extension[this.cssOrigin === "user" ? "userCSSCode" : "authorCSSCode"];
353 extension[matcher.wantReturnValue ? "dynamicScripts" : "staticScripts"];
355 /** @type {WeakSet<Document>} A set of documents injected into. */
356 this.injectedInto = new WeakSet();
358 if (matcher.wantReturnValue) {
359 this.compileScripts();
364 get requiresCleanup() {
365 return !this.removeCSS && (!!this.css.length || this.cssCodeHash);
368 async addCSSCode(cssCode) {
373 // Store the hash of the cssCode.
374 const buffer = await crypto.subtle.digest(
376 new TextEncoder().encode(cssCode)
378 this.cssCodeHash = String.fromCharCode(...new Uint16Array(buffer));
380 // Cache and preload the cssCode stylesheet.
381 this.cssCodeCache.addCSSCode(this.cssCodeHash, cssCode);
385 return this.js.map(url => this.scriptCache.get(url));
389 return this.css.map(url => this.cssCache.get(url));
394 this.compileScripts();
398 if (this.requiresCleanup) {
400 let { windowUtils } = window;
403 this.cssOrigin === "user"
404 ? windowUtils.USER_SHEET
405 : windowUtils.AUTHOR_SHEET;
407 for (let url of this.css) {
408 this.cssCache.deleteDocument(url, window.document);
410 if (!window.closed) {
411 runSafeSyncWithoutClone(
412 windowUtils.removeSheetUsingURIString,
419 const { cssCodeHash } = this;
421 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
422 if (!window.closed) {
423 this.cssCodeCache.get(cssCodeHash).then(({ uri }) => {
424 runSafeSyncWithoutClone(windowUtils.removeSheet, uri, type);
427 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
431 // Clear any sheets that were kept alive past their timeout as
432 // a result of living in this document.
433 this.cssCodeCache.clear(CSSCODE_EXPIRY_TIMEOUT_MS);
434 this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
438 matchesWindowGlobal(windowGlobal, ignorePermissions) {
439 return this.matcher.matchesWindowGlobal(windowGlobal, ignorePermissions);
442 async injectInto(window, reportExceptions = true) {
444 !lazy.isContentScriptProcess ||
445 this.injectedInto.has(window.document)
449 this.injectedInto.add(window.document);
451 let context = this.extension.getContext(window);
452 for (let script of this.matcher.jsPaths) {
453 context.logActivity(this.scriptType, script, {
454 url: window.location.href,
459 if (this.runAt === "document_end") {
460 await promiseDocumentReady(window.document);
461 } else if (this.runAt === "document_idle") {
463 promiseDocumentIdle(window),
464 promiseDocumentLoaded(window.document),
468 return this.inject(context, reportExceptions);
470 return Promise.reject(context.normalizeError(e));
475 * Tries to inject this script into the given window and sandbox, if
476 * there are pending operations for the window's current load state.
478 * @param {ContentScriptContextChild} context
479 * The content script context into which to inject the scripts.
480 * @param {boolean} reportExceptions
481 * Defaults to true and reports any exception directly to the console
482 * and no exception will be thrown out of this function.
483 * @returns {Promise<any>}
484 * Resolves to the last value in the evaluated script, when
485 * execution is complete.
487 async inject(context, reportExceptions = true) {
488 DocumentManager.lazyInit();
489 if (this.requiresCleanup) {
490 context.addScript(this);
493 const { cssCodeHash } = this;
496 if (this.css.length || cssCodeHash) {
497 let window = context.contentWindow;
498 let { windowUtils } = window;
501 this.cssOrigin === "user"
502 ? windowUtils.USER_SHEET
503 : windowUtils.AUTHOR_SHEET;
505 if (this.removeCSS) {
506 for (let url of this.css) {
507 this.cssCache.deleteDocument(url, window.document);
509 runSafeSyncWithoutClone(
510 windowUtils.removeSheetUsingURIString,
516 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
517 const { uri } = await this.cssCodeCache.get(cssCodeHash);
518 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
520 runSafeSyncWithoutClone(windowUtils.removeSheet, uri, type);
523 cssPromise = Promise.all(this.loadCSS()).then(sheets => {
524 let window = context.contentWindow;
529 for (let { url, sheet } of sheets) {
530 this.cssCache.addDocument(url, window.document);
532 runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
537 cssPromise = cssPromise.then(async () => {
538 const { sheet } = await this.cssCodeCache.get(cssCodeHash);
539 this.cssCodeCache.addDocument(cssCodeHash, window.document);
541 runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
545 // We're loading stylesheets via the stylesheet service, which means
546 // that the normal mechanism for blocking layout and onload for pending
547 // stylesheets aren't in effect (since there's no document to block). So
548 // we need to do something custom here, similar to what we do for
549 // scripts. Blocking parsing is overkill, since we really just want to
550 // block layout and onload. But we have an API to do the former and not
551 // the latter, so we do it that way. This hopefully isn't a performance
552 // problem since there are no network loads involved, and since we cache
553 // the stylesheets on first load. We should fix this up if it does becomes
555 if (this.css.length) {
556 context.contentWindow.document.blockParsing(cssPromise, {
557 blockScriptCreated: false,
563 let scripts = this.getCompiledScripts(context);
564 if (scripts instanceof Promise) {
565 scripts = await scripts;
568 // Make sure we've injected any related CSS before we run content scripts.
573 const { extension } = context;
575 // The evaluations below may throw, in which case the promise will be
576 // automatically rejected.
577 lazy.ExtensionTelemetry.contentScriptInjection.stopwatchStart(
582 for (let script of scripts) {
583 result = script.executeInGlobal(context.cloneScope, {
588 if (this.matcher.jsCode) {
589 result = Cu.evalInSandbox(
598 lazy.ExtensionTelemetry.contentScriptInjection.stopwatchFinish(
608 * Get the compiled scripts (if they are already precompiled and cached) or a promise which resolves
609 * to the precompiled scripts (once they have been compiled and cached).
611 * @param {ContentScriptContextChild} context
612 * The document to block the parsing on, if the scripts are not yet precompiled and cached.
614 * @returns {Array<PreloadedScript> | Promise<Array<PreloadedScript>>}
615 * Returns an array of preloaded scripts if they are already available, or a promise which
616 * resolves to the array of the preloaded scripts once they are precompiled and cached.
618 getCompiledScripts(context) {
619 let scriptPromises = this.compileScripts();
620 let scripts = scriptPromises.map(promise => promise.script);
622 // If not all scripts are already available in the cache, block
623 // parsing and wait all promises to resolve.
624 if (!scripts.every(script => script)) {
625 let promise = Promise.all(scriptPromises);
627 // If there is any syntax error, the script promises will be rejected.
629 // Notify the exception directly to the console so that it can
630 // be displayed in the web console by flagging the error with the right
632 for (const p of scriptPromises) {
634 Services.console.logMessage(
636 `${error.name}: ${error.message}`,
641 Ci.nsIScriptError.errorFlag,
642 "content javascript",
643 context.innerWindowID
649 // If we're supposed to inject at the start of the document load,
650 // and we haven't already missed that point, block further parsing
651 // until the scripts have been loaded.
652 const { document } = context.contentWindow;
654 this.runAt === "document_start" &&
655 document.readyState !== "complete"
657 document.blockParsing(promise, { blockScriptCreated: false });
667 // Represents a user script.
668 class UserScript extends Script {
670 * @param {BrowserExtensionContent} extension
671 * @param {WebExtensionContentScript|object} matcher
672 * An object with a "matchesWindowGlobal" method and content script
675 constructor(extension, matcher) {
676 super(extension, matcher);
677 this.scriptType = "user_script";
679 // This is an opaque object that the extension provides, it is associated to
680 // the particular userScript and it is passed as a parameter to the custom
681 // userScripts APIs defined by the extension.
682 this.scriptMetadata = matcher.userScriptOptions.scriptMetadata;
684 extension.manifest.user_scripts &&
685 extension.manifest.user_scripts.api_script;
687 // Add the apiScript to the js scripts to compile.
688 if (this.apiScriptURL) {
689 this.js = [this.apiScriptURL].concat(this.js);
692 // WeakMap<ContentScriptContextChild, Sandbox>
693 this.sandboxes = new DefaultWeakMap(context => {
694 return this.createSandbox(context);
698 async inject(context) {
699 DocumentManager.lazyInit();
701 let scripts = this.getCompiledScripts(context);
702 if (scripts instanceof Promise) {
703 scripts = await scripts;
706 let apiScript, sandboxScripts;
708 if (this.apiScriptURL) {
709 [apiScript, ...sandboxScripts] = scripts;
711 sandboxScripts = scripts;
714 // Load and execute the API script once per context.
716 context.executeAPIScript(apiScript);
719 let userScriptSandbox = this.sandboxes.get(context);
721 context.callOnClose({
723 // Destroy the userScript sandbox when the related ContentScriptContextChild instance
725 this.sandboxes.delete(context);
726 Cu.nukeSandbox(userScriptSandbox);
730 // Notify listeners subscribed to the userScripts.onBeforeScript API event,
731 // to allow extension API script to provide its custom APIs to the userScript.
733 context.userScriptsEvents.emit(
740 for (let script of sandboxScripts) {
741 script.executeInGlobal(userScriptSandbox);
745 createSandbox(context) {
746 const { contentWindow } = context;
747 const contentPrincipal = contentWindow.document.nodePrincipal;
748 const ssm = Services.scriptSecurityManager;
751 if (contentPrincipal.isSystemPrincipal) {
752 principal = ssm.createNullPrincipal(contentPrincipal.originAttributes);
754 principal = [contentPrincipal];
757 const sandbox = Cu.Sandbox(principal, {
758 sandboxName: `User Script registered by ${this.extension.policy.debugName}`,
759 sandboxPrototype: contentWindow,
760 sameZoneAs: contentWindow,
762 wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
763 originAttributes: contentPrincipal.originAttributes,
765 "inner-window-id": context.innerWindowID,
766 addonId: this.extension.policy.id,
774 var contentScripts = new DefaultWeakMap(matcher => {
775 const extension = lazy.ExtensionProcessScript.extensions.get(
779 if ("userScriptOptions" in matcher) {
780 return new UserScript(extension, matcher);
783 return new Script(extension, matcher);
787 * An execution context for semi-privileged extension content scripts.
789 * This is the child side of the ContentScriptContextParent class
790 * defined in ExtensionParent.sys.mjs.
792 class ContentScriptContextChild extends BaseContext {
793 constructor(extension, contentWindow) {
794 super("content_child", extension);
796 this.setContentWindow(contentWindow);
798 let frameId = lazy.WebNavigationFrames.getFrameId(contentWindow);
799 this.frameId = frameId;
801 this.browsingContextId = contentWindow.docShell.browsingContext.id;
805 let contentPrincipal = contentWindow.document.nodePrincipal;
806 let ssm = Services.scriptSecurityManager;
808 // Copy origin attributes from the content window origin attributes to
809 // preserve the user context id.
810 let attrs = contentPrincipal.originAttributes;
811 let extensionPrincipal = ssm.createContentPrincipal(
812 this.extension.baseURI,
816 this.isExtensionPage = contentPrincipal.equals(extensionPrincipal);
818 if (this.isExtensionPage) {
819 // This is an iframe with content script API enabled and its principal
820 // should be the contentWindow itself. We create a sandbox with the
821 // contentWindow as principal and with X-rays disabled because it
822 // enables us to create the APIs object in this sandbox object and then
823 // copying it into the iframe's window. See bug 1214658.
824 this.sandbox = Cu.Sandbox(contentWindow, {
825 sandboxName: `Web-Accessible Extension Page ${extension.policy.debugName}`,
826 sandboxPrototype: contentWindow,
827 sameZoneAs: contentWindow,
829 isWebExtensionContentScript: true,
833 if (contentPrincipal.isSystemPrincipal) {
834 // Make sure we don't hand out the system principal by accident.
835 // Also make sure that the null principal has the right origin attributes.
836 principal = ssm.createNullPrincipal(attrs);
838 principal = [contentPrincipal, extensionPrincipal];
840 // This metadata is required by the Developer Tools, in order for
841 // the content script to be associated with both the extension and
842 // the tab holding the content page.
844 "inner-window-id": this.innerWindowID,
845 addonId: extensionPrincipal.addonId,
848 let isMV2 = extension.manifestVersion == 2;
849 let wantGlobalProperties;
851 // In MV2, fetch/XHR support cross-origin requests.
852 // WebSocket was also included to avoid CSP effects (bug 1676024).
853 wantGlobalProperties = ["XMLHttpRequest", "fetch", "WebSocket"];
855 // In MV3, fetch/XHR have the same capabilities as the web page.
856 wantGlobalProperties = [];
858 this.sandbox = Cu.Sandbox(principal, {
860 sandboxName: `Content Script ${extension.policy.debugName}`,
861 sandboxPrototype: contentWindow,
862 sameZoneAs: contentWindow,
864 isWebExtensionContentScript: true,
865 wantExportHelpers: true,
866 wantGlobalProperties,
867 originAttributes: attrs,
870 // Preserve a copy of the original Error and Promise globals from the sandbox object,
871 // which are used in the WebExtensions internals (before any content script code had
872 // any chance to redefine them).
873 this.cloneScopePromise = this.sandbox.Promise;
874 this.cloneScopeError = this.sandbox.Error;
877 // Preserve a copy of the original window's XMLHttpRequest and fetch
878 // in a content object (fetch is manually binded to the window
879 // to prevent it from raising a TypeError because content object is not
884 XMLHttpRequest: window.XMLHttpRequest,
885 fetch: window.fetch.bind(window),
886 WebSocket: window.WebSocket,
890 window.XMLHttpRequest = XMLHttpRequest;
891 window.fetch = fetch;
892 window.WebSocket = WebSocket;
897 // The sandbox's JSON API can deal with values from the sandbox and the
898 // contentWindow, but window.JSON cannot (and it could potentially be
899 // spoofed by the web page). jQuery.parseJSON relies on window.JSON.
900 Cu.evalInSandbox("window.JSON = JSON;", this.sandbox);
904 Object.defineProperty(this, "principal", {
905 value: Cu.getObjectPrincipal(this.sandbox),
910 this.url = contentWindow.location.href;
912 lazy.Schemas.exportLazyGetter(
917 lazy.Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
919 // Keep track if the userScript API script has been already executed in this context
920 // (e.g. because there are more then one UserScripts that match the related webpage
921 // and so the UserScript apiScript has already been executed).
922 this.hasUserScriptAPIs = false;
924 // A lazy created EventEmitter related to userScripts-specific events.
925 defineLazyGetter(this, "userScriptsEvents", () => {
926 return new ExtensionCommon.EventEmitter();
931 if (!this.isExtensionPage) {
932 throw new Error("Cannot inject extension API into non-extension window");
935 // This is an iframe with content script API enabled (See Bug 1214658)
936 lazy.Schemas.exportLazyGetter(
941 lazy.Schemas.exportLazyGetter(
948 async logActivity(type, name, data) {
949 ExtensionActivityLogChild.log(this, type, name, data);
956 async executeAPIScript(apiScript) {
957 // Execute the UserScript apiScript only once per context (e.g. more then one UserScripts
958 // match the same webpage and the apiScript has already been executed).
959 if (apiScript && !this.hasUserScriptAPIs) {
960 this.hasUserScriptAPIs = true;
961 apiScript.executeInGlobal(this.cloneScope);
966 if (script.requiresCleanup) {
967 this.scripts.push(script);
974 // Cleanup the scripts even if the contentWindow have been destroyed.
975 for (let script of this.scripts) {
976 script.cleanup(this.contentWindow);
979 if (this.contentWindow) {
980 // Overwrite the content script APIs with an empty object if the APIs objects are still
981 // defined in the content window (See Bug 1214658).
982 if (this.isExtensionPage) {
983 Cu.createObjectIn(this.contentWindow, { defineAs: "browser" });
984 Cu.createObjectIn(this.contentWindow, { defineAs: "chrome" });
987 Cu.nukeSandbox(this.sandbox);
993 apiManager.lazyInit();
994 let can = new CanOfAPIs(this, apiManager, {});
995 let childManager = new ChildAPIManager(this, this.messageManager, can, {
996 envType: "content_parent",
999 this.callOnClose(childManager);
1000 return redefineGetter(this, "childManager", childManager);
1004 let chromeObj = Cu.createObjectIn(this.sandbox);
1005 this.childManager.inject(chromeObj);
1006 return redefineGetter(this, "chromeObj", chromeObj);
1010 return redefineGetter(this, "messenger", new Messenger(this));
1014 // Responsible for creating ExtensionContexts and injecting content
1015 // scripts into them when new documents are created.
1017 // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
1018 contexts: new Map(),
1023 if (this.initialized) {
1026 this.initialized = true;
1028 Services.obs.addObserver(this, "inner-window-destroyed");
1029 Services.obs.addObserver(this, "memory-pressure");
1033 Services.obs.removeObserver(this, "inner-window-destroyed");
1034 Services.obs.removeObserver(this, "memory-pressure");
1038 "inner-window-destroyed"(subject) {
1039 let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
1041 // Close any existent content-script context for the destroyed window.
1042 if (this.contexts.has(windowId)) {
1043 let extensions = this.contexts.get(windowId);
1044 for (let context of extensions.values()) {
1048 this.contexts.delete(windowId);
1051 "memory-pressure"(subject, topic, data) {
1052 let timeout = data === "heap-minimize" ? 0 : undefined;
1054 for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(
1057 cache.clear(timeout);
1063 * @param {object} subject
1064 * @param {keyof typeof DocumentManager.observers} topic
1067 observe(subject, topic, data) {
1068 this.observers[topic].call(this, subject, topic, data);
1071 shutdownExtension(extension) {
1072 for (let extensions of this.contexts.values()) {
1073 let context = extensions.get(extension);
1076 extensions.delete(extension);
1081 getContexts(window) {
1082 let winId = getInnerWindowID(window);
1084 let extensions = this.contexts.get(winId);
1086 extensions = new Map();
1087 this.contexts.set(winId, extensions);
1093 // For test use only.
1094 getContext(extensionId, window) {
1095 for (let [extension, context] of this.getContexts(window)) {
1096 if (extension.id === extensionId) {
1102 getContentScriptGlobals(window) {
1103 let extensions = this.contexts.get(getInnerWindowID(window));
1106 return Array.from(extensions.values(), ctx => ctx.sandbox);
1112 initExtensionContext(extension, window) {
1113 extension.getContext(window).injectAPI();
1117 export var ExtensionContent = {
1118 BrowserExtensionContent,
1122 shutdownExtension(extension) {
1123 DocumentManager.shutdownExtension(extension);
1126 // This helper is exported to be integrated in the devtools RDP actors,
1127 // that can use it to retrieve the existent WebExtensions ContentScripts
1128 // of a target window and be able to show the ContentScripts source in the
1129 // DevTools Debugger panel.
1130 getContentScriptGlobals(window) {
1131 return DocumentManager.getContentScriptGlobals(window);
1134 initExtensionContext(extension, window) {
1135 DocumentManager.initExtensionContext(extension, window);
1138 getContext(extension, window) {
1139 let extensions = DocumentManager.getContexts(window);
1141 let context = extensions.get(extension);
1143 context = new ContentScriptContextChild(extension, window);
1144 extensions.set(extension, context);
1149 // For test use only.
1150 getContextByExtensionId(extensionId, window) {
1151 return DocumentManager.getContext(extensionId, window);
1154 async handleDetectLanguage({ windows }) {
1155 let wgc = WindowGlobalChild.getByInnerWindowId(windows[0]);
1156 let doc = wgc.browsingContext.window.document;
1157 await promiseDocumentReady(doc);
1159 // The CLD2 library can analyze HTML, but that uses more memory, and
1160 // emscripten can't shrink its heap, so we use plain text instead.
1161 let encoder = Cu.createDocumentEncoder("text/plain");
1162 encoder.init(doc, "text/plain", Ci.nsIDocumentEncoder.SkipInvisibleContent);
1164 let result = await lazy.LanguageDetector.detectLanguage({
1166 doc.documentElement.getAttribute("xml:lang") ||
1167 doc.documentElement.getAttribute("lang") ||
1168 doc.contentLanguage ||
1170 tld: doc.location.hostname.match(/[a-z]*$/)[0],
1171 text: encoder.encodeToStringWithMaxLength(60 * 1024),
1172 encoding: doc.characterSet,
1174 return result.language === "un" ? "und" : result.language;
1177 // Activate MV3 content scripts in all same-origin frames for this tab.
1178 handleActivateScripts({ options, windows }) {
1179 let policy = WebExtensionPolicy.getByID(options.id);
1181 // Order content scripts by run_at timing.
1182 let runAt = { document_start: [], document_end: [], document_idle: [] };
1183 for (let matcher of policy.contentScripts) {
1184 runAt[matcher.runAt].push(this.contentScripts.get(matcher));
1187 // If we got here, checks in TabManagerBase.activateScripts assert:
1188 // 1) this is a MV3 extension, with Origin Controls,
1189 // 2) with a host permission (or content script) for the tab's top origin,
1190 // 3) and that host permission hasn't been granted yet.
1192 // We treat the action click as implicit user's choice to activate the
1193 // extension on the current site, so we can safely run (matching) content
1194 // scripts in all sameOriginWithTop frames while ignoring host permission.
1196 let { browsingContext } = WindowGlobalChild.getByInnerWindowId(windows[0]);
1197 for (let bc of browsingContext.getAllBrowsingContextsInSubtree()) {
1198 let wgc = bc.currentWindowContext.windowGlobalChild;
1199 if (wgc?.sameOriginWithTop) {
1200 // This is TOCTOU safe: if a frame navigated after same-origin check,
1201 // wgc.isClosed would be true and .matchesWindowGlobal() would fail.
1202 const runScript = cs => {
1203 if (cs.matchesWindowGlobal(wgc, /* ignorePermissions */ true)) {
1204 return cs.injectInto(bc.window);
1208 // Inject all matching content scripts in proper run_at order.
1209 Promise.all(runAt.document_start.map(runScript))
1210 .then(() => Promise.all(runAt.document_end.map(runScript)))
1211 .then(() => Promise.all(runAt.document_idle.map(runScript)));
1216 // Used to executeScript, insertCSS and removeCSS.
1217 async handleActorExecute({ options, windows }) {
1218 let policy = WebExtensionPolicy.getByID(options.extensionId);
1219 // `WebExtensionContentScript` uses `MozDocumentMatcher::Matches` to ensure
1220 // that a script can be run in a document. That requires either `frameId`
1221 // or `allFrames` to be set. When `frameIds` (plural) is used, we force
1222 // `allFrames` to be `true` in order to match any frame. This is OK because
1223 // `executeInWin()` below looks up the window for the given `frameIds`
1224 // immediately before `script.injectInto()`. Due to this, we won't run
1225 // scripts in windows with non-matching `frameId`, despite `allFrames`
1226 // being set to `true`.
1227 if (options.frameIds) {
1228 options.allFrames = true;
1230 let matcher = new WebExtensionContentScript(policy, options);
1232 Object.assign(matcher, {
1233 wantReturnValue: options.wantReturnValue,
1234 removeCSS: options.removeCSS,
1235 cssOrigin: options.cssOrigin,
1236 jsCode: options.jsCode,
1238 let script = contentScripts.get(matcher);
1240 // Add the cssCode to the script, so that it can be converted into a cached URL.
1241 await script.addCSSCode(options.cssCode);
1242 delete options.cssCode;
1244 const executeInWin = innerId => {
1245 let wg = WindowGlobalChild.getByInnerWindowId(innerId);
1246 if (wg?.isCurrentGlobal && script.matchesWindowGlobal(wg)) {
1247 let bc = wg.browsingContext;
1250 frameId: bc.parent ? bc.id : 0,
1251 // Disable exception reporting directly to the console
1252 // in order to pass the exceptions back to the callsite.
1253 promise: script.injectInto(bc.window, false),
1258 let promisesWithFrameIds = windows.map(executeInWin).filter(obj => obj);
1260 let result = await Promise.all(
1261 promisesWithFrameIds.map(async ({ frameId, promise }) => {
1262 if (!options.returnResultsWithFrameIds) {
1267 const result = await promise;
1269 return { frameId, result };
1271 return { frameId, error };
1275 // This is useful when we do not return results/errors with frame IDs in
1276 // the promises above.
1277 e => Promise.reject({ message: e.message })
1281 // Check if the result can be structured-cloned before sending back.
1282 return Cu.cloneInto(result, this);
1284 let path = options.jsPaths.slice(-1)[0] ?? "<anonymous code>";
1285 let message = `Script '${path}' result is non-structured-clonable data`;
1286 return Promise.reject({ message, fileName: path });
1292 * Child side of the ExtensionContent process actor, handles some tabs.* APIs.
1294 export class ExtensionContentChild extends JSProcessActorChild {
1295 receiveMessage({ name, data }) {
1296 if (!lazy.isContentScriptProcess) {
1300 case "DetectLanguage":
1301 return ExtensionContent.handleDetectLanguage(data);
1303 return ExtensionContent.handleActorExecute(data);
1304 case "ActivateScripts":
1305 return ExtensionContent.handleActivateScripts(data);