Bug 1708243 - Part 2: stop using sender data from the child process r=robwu,agi
[gecko.git] / toolkit / components / extensions / ExtensionContent.jsm
blob92653179a003a6a7cfd0261bdd2508a30d8f4efc
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/. */
6 "use strict";
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",
23 });
25 XPCOMUtils.defineLazyServiceGetter(
26   this,
27   "styleSheetService",
28   "@mozilla.org/content/style-sheet-service;1",
29   "nsIStyleSheetService"
32 const Timer = Components.Constructor(
33   "@mozilla.org/timer;1",
34   "nsITimer",
35   "initWithCallback"
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"]);
50 const {
51   DefaultMap,
52   DefaultWeakMap,
53   getInnerWindowID,
54   promiseDocumentIdle,
55   promiseDocumentLoaded,
56   promiseDocumentReady,
57 } = ExtensionUtils;
59 const {
60   BaseContext,
61   CanOfAPIs,
62   SchemaAPIManager,
63   defineLazyGetter,
64   runSafeSyncWithoutClone,
65 } = ExtensionCommon;
67 const { BrowserExtensionContent, ChildAPIManager, Messenger } = ExtensionChild;
69 XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
71 XPCOMUtils.defineLazyGetter(this, "isContentScriptProcess", () => {
72   return (
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"
77   );
78 });
80 var DocumentManager;
82 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
84 var apiManager = new (class extends SchemaAPIManager {
85   constructor() {
86     super("content", Schemas);
87     this.initialized = false;
88   }
90   lazyInit() {
91     if (!this.initialized) {
92       this.initialized = true;
93       this.initGlobal();
94       for (let { value } of Services.catMan.enumerateCategory(
95         CATEGORY_EXTENSION_SCRIPTS_CONTENT
96       )) {
97         this.loadScript(value);
98       }
99     }
100   }
101 })();
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) {
114     super(getter);
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", () => {
124       this.clear(-1);
125     });
126   }
128   get(url) {
129     let promise = super.get(url);
131     promise.lastUsed = Date.now();
132     if (promise.timer) {
133       promise.timer.cancel();
134     }
135     promise.timer = Timer(
136       this.delete.bind(this, url),
137       this.expiryTimeout,
138       Ci.nsITimer.TYPE_ONE_SHOT
139     );
141     return promise;
142   }
144   delete(url) {
145     if (this.has(url)) {
146       super.get(url).timer.cancel();
147     }
149     super.delete(url);
150   }
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) {
159         this.delete(url);
160       }
161     }
162   }
165 class ScriptCache extends CacheMap {
166   constructor(options, extension) {
167     super(SCRIPT_EXPIRY_TIMEOUT_MS, null, extension);
168     this.options = options;
169   }
171   defaultConstructor(url) {
172     let promise = ChromeUtils.compileScript(url, this.options);
173     promise.then(script => {
174       promise.script = script;
175     });
176     return promise;
177   }
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).
184  */
185 class BaseCSSCache extends CacheMap {
186   constructor(expiryTimeout, defaultConstructor, extension) {
187     super(expiryTimeout, defaultConstructor, extension);
188   }
190   addDocument(key, document) {
191     sheetCacheDocuments.get(this.get(key)).add(document);
192   }
194   deleteDocument(key, document) {
195     sheetCacheDocuments.get(this.get(key)).delete(document);
196   }
198   delete(key) {
199     if (this.has(key)) {
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
205       // still in use.
206       let docs = ChromeUtils.nondeterministicGetWeakSetKeys(
207         sheetCacheDocuments.get(promise)
208       );
209       if (docs.length) {
210         return;
211       }
212     }
214     super.delete(key);
215   }
219  * Cache of the preloaded stylesheet defined by url.
220  */
221 class CSSCache extends BaseCSSCache {
222   constructor(sheetType, extension) {
223     super(
224       CSS_EXPIRY_TIMEOUT_MS,
225       url => {
226         let uri = Services.io.newURI(url);
227         return styleSheetService
228           .preloadSheetAsync(uri, sheetType)
229           .then(sheet => {
230             return { url, sheet };
231           });
232       },
233       extension
234     );
235   }
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.
241  */
242 class CSSCodeCache extends BaseCSSCache {
243   constructor(sheetType, extension) {
244     super(
245       CSSCODE_EXPIRY_TIMEOUT_MS,
246       hash => {
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.
250           throw new Error(
251             "Unexistent cached cssCode stylesheet: " + Error().stack
252           );
253         }
255         return super.get(hash);
256       },
257       extension
258     );
260     // Store the preferred sheetType (used to preload the expected stylesheet type in
261     // the addCSSCode method).
262     this.sheetType = sheetType;
263   }
265   addCSSCode(hash, cssCode) {
266     if (this.has(hash)) {
267       // This cssCode have been already cached, no need to create it again.
268       return;
269     }
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)
276     );
277     const value = styleSheetService
278       .preloadSheetAsync(uri, this.sheetType)
279       .then(sheet => {
280         return { sheet, uri };
281       });
283     super.set(hash, value);
284   }
287 defineLazyGetter(
288   BrowserExtensionContent.prototype,
289   "staticScripts",
290   function() {
291     return new ScriptCache({ hasReturnValue: false }, this);
292   }
295 defineLazyGetter(
296   BrowserExtensionContent.prototype,
297   "dynamicScripts",
298   function() {
299     return new ScriptCache({ hasReturnValue: true }, this);
300   }
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);
318 defineLazyGetter(
319   BrowserExtensionContent.prototype,
320   "authorCSSCode",
321   function() {
322     return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
323   }
326 // Represents a content script.
327 class Script {
328   /**
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
336    */
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;
350     this.cssCache =
351       extension[this.cssOrigin === "user" ? "userCSS" : "authorCSS"];
352     this.cssCodeCache =
353       extension[this.cssOrigin === "user" ? "userCSSCode" : "authorCSSCode"];
354     this.scriptCache =
355       extension[matcher.wantReturnValue ? "dynamicScripts" : "staticScripts"];
357     if (matcher.wantReturnValue) {
358       this.compileScripts();
359       this.loadCSS();
360     }
361   }
363   get requiresCleanup() {
364     return !this.removeCSS && (!!this.css.length || this.cssCodeHash);
365   }
367   async addCSSCode(cssCode) {
368     if (!cssCode) {
369       return;
370     }
372     // Store the hash of the cssCode.
373     const buffer = await crypto.subtle.digest(
374       "SHA-1",
375       new TextEncoder().encode(cssCode)
376     );
377     this.cssCodeHash = String.fromCharCode(...new Uint16Array(buffer));
379     // Cache and preload the cssCode stylesheet.
380     this.cssCodeCache.addCSSCode(this.cssCodeHash, cssCode);
381   }
383   compileScripts() {
384     return this.js.map(url => this.scriptCache.get(url));
385   }
387   loadCSS() {
388     return this.css.map(url => this.cssCache.get(url));
389   }
391   preload() {
392     this.loadCSS();
393     this.compileScripts();
394   }
396   cleanup(window) {
397     if (this.requiresCleanup) {
398       if (window) {
399         let { windowUtils } = window;
401         let type =
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,
412               url,
413               type
414             );
415           }
416         }
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);
424             });
425           }
426           this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
427         }
428       }
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);
434     }
435   }
437   matchesWindowGlobal(windowGlobal) {
438     return this.matcher.matchesWindowGlobal(windowGlobal);
439   }
441   async injectInto(window) {
442     if (!isContentScriptProcess) {
443       return;
444     }
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,
450       });
451     }
453     try {
454       if (this.runAt === "document_end") {
455         await promiseDocumentReady(window.document);
456       } else if (this.runAt === "document_idle") {
457         await Promise.race([
458           promiseDocumentIdle(window),
459           promiseDocumentLoaded(window.document),
460         ]);
461       }
463       return this.inject(context);
464     } catch (e) {
465       return Promise.reject(context.normalizeError(e));
466     }
467   }
469   /**
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.
472    *
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.
478    */
479   async inject(context) {
480     DocumentManager.lazyInit();
481     if (this.requiresCleanup) {
482       context.addScript(this);
483     }
485     const { cssCodeHash } = this;
487     let cssPromise;
488     if (this.css.length || cssCodeHash) {
489       let window = context.contentWindow;
490       let { windowUtils } = window;
492       let type =
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,
503             url,
504             type
505           );
506         }
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);
513         }
514       } else {
515         cssPromise = Promise.all(this.loadCSS()).then(sheets => {
516           let window = context.contentWindow;
517           if (!window) {
518             return;
519           }
521           for (let { url, sheet } of sheets) {
522             this.cssCache.addDocument(url, window.document);
524             runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
525           }
526         });
528         if (cssCodeHash) {
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);
534           });
535         }
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
546         // a problem.
547         if (this.css.length) {
548           context.contentWindow.document.blockParsing(cssPromise, {
549             blockScriptCreated: false,
550           });
551         }
552       }
553     }
555     let scripts = this.getCompiledScripts(context);
556     if (scripts instanceof Promise) {
557       scripts = await scripts;
558     }
560     // Make sure we've injected any related CSS before we run content scripts.
561     await cssPromise;
563     let result;
565     const { extension } = context;
567     // The evaluations below may throw, in which case the promise will be
568     // automatically rejected.
569     ExtensionTelemetry.contentScriptInjection.stopwatchStart(
570       extension,
571       context
572     );
573     try {
574       for (let script of scripts) {
575         result = script.executeInGlobal(context.cloneScope);
576       }
578       if (this.matcher.jsCode) {
579         result = Cu.evalInSandbox(
580           this.matcher.jsCode,
581           context.cloneScope,
582           "latest",
583           "sandbox eval code",
584           1
585         );
586       }
587     } finally {
588       ExtensionTelemetry.contentScriptInjection.stopwatchFinish(
589         extension,
590         context
591       );
592     }
594     return result;
595   }
597   /**
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).
600    *
601    * @param {BaseContext} context
602    *        The document to block the parsing on, if the scripts are not yet precompiled and cached.
603    *
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.
607    */
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;
621       if (
622         this.runAt === "document_start" &&
623         document.readyState !== "complete"
624       ) {
625         document.blockParsing(promise, { blockScriptCreated: false });
626       }
628       return promise;
629     }
631     return scripts;
632   }
635 // Represents a user script.
636 class UserScript extends Script {
637   /**
638    * @param {BrowserExtensionContent} extension
639    * @param {WebExtensionContentScript|object} matcher
640    *        An object with a "matchesWindowGlobal" method and content script
641    *        execution details.
642    */
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;
651     this.apiScriptURL =
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);
658     }
660     // WeakMap<ContentScriptContextChild, Sandbox>
661     this.sandboxes = new DefaultWeakMap(context => {
662       return this.createSandbox(context);
663     });
664   }
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;
674     }
676     let apiScript, sandboxScripts;
678     if (this.apiScriptURL) {
679       [apiScript, ...sandboxScripts] = scripts;
680     } else {
681       sandboxScripts = scripts;
682     }
684     // Load and execute the API script once per context.
685     if (apiScript) {
686       context.executeAPIScript(apiScript);
687     }
689     // The evaluations below may throw, in which case the promise will be
690     // automatically rejected.
691     ExtensionTelemetry.userScriptInjection.stopwatchStart(extension, context);
692     try {
693       let userScriptSandbox = this.sandboxes.get(context);
695       context.callOnClose({
696         close: () => {
697           // Destroy the userScript sandbox when the related ContentScriptContextChild instance
698           // is being closed.
699           this.sandboxes.delete(context);
700           Cu.nukeSandbox(userScriptSandbox);
701         },
702       });
704       // Notify listeners subscribed to the userScripts.onBeforeScript API event,
705       // to allow extension API script to provide its custom APIs to the userScript.
706       if (apiScript) {
707         context.userScriptsEvents.emit(
708           "on-before-script",
709           this.scriptMetadata,
710           userScriptSandbox
711         );
712       }
714       for (let script of sandboxScripts) {
715         script.executeInGlobal(userScriptSandbox);
716       }
717     } finally {
718       ExtensionTelemetry.userScriptInjection.stopwatchFinish(
719         extension,
720         context
721       );
722     }
723   }
725   createSandbox(context) {
726     const { contentWindow } = context;
727     const contentPrincipal = contentWindow.document.nodePrincipal;
728     const ssm = Services.scriptSecurityManager;
730     let principal;
731     if (contentPrincipal.isSystemPrincipal) {
732       principal = ssm.createNullPrincipal(contentPrincipal.originAttributes);
733     } else {
734       principal = [contentPrincipal];
735     }
737     const sandbox = Cu.Sandbox(principal, {
738       sandboxName: `User Script registered by ${this.extension.policy.debugName}`,
739       sandboxPrototype: contentWindow,
740       sameZoneAs: contentWindow,
741       wantXrays: true,
742       wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
743       originAttributes: contentPrincipal.originAttributes,
744       metadata: {
745         "inner-window-id": context.innerWindowID,
746         addonId: this.extension.policy.id,
747       },
748     });
750     return sandbox;
751   }
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);
759   }
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.
769  */
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;
781     this.scripts = [];
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,
791       attrs
792     );
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,
806         wantXrays: false,
807         isWebExtensionContentScript: true,
808       });
809     } else {
810       let principal;
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);
815       } else {
816         principal = [contentPrincipal, extensionPrincipal];
817       }
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.
821       let metadata = {
822         "inner-window-id": this.innerWindowID,
823         addonId: extensionPrincipal.addonId,
824       };
826       this.sandbox = Cu.Sandbox(principal, {
827         metadata,
828         sandboxName: `Content Script ${extension.policy.debugName}`,
829         sandboxPrototype: contentWindow,
830         sameZoneAs: contentWindow,
831         wantXrays: true,
832         isWebExtensionContentScript: true,
833         wantExportHelpers: true,
834         wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
835         originAttributes: attrs,
836       });
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
847       // a real window).
848       Cu.evalInSandbox(
849         `
850         this.content = {
851           XMLHttpRequest: window.XMLHttpRequest,
852           fetch: window.fetch.bind(window),
853           WebSocket: window.WebSocket,
854         };
856         window.JSON = JSON;
857         window.XMLHttpRequest = XMLHttpRequest;
858         window.fetch = fetch;
859         window.WebSocket = WebSocket;
860       `,
861         this.sandbox
862       );
863     }
865     Object.defineProperty(this, "principal", {
866       value: Cu.getObjectPrincipal(this.sandbox),
867       enumerable: true,
868       configurable: true,
869     });
871     this.url = contentWindow.location.href;
873     defineLazyGetter(this, "chromeObj", () => {
874       let chromeObj = Cu.createObjectIn(this.sandbox);
876       this.childManager.inject(chromeObj);
877       return chromeObj;
878     });
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();
891     });
892   }
894   injectAPI() {
895     if (!this.isExtensionPage) {
896       throw new Error("Cannot inject extension API into non-extension window");
897     }
899     // This is an iframe with content script API enabled (See Bug 1214658)
900     Schemas.exportLazyGetter(
901       this.contentWindow,
902       "browser",
903       () => this.chromeObj
904     );
905     Schemas.exportLazyGetter(
906       this.contentWindow,
907       "chrome",
908       () => this.chromeObj
909     );
910   }
912   async logActivity(type, name, data) {
913     ExtensionActivityLogChild.log(this, type, name, data);
914   }
916   get cloneScope() {
917     return this.sandbox;
918   }
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);
926     }
927   }
929   addScript(script) {
930     if (script.requiresCleanup) {
931       this.scripts.push(script);
932     }
933   }
935   close() {
936     super.unload();
938     // Cleanup the scripts even if the contentWindow have been destroyed.
939     for (let script of this.scripts) {
940       script.cleanup(this.contentWindow);
941     }
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" });
949       }
950     }
951     Cu.nukeSandbox(this.sandbox);
953     this.sandbox = null;
954   }
957 defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
958   return new Messenger(this);
961 defineLazyGetter(
962   ContentScriptContextChild.prototype,
963   "childManager",
964   function() {
965     apiManager.lazyInit();
967     let localApis = {};
968     let can = new CanOfAPIs(this, apiManager, localApis);
970     let childManager = new ChildAPIManager(this, this.messageManager, can, {
971       envType: "content_parent",
972       url: this.url,
973     });
975     this.callOnClose(childManager);
977     return childManager;
978   }
981 // Responsible for creating ExtensionContexts and injecting content
982 // scripts into them when new documents are created.
983 DocumentManager = {
984   // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
985   contexts: new Map(),
987   initialized: false,
989   lazyInit() {
990     if (this.initialized) {
991       return;
992     }
993     this.initialized = true;
995     Services.obs.addObserver(this, "inner-window-destroyed");
996     Services.obs.addObserver(this, "memory-pressure");
997   },
999   uninit() {
1000     Services.obs.removeObserver(this, "inner-window-destroyed");
1001     Services.obs.removeObserver(this, "memory-pressure");
1002   },
1004   observers: {
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()) {
1014           context.close();
1015         }
1017         this.contexts.delete(windowId);
1018       }
1019     },
1020     "memory-pressure"(subject, topic, data) {
1021       let timeout = data === "heap-minimize" ? 0 : undefined;
1023       for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(
1024         scriptCaches
1025       )) {
1026         cache.clear(timeout);
1027       }
1028     },
1029   },
1031   observe(subject, topic, data) {
1032     this.observers[topic].call(this, subject, topic, data);
1033   },
1035   shutdownExtension(extension) {
1036     for (let extensions of this.contexts.values()) {
1037       let context = extensions.get(extension);
1038       if (context) {
1039         context.close();
1040         extensions.delete(extension);
1041       }
1042     }
1043   },
1045   getContexts(window) {
1046     let winId = getInnerWindowID(window);
1048     let extensions = this.contexts.get(winId);
1049     if (!extensions) {
1050       extensions = new Map();
1051       this.contexts.set(winId, extensions);
1052     }
1054     return extensions;
1055   },
1057   // For test use only.
1058   getContext(extensionId, window) {
1059     for (let [extension, context] of this.getContexts(window)) {
1060       if (extension.id === extensionId) {
1061         return context;
1062       }
1063     }
1064   },
1066   getContentScriptGlobals(window) {
1067     let extensions = this.contexts.get(getInnerWindowID(window));
1069     if (extensions) {
1070       return Array.from(extensions.values(), ctx => ctx.sandbox);
1071     }
1073     return [];
1074   },
1076   initExtensionContext(extension, window) {
1077     extension.getContext(window).injectAPI();
1078   },
1081 var ExtensionContent = {
1082   BrowserExtensionContent,
1084   contentScripts,
1086   shutdownExtension(extension) {
1087     DocumentManager.shutdownExtension(extension);
1088   },
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);
1096   },
1098   initExtensionContext(extension, window) {
1099     DocumentManager.initExtensionContext(extension, window);
1100   },
1102   getContext(extension, window) {
1103     let extensions = DocumentManager.getContexts(window);
1105     let context = extensions.get(extension);
1106     if (!context) {
1107       context = new ContentScriptContextChild(extension, window);
1108       extensions.set(extension, context);
1109     }
1110     return context;
1111   },
1113   handleDetectLanguage(global, target) {
1114     let doc = target.content.document;
1116     return promiseDocumentReady(doc).then(() => {
1117       let elem = doc.documentElement;
1119       let language =
1120         elem.getAttribute("xml:lang") ||
1121         elem.getAttribute("lang") ||
1122         doc.contentLanguage ||
1123         null;
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");
1136       encoder.init(
1137         doc,
1138         "text/plain",
1139         Ci.nsIDocumentEncoder.SkipInvisibleContent
1140       );
1141       let text = encoder.encodeToStringWithMaxLength(60 * 1024);
1143       let encoding = doc.characterSet;
1145       return LanguageDetector.detectLanguage({
1146         language,
1147         tld,
1148         text,
1149         encoding,
1150       }).then(result => (result.language === "un" ? "und" : result.language));
1151     });
1152   },
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,
1164     });
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);
1175       }
1176     };
1178     let all = Promise.all(windows.map(executeInWin).filter(p => p));
1179     let result = await all.catch(e => Promise.reject({ message: e.message }));
1181     try {
1182       // Check if the result can be structured-cloned before sending back.
1183       return Cu.cloneInto(result, this);
1184     } catch (e) {
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 });
1188     }
1189   },
1191   async receiveMessage(global, name, target) {
1192     if (name === "Extension:DetectLanguage") {
1193       return this.handleDetectLanguage(global, target);
1194     }
1195   },
1197   // Helpers
1199   *enumerateWindows(docShell) {
1200     let docShells = docShell.getAllDocShellsInSubtree(
1201       docShell.typeContent,
1202       docShell.ENUMERATE_FORWARDS
1203     );
1205     for (let docShell of docShells) {
1206       try {
1207         yield docShell.domWindow;
1208       } catch (e) {
1209         // This can fail if the docShell is being destroyed, so just
1210         // ignore the error.
1211       }
1212     }
1213   },
1217  * Child side of the ExtensionContent process actor, handles some tabs.* APIs.
1218  */
1219 class ExtensionContentChild extends JSProcessActorChild {
1220   receiveMessage({ name, data }) {
1221     if (!isContentScriptProcess) {
1222       return;
1223     }
1224     switch (name) {
1225       case "Execute":
1226         return ExtensionContent.handleActorExecute(data);
1227     }
1228   }