Bug 1885565 - Part 1: Add mozac_ic_avatar_circle_24 to ui-icons r=android-reviewers...
[gecko.git] / toolkit / components / extensions / ExtensionContent.sys.mjs
blob015d1bc7c69142be68685283abab812259e68ea5
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";
10 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   ExtensionProcessScript:
14     "resource://gre/modules/ExtensionProcessScript.sys.mjs",
15   ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
16   LanguageDetector:
17     "resource://gre/modules/translation/LanguageDetector.sys.mjs",
18   Schemas: "resource://gre/modules/Schemas.sys.mjs",
19   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs",
20 });
22 XPCOMUtils.defineLazyServiceGetter(
23   lazy,
24   "styleSheetService",
25   "@mozilla.org/content/style-sheet-service;1",
26   "nsIStyleSheetService"
29 const Timer = Components.Constructor(
30   "@mozilla.org/timer;1",
31   "nsITimer",
32   "initWithCallback"
35 const ScriptError = Components.Constructor(
36   "@mozilla.org/scripterror;1",
37   "nsIScriptError",
38   "initWithWindowID"
41 import {
42   ExtensionChild,
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";
48 const {
49   DefaultMap,
50   DefaultWeakMap,
51   getInnerWindowID,
52   promiseDocumentIdle,
53   promiseDocumentLoaded,
54   promiseDocumentReady,
55 } = ExtensionUtils;
57 const {
58   BaseContext,
59   CanOfAPIs,
60   SchemaAPIManager,
61   defineLazyGetter,
62   redefineGetter,
63   runSafeSyncWithoutClone,
64 } = ExtensionCommon;
66 const { BrowserExtensionContent, ChildAPIManager, Messenger } = ExtensionChild;
68 ChromeUtils.defineLazyGetter(lazy, "isContentScriptProcess", () => {
69   return (
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"
74   );
75 });
77 var DocumentManager;
79 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
81 var apiManager = new (class extends SchemaAPIManager {
82   constructor() {
83     super("content", lazy.Schemas);
84     this.initialized = false;
85   }
87   lazyInit() {
88     if (!this.initialized) {
89       this.initialized = true;
90       this.initGlobal();
91       for (let { value } of Services.catMan.enumerateCategory(
92         CATEGORY_EXTENSION_SCRIPTS_CONTENT
93       )) {
94         this.loadScript(value);
95       }
96     }
97   }
98 })();
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) {
111     super(getter);
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", () => {
121       this.clear(-1);
122     });
123   }
125   get(url) {
126     let promise = super.get(url);
128     promise.lastUsed = Date.now();
129     if (promise.timer) {
130       promise.timer.cancel();
131     }
132     promise.timer = Timer(
133       this.delete.bind(this, url),
134       this.expiryTimeout,
135       Ci.nsITimer.TYPE_ONE_SHOT
136     );
138     return promise;
139   }
141   delete(url) {
142     if (this.has(url)) {
143       super.get(url).timer.cancel();
144     }
146     return super.delete(url);
147   }
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) {
156         this.delete(url);
157       }
158     }
159   }
162 class ScriptCache extends CacheMap {
163   constructor(options, extension) {
164     super(
165       SCRIPT_EXPIRY_TIMEOUT_MS,
166       url => {
167         let promise = ChromeUtils.compileScript(url, options);
168         promise.then(script => {
169           promise.script = script;
170         });
171         return promise;
172       },
173       extension
174     );
175   }
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).
182  */
183 class BaseCSSCache extends CacheMap {
184   constructor(expiryTimeout, defaultConstructor, extension) {
185     super(expiryTimeout, defaultConstructor, extension);
186   }
188   addDocument(key, document) {
189     sheetCacheDocuments.get(this.get(key)).add(document);
190   }
192   deleteDocument(key, document) {
193     sheetCacheDocuments.get(this.get(key)).delete(document);
194   }
196   delete(key) {
197     if (this.has(key)) {
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
203       // still in use.
204       let docs = ChromeUtils.nondeterministicGetWeakSetKeys(
205         sheetCacheDocuments.get(promise)
206       );
207       if (docs.length) {
208         return;
209       }
210     }
212     return super.delete(key);
213   }
217  * Cache of the preloaded stylesheet defined by url.
218  */
219 class CSSCache extends BaseCSSCache {
220   constructor(sheetType, extension) {
221     super(
222       CSS_EXPIRY_TIMEOUT_MS,
223       url => {
224         let uri = Services.io.newURI(url);
225         return lazy.styleSheetService
226           .preloadSheetAsync(uri, sheetType)
227           .then(sheet => {
228             return { url, sheet };
229           });
230       },
231       extension
232     );
233   }
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.
239  */
240 class CSSCodeCache extends BaseCSSCache {
241   constructor(sheetType, extension) {
242     super(
243       CSSCODE_EXPIRY_TIMEOUT_MS,
244       hash => {
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.
248           throw new Error(
249             "Unexistent cached cssCode stylesheet: " + Error().stack
250           );
251         }
253         return super.get(hash);
254       },
255       extension
256     );
258     // Store the preferred sheetType (used to preload the expected stylesheet type in
259     // the addCSSCode method).
260     this.sheetType = sheetType;
261   }
263   addCSSCode(hash, cssCode) {
264     if (this.has(hash)) {
265       // This cssCode have been already cached, no need to create it again.
266       return;
267     }
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)
274     );
275     const value = lazy.styleSheetService
276       .preloadSheetAsync(uri, this.sheetType)
277       .then(sheet => {
278         return { sheet, uri };
279       });
281     super.set(hash, value);
282   }
285 defineLazyGetter(
286   BrowserExtensionContent.prototype,
287   "staticScripts",
288   function () {
289     return new ScriptCache({ hasReturnValue: false }, this);
290   }
293 defineLazyGetter(
294   BrowserExtensionContent.prototype,
295   "dynamicScripts",
296   function () {
297     return new ScriptCache({ hasReturnValue: true }, this);
298   }
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);
316 defineLazyGetter(
317   BrowserExtensionContent.prototype,
318   "authorCSSCode",
319   function () {
320     return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
321   }
324 // Represents a content script.
325 class Script {
326   /**
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
334    */
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;
348     this.cssCache =
349       extension[this.cssOrigin === "user" ? "userCSS" : "authorCSS"];
350     this.cssCodeCache =
351       extension[this.cssOrigin === "user" ? "userCSSCode" : "authorCSSCode"];
352     this.scriptCache =
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();
360       this.loadCSS();
361     }
362   }
364   get requiresCleanup() {
365     return !this.removeCSS && (!!this.css.length || this.cssCodeHash);
366   }
368   async addCSSCode(cssCode) {
369     if (!cssCode) {
370       return;
371     }
373     // Store the hash of the cssCode.
374     const buffer = await crypto.subtle.digest(
375       "SHA-1",
376       new TextEncoder().encode(cssCode)
377     );
378     this.cssCodeHash = String.fromCharCode(...new Uint16Array(buffer));
380     // Cache and preload the cssCode stylesheet.
381     this.cssCodeCache.addCSSCode(this.cssCodeHash, cssCode);
382   }
384   compileScripts() {
385     return this.js.map(url => this.scriptCache.get(url));
386   }
388   loadCSS() {
389     return this.css.map(url => this.cssCache.get(url));
390   }
392   preload() {
393     this.loadCSS();
394     this.compileScripts();
395   }
397   cleanup(window) {
398     if (this.requiresCleanup) {
399       if (window) {
400         let { windowUtils } = window;
402         let type =
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,
413               url,
414               type
415             );
416           }
417         }
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);
425             });
426           }
427           this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
428         }
429       }
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);
435     }
436   }
438   matchesWindowGlobal(windowGlobal, ignorePermissions) {
439     return this.matcher.matchesWindowGlobal(windowGlobal, ignorePermissions);
440   }
442   async injectInto(window, reportExceptions = true) {
443     if (
444       !lazy.isContentScriptProcess ||
445       this.injectedInto.has(window.document)
446     ) {
447       return;
448     }
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,
455       });
456     }
458     try {
459       if (this.runAt === "document_end") {
460         await promiseDocumentReady(window.document);
461       } else if (this.runAt === "document_idle") {
462         await Promise.race([
463           promiseDocumentIdle(window),
464           promiseDocumentLoaded(window.document),
465         ]);
466       }
468       return this.inject(context, reportExceptions);
469     } catch (e) {
470       return Promise.reject(context.normalizeError(e));
471     }
472   }
474   /**
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.
477    *
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.
486    */
487   async inject(context, reportExceptions = true) {
488     DocumentManager.lazyInit();
489     if (this.requiresCleanup) {
490       context.addScript(this);
491     }
493     const { cssCodeHash } = this;
495     let cssPromise;
496     if (this.css.length || cssCodeHash) {
497       let window = context.contentWindow;
498       let { windowUtils } = window;
500       let type =
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,
511             url,
512             type
513           );
514         }
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);
521         }
522       } else {
523         cssPromise = Promise.all(this.loadCSS()).then(sheets => {
524           let window = context.contentWindow;
525           if (!window) {
526             return;
527           }
529           for (let { url, sheet } of sheets) {
530             this.cssCache.addDocument(url, window.document);
532             runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
533           }
534         });
536         if (cssCodeHash) {
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);
542           });
543         }
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
554         // a problem.
555         if (this.css.length) {
556           context.contentWindow.document.blockParsing(cssPromise, {
557             blockScriptCreated: false,
558           });
559         }
560       }
561     }
563     let scripts = this.getCompiledScripts(context);
564     if (scripts instanceof Promise) {
565       scripts = await scripts;
566     }
568     // Make sure we've injected any related CSS before we run content scripts.
569     await cssPromise;
571     let result;
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(
578       extension,
579       context
580     );
581     try {
582       for (let script of scripts) {
583         result = script.executeInGlobal(context.cloneScope, {
584           reportExceptions,
585         });
586       }
588       if (this.matcher.jsCode) {
589         result = Cu.evalInSandbox(
590           this.matcher.jsCode,
591           context.cloneScope,
592           "latest",
593           "sandbox eval code",
594           1
595         );
596       }
597     } finally {
598       lazy.ExtensionTelemetry.contentScriptInjection.stopwatchFinish(
599         extension,
600         context
601       );
602     }
604     return result;
605   }
607   /**
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).
610    *
611    * @param {ContentScriptContextChild} context
612    *        The document to block the parsing on, if the scripts are not yet precompiled and cached.
613    *
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.
617    */
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.
628       //
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
631       // innerWindowID.
632       for (const p of scriptPromises) {
633         p.catch(error => {
634           Services.console.logMessage(
635             new ScriptError(
636               `${error.name}: ${error.message}`,
637               error.fileName,
638               null,
639               error.lineNumber,
640               error.columnNumber,
641               Ci.nsIScriptError.errorFlag,
642               "content javascript",
643               context.innerWindowID
644             )
645           );
646         });
647       }
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;
653       if (
654         this.runAt === "document_start" &&
655         document.readyState !== "complete"
656       ) {
657         document.blockParsing(promise, { blockScriptCreated: false });
658       }
660       return promise;
661     }
663     return scripts;
664   }
667 // Represents a user script.
668 class UserScript extends Script {
669   /**
670    * @param {BrowserExtensionContent} extension
671    * @param {WebExtensionContentScript|object} matcher
672    *        An object with a "matchesWindowGlobal" method and content script
673    *        execution details.
674    */
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;
683     this.apiScriptURL =
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);
690     }
692     // WeakMap<ContentScriptContextChild, Sandbox>
693     this.sandboxes = new DefaultWeakMap(context => {
694       return this.createSandbox(context);
695     });
696   }
698   async inject(context) {
699     DocumentManager.lazyInit();
701     let scripts = this.getCompiledScripts(context);
702     if (scripts instanceof Promise) {
703       scripts = await scripts;
704     }
706     let apiScript, sandboxScripts;
708     if (this.apiScriptURL) {
709       [apiScript, ...sandboxScripts] = scripts;
710     } else {
711       sandboxScripts = scripts;
712     }
714     // Load and execute the API script once per context.
715     if (apiScript) {
716       context.executeAPIScript(apiScript);
717     }
719     let userScriptSandbox = this.sandboxes.get(context);
721     context.callOnClose({
722       close: () => {
723         // Destroy the userScript sandbox when the related ContentScriptContextChild instance
724         // is being closed.
725         this.sandboxes.delete(context);
726         Cu.nukeSandbox(userScriptSandbox);
727       },
728     });
730     // Notify listeners subscribed to the userScripts.onBeforeScript API event,
731     // to allow extension API script to provide its custom APIs to the userScript.
732     if (apiScript) {
733       context.userScriptsEvents.emit(
734         "on-before-script",
735         this.scriptMetadata,
736         userScriptSandbox
737       );
738     }
740     for (let script of sandboxScripts) {
741       script.executeInGlobal(userScriptSandbox);
742     }
743   }
745   createSandbox(context) {
746     const { contentWindow } = context;
747     const contentPrincipal = contentWindow.document.nodePrincipal;
748     const ssm = Services.scriptSecurityManager;
750     let principal;
751     if (contentPrincipal.isSystemPrincipal) {
752       principal = ssm.createNullPrincipal(contentPrincipal.originAttributes);
753     } else {
754       principal = [contentPrincipal];
755     }
757     const sandbox = Cu.Sandbox(principal, {
758       sandboxName: `User Script registered by ${this.extension.policy.debugName}`,
759       sandboxPrototype: contentWindow,
760       sameZoneAs: contentWindow,
761       wantXrays: true,
762       wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
763       originAttributes: contentPrincipal.originAttributes,
764       metadata: {
765         "inner-window-id": context.innerWindowID,
766         addonId: this.extension.policy.id,
767       },
768     });
770     return sandbox;
771   }
774 var contentScripts = new DefaultWeakMap(matcher => {
775   const extension = lazy.ExtensionProcessScript.extensions.get(
776     matcher.extension
777   );
779   if ("userScriptOptions" in matcher) {
780     return new UserScript(extension, matcher);
781   }
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.
791  */
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;
803     this.scripts = [];
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,
813       attrs
814     );
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,
828         wantXrays: false,
829         isWebExtensionContentScript: true,
830       });
831     } else {
832       let principal;
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);
837       } else {
838         principal = [contentPrincipal, extensionPrincipal];
839       }
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.
843       let metadata = {
844         "inner-window-id": this.innerWindowID,
845         addonId: extensionPrincipal.addonId,
846       };
848       let isMV2 = extension.manifestVersion == 2;
849       let wantGlobalProperties;
850       if (isMV2) {
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"];
854       } else {
855         // In MV3, fetch/XHR have the same capabilities as the web page.
856         wantGlobalProperties = [];
857       }
858       this.sandbox = Cu.Sandbox(principal, {
859         metadata,
860         sandboxName: `Content Script ${extension.policy.debugName}`,
861         sandboxPrototype: contentWindow,
862         sameZoneAs: contentWindow,
863         wantXrays: true,
864         isWebExtensionContentScript: true,
865         wantExportHelpers: true,
866         wantGlobalProperties,
867         originAttributes: attrs,
868       });
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;
876       if (isMV2) {
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
880         // a real window).
881         Cu.evalInSandbox(
882           `
883           this.content = {
884             XMLHttpRequest: window.XMLHttpRequest,
885             fetch: window.fetch.bind(window),
886             WebSocket: window.WebSocket,
887           };
889           window.JSON = JSON;
890           window.XMLHttpRequest = XMLHttpRequest;
891           window.fetch = fetch;
892           window.WebSocket = WebSocket;
893         `,
894           this.sandbox
895         );
896       } else {
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);
901       }
902     }
904     Object.defineProperty(this, "principal", {
905       value: Cu.getObjectPrincipal(this.sandbox),
906       enumerable: true,
907       configurable: true,
908     });
910     this.url = contentWindow.location.href;
912     lazy.Schemas.exportLazyGetter(
913       this.sandbox,
914       "browser",
915       () => this.chromeObj
916     );
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();
927     });
928   }
930   injectAPI() {
931     if (!this.isExtensionPage) {
932       throw new Error("Cannot inject extension API into non-extension window");
933     }
935     // This is an iframe with content script API enabled (See Bug 1214658)
936     lazy.Schemas.exportLazyGetter(
937       this.contentWindow,
938       "browser",
939       () => this.chromeObj
940     );
941     lazy.Schemas.exportLazyGetter(
942       this.contentWindow,
943       "chrome",
944       () => this.chromeObj
945     );
946   }
948   async logActivity(type, name, data) {
949     ExtensionActivityLogChild.log(this, type, name, data);
950   }
952   get cloneScope() {
953     return this.sandbox;
954   }
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);
962     }
963   }
965   addScript(script) {
966     if (script.requiresCleanup) {
967       this.scripts.push(script);
968     }
969   }
971   close() {
972     super.unload();
974     // Cleanup the scripts even if the contentWindow have been destroyed.
975     for (let script of this.scripts) {
976       script.cleanup(this.contentWindow);
977     }
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" });
985       }
986     }
987     Cu.nukeSandbox(this.sandbox);
989     this.sandbox = null;
990   }
992   get childManager() {
993     apiManager.lazyInit();
994     let can = new CanOfAPIs(this, apiManager, {});
995     let childManager = new ChildAPIManager(this, this.messageManager, can, {
996       envType: "content_parent",
997       url: this.url,
998     });
999     this.callOnClose(childManager);
1000     return redefineGetter(this, "childManager", childManager);
1001   }
1003   get chromeObj() {
1004     let chromeObj = Cu.createObjectIn(this.sandbox);
1005     this.childManager.inject(chromeObj);
1006     return redefineGetter(this, "chromeObj", chromeObj);
1007   }
1009   get messenger() {
1010     return redefineGetter(this, "messenger", new Messenger(this));
1011   }
1014 // Responsible for creating ExtensionContexts and injecting content
1015 // scripts into them when new documents are created.
1016 DocumentManager = {
1017   // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
1018   contexts: new Map(),
1020   initialized: false,
1022   lazyInit() {
1023     if (this.initialized) {
1024       return;
1025     }
1026     this.initialized = true;
1028     Services.obs.addObserver(this, "inner-window-destroyed");
1029     Services.obs.addObserver(this, "memory-pressure");
1030   },
1032   uninit() {
1033     Services.obs.removeObserver(this, "inner-window-destroyed");
1034     Services.obs.removeObserver(this, "memory-pressure");
1035   },
1037   observers: {
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()) {
1045           context.close();
1046         }
1048         this.contexts.delete(windowId);
1049       }
1050     },
1051     "memory-pressure"(subject, topic, data) {
1052       let timeout = data === "heap-minimize" ? 0 : undefined;
1054       for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(
1055         scriptCaches
1056       )) {
1057         cache.clear(timeout);
1058       }
1059     },
1060   },
1062   /**
1063    * @param {object} subject
1064    * @param {keyof typeof DocumentManager.observers} topic
1065    * @param {any} data
1066    */
1067   observe(subject, topic, data) {
1068     this.observers[topic].call(this, subject, topic, data);
1069   },
1071   shutdownExtension(extension) {
1072     for (let extensions of this.contexts.values()) {
1073       let context = extensions.get(extension);
1074       if (context) {
1075         context.close();
1076         extensions.delete(extension);
1077       }
1078     }
1079   },
1081   getContexts(window) {
1082     let winId = getInnerWindowID(window);
1084     let extensions = this.contexts.get(winId);
1085     if (!extensions) {
1086       extensions = new Map();
1087       this.contexts.set(winId, extensions);
1088     }
1090     return extensions;
1091   },
1093   // For test use only.
1094   getContext(extensionId, window) {
1095     for (let [extension, context] of this.getContexts(window)) {
1096       if (extension.id === extensionId) {
1097         return context;
1098       }
1099     }
1100   },
1102   getContentScriptGlobals(window) {
1103     let extensions = this.contexts.get(getInnerWindowID(window));
1105     if (extensions) {
1106       return Array.from(extensions.values(), ctx => ctx.sandbox);
1107     }
1109     return [];
1110   },
1112   initExtensionContext(extension, window) {
1113     extension.getContext(window).injectAPI();
1114   },
1117 export var ExtensionContent = {
1118   BrowserExtensionContent,
1120   contentScripts,
1122   shutdownExtension(extension) {
1123     DocumentManager.shutdownExtension(extension);
1124   },
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);
1132   },
1134   initExtensionContext(extension, window) {
1135     DocumentManager.initExtensionContext(extension, window);
1136   },
1138   getContext(extension, window) {
1139     let extensions = DocumentManager.getContexts(window);
1141     let context = extensions.get(extension);
1142     if (!context) {
1143       context = new ContentScriptContextChild(extension, window);
1144       extensions.set(extension, context);
1145     }
1146     return context;
1147   },
1149   // For test use only.
1150   getContextByExtensionId(extensionId, window) {
1151     return DocumentManager.getContext(extensionId, window);
1152   },
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({
1165       language:
1166         doc.documentElement.getAttribute("xml:lang") ||
1167         doc.documentElement.getAttribute("lang") ||
1168         doc.contentLanguage ||
1169         null,
1170       tld: doc.location.hostname.match(/[a-z]*$/)[0],
1171       text: encoder.encodeToStringWithMaxLength(60 * 1024),
1172       encoding: doc.characterSet,
1173     });
1174     return result.language === "un" ? "und" : result.language;
1175   },
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));
1185     }
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);
1205           }
1206         };
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)));
1212       }
1213     }
1214   },
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;
1229     }
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,
1237     });
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;
1249         return {
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),
1254         };
1255       }
1256     };
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) {
1263           return promise;
1264         }
1266         try {
1267           const result = await promise;
1269           return { frameId, result };
1270         } catch (error) {
1271           return { frameId, error };
1272         }
1273       })
1274     ).catch(
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 })
1278     );
1280     try {
1281       // Check if the result can be structured-cloned before sending back.
1282       return Cu.cloneInto(result, this);
1283     } catch (e) {
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 });
1287     }
1288   },
1292  * Child side of the ExtensionContent process actor, handles some tabs.* APIs.
1293  */
1294 export class ExtensionContentChild extends JSProcessActorChild {
1295   receiveMessage({ name, data }) {
1296     if (!lazy.isContentScriptProcess) {
1297       return;
1298     }
1299     switch (name) {
1300       case "DetectLanguage":
1301         return ExtensionContent.handleDetectLanguage(data);
1302       case "Execute":
1303         return ExtensionContent.handleActorExecute(data);
1304       case "ActivateScripts":
1305         return ExtensionContent.handleActivateScripts(data);
1306     }
1307   }