· 6 years ago · Oct 15, 2019, 09:38 AM
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";
7
8var EXPORTED_SYMBOLS = ["ExtensionContent"];
9
10/* globals ExtensionContent */
11
12const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
13const { XPCOMUtils } = ChromeUtils.import(
14 "resource://gre/modules/XPCOMUtils.jsm"
15);
16
17XPCOMUtils.defineLazyModuleGetters(this, {
18 ExtensionProcessScript: "resource://gre/modules/ExtensionProcessScript.jsm",
19 ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm",
20 LanguageDetector: "resource:///modules/translation/LanguageDetector.jsm",
21 MessageChannel: "resource://gre/modules/MessageChannel.jsm",
22 Schemas: "resource://gre/modules/Schemas.jsm",
23 WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
24});
25
26XPCOMUtils.defineLazyServiceGetter(
27 this,
28 "styleSheetService",
29 "@mozilla.org/content/style-sheet-service;1",
30 "nsIStyleSheetService"
31);
32
33const Timer = Components.Constructor(
34 "@mozilla.org/timer;1",
35 "nsITimer",
36 "initWithCallback"
37);
38
39const { ExtensionChild } = ChromeUtils.import(
40 "resource://gre/modules/ExtensionChild.jsm"
41);
42const { ExtensionCommon } = ChromeUtils.import(
43 "resource://gre/modules/ExtensionCommon.jsm"
44);
45const { ExtensionUtils } = ChromeUtils.import(
46 "resource://gre/modules/ExtensionUtils.jsm"
47);
48
49XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
50
51const {
52 DefaultMap,
53 DefaultWeakMap,
54 getInnerWindowID,
55 getWinUtils,
56 promiseDocumentIdle,
57 promiseDocumentLoaded,
58 promiseDocumentReady,
59} = ExtensionUtils;
60
61const {
62 BaseContext,
63 CanOfAPIs,
64 SchemaAPIManager,
65 defineLazyGetter,
66 runSafeSyncWithoutClone,
67} = ExtensionCommon;
68
69const { BrowserExtensionContent, ChildAPIManager, Messenger } = ExtensionChild;
70
71XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
72
73XPCOMUtils.defineLazyGetter(this, "isContentScriptProcess", () => {
74 return (
75 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT ||
76 !WebExtensionPolicy.useRemoteWebExtensions
77 );
78});
79
80var DocumentManager;
81
82const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
83
84var apiManager = new (class extends SchemaAPIManager {
85 constructor() {
86 super("content", Schemas);
87 this.initialized = false;
88 }
89
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})();
102
103const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
104const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
105
106const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
107const CSSCODE_EXPIRY_TIMEOUT_MS = 10 * 60 * 1000;
108
109const scriptCaches = new WeakSet();
110const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
111
112class CacheMap extends DefaultMap {
113 constructor(timeout, getter, extension) {
114 super(getter);
115
116 this.expiryTimeout = timeout;
117
118 scriptCaches.add(this);
119
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 }
127
128 get(url) {
129 let promise = super.get(url);
130
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 );
140
141 return promise;
142 }
143
144 delete(url) {
145 if (this.has(url)) {
146 super.get(url).timer.cancel();
147 }
148
149 super.delete(url);
150 }
151
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 }
163}
164
165class ScriptCache extends CacheMap {
166 constructor(options, extension) {
167 super(SCRIPT_EXPIRY_TIMEOUT_MS, null, extension);
168 this.options = options;
169 }
170
171 defaultConstructor(url) {
172 let promise = ChromeUtils.compileScript(url, this.options);
173 promise.then(script => {
174 promise.script = script;
175 });
176 return promise;
177 }
178}
179
180/**
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 */
185class BaseCSSCache extends CacheMap {
186 constructor(expiryTimeout, defaultConstructor, extension) {
187 super(expiryTimeout, defaultConstructor, extension);
188 }
189
190 addDocument(key, document) {
191 sheetCacheDocuments.get(this.get(key)).add(document);
192 }
193
194 deleteDocument(key, document) {
195 sheetCacheDocuments.get(this.get(key)).delete(document);
196 }
197
198 delete(key) {
199 if (this.has(key)) {
200 let promise = this.get(key);
201
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 }
213
214 super.delete(key);
215 }
216}
217
218/**
219 * Cache of the preloaded stylesheet defined by url.
220 */
221class 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 }
236}
237
238/**
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 */
242class 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 }
254
255 return super.get(hash);
256 },
257 extension
258 );
259
260 // Store the preferred sheetType (used to preload the expected stylesheet type in
261 // the addCSSCode method).
262 this.sheetType = sheetType;
263 }
264
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 const uri = Services.io.newURI(
271 "data:text/css;charset=utf-8," + encodeURIComponent(cssCode)
272 );
273 const value = styleSheetService
274 .preloadSheetAsync(uri, this.sheetType)
275 .then(sheet => {
276 return { sheet, uri };
277 });
278
279 super.set(hash, value);
280 }
281}
282
283defineLazyGetter(
284 BrowserExtensionContent.prototype,
285 "staticScripts",
286 function() {
287 return new ScriptCache({ hasReturnValue: false }, this);
288 }
289);
290
291defineLazyGetter(
292 BrowserExtensionContent.prototype,
293 "dynamicScripts",
294 function() {
295 return new ScriptCache({ hasReturnValue: true }, this);
296 }
297);
298
299defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", function() {
300 return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET, this);
301});
302
303defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", function() {
304 return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
305});
306
307// These two caches are similar to the above but specialized to cache the cssCode
308// using an hash computed from the cssCode string as the key (instead of the generated data
309// URI which can be pretty long for bigger injected cssCode).
310defineLazyGetter(BrowserExtensionContent.prototype, "userCSSCode", function() {
311 return new CSSCodeCache(Ci.nsIStyleSheetService.USER_SHEET, this);
312});
313
314defineLazyGetter(
315 BrowserExtensionContent.prototype,
316 "authorCSSCode",
317 function() {
318 return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
319 }
320);
321
322// Represents a content script.
323class Script {
324 /**
325 * @param {BrowserExtensionContent} extension
326 * @param {WebExtensionContentScript|object} matcher
327 * An object with a "matchesWindow" method and content script execution
328 * details. This is usually a plain WebExtensionContentScript object,
329 * except when the script is run via `tabs.executeScript`. In this
330 * case, the object may have some extra properties:
331 * wantReturnValue, removeCSS, cssOrigin, jsCode
332 */
333 constructor(extension, matcher) {
334 this.extension = extension;
335 this.matcher = matcher;
336
337 this.runAt = this.matcher.runAt;
338 this.js = this.matcher.jsPaths;
339 this.css = this.matcher.cssPaths.slice();
340 this.cssCodeHash = null;
341
342 this.removeCSS = this.matcher.removeCSS;
343 this.cssOrigin = this.matcher.cssOrigin;
344
345 this.cssCache =
346 extension[this.cssOrigin === "user" ? "userCSS" : "authorCSS"];
347 this.cssCodeCache =
348 extension[this.cssOrigin === "user" ? "userCSSCode" : "authorCSSCode"];
349 this.scriptCache =
350 extension[matcher.wantReturnValue ? "dynamicScripts" : "staticScripts"];
351
352 if (matcher.wantReturnValue) {
353 this.compileScripts();
354 this.loadCSS();
355 }
356 }
357
358 get requiresCleanup() {
359 return !this.removeCSS && (this.css.length > 0 || this.cssCodeHash);
360 }
361
362 async addCSSCode(cssCode) {
363 if (!cssCode) {
364 return;
365 }
366
367 // Store the hash of the cssCode.
368 const buffer = await crypto.subtle.digest(
369 "SHA-1",
370 new TextEncoder().encode(cssCode)
371 );
372 this.cssCodeHash = String.fromCharCode(...new Uint16Array(buffer));
373
374 // Cache and preload the cssCode stylesheet.
375 this.cssCodeCache.addCSSCode(this.cssCodeHash, cssCode);
376 }
377
378 compileScripts() {
379 return this.js.map(url => this.scriptCache.get(url));
380 }
381
382 loadCSS() {
383 return this.css.map(url => this.cssCache.get(url));
384 }
385
386 preload() {
387 this.loadCSS();
388 this.compileScripts();
389 }
390
391 cleanup(window) {
392 if (this.requiresCleanup) {
393 if (window) {
394 let winUtils = getWinUtils(window);
395
396 let type =
397 this.cssOrigin === "user"
398 ? winUtils.USER_SHEET
399 : winUtils.AUTHOR_SHEET;
400
401 for (let url of this.css) {
402 this.cssCache.deleteDocument(url, window.document);
403 runSafeSyncWithoutClone(
404 winUtils.removeSheetUsingURIString,
405 url,
406 type
407 );
408 }
409
410 const { cssCodeHash } = this;
411
412 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
413 this.cssCodeCache.get(cssCodeHash).then(({ uri }) => {
414 runSafeSyncWithoutClone(winUtils.removeSheet, uri, type);
415 });
416 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
417 }
418 }
419
420 // Clear any sheets that were kept alive past their timeout as
421 // a result of living in this document.
422 this.cssCodeCache.clear(CSSCODE_EXPIRY_TIMEOUT_MS);
423 this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
424 }
425 }
426
427 matchesWindow(window) {
428 return this.matcher.matchesWindow(window);
429 }
430
431 async injectInto(window) {
432 if (!isContentScriptProcess) {
433 return;
434 }
435
436 let context = this.extension.getContext(window);
437 try {
438 if (this.runAt === "document_end") {
439 await promiseDocumentReady(window.document);
440 } else if (this.runAt === "document_idle") {
441 await Promise.race([
442 promiseDocumentIdle(window),
443 promiseDocumentLoaded(window.document),
444 ]);
445 }
446
447 return this.inject(context);
448 } catch (e) {
449 return Promise.reject(context.normalizeError(e));
450 }
451 }
452
453 /**
454 * Tries to inject this script into the given window and sandbox, if
455 * there are pending operations for the window's current load state.
456 *
457 * @param {BaseContext} context
458 * The content script context into which to inject the scripts.
459 * @returns {Promise<any>}
460 * Resolves to the last value in the evaluated script, when
461 * execution is complete.
462 */
463 async inject(context) {
464 DocumentManager.lazyInit();
465 if (this.requiresCleanup) {
466 context.addScript(this);
467 }
468
469 const { cssCodeHash } = this;
470
471 let cssPromise;
472 if (this.css.length || cssCodeHash) {
473 let window = context.contentWindow;
474 let winUtils = getWinUtils(window);
475
476 let type =
477 this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
478
479 if (this.removeCSS) {
480 for (let url of this.css) {
481 this.cssCache.deleteDocument(url, window.document);
482
483 runSafeSyncWithoutClone(
484 winUtils.removeSheetUsingURIString,
485 url,
486 type
487 );
488 }
489
490 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
491 const { uri } = await this.cssCodeCache.get(cssCodeHash);
492 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
493
494 runSafeSyncWithoutClone(winUtils.removeSheet, uri, type);
495 }
496 } else {
497 cssPromise = Promise.all(this.loadCSS()).then(sheets => {
498 let window = context.contentWindow;
499 if (!window) {
500 return;
501 }
502
503 for (let { url, sheet } of sheets) {
504 this.cssCache.addDocument(url, window.document);
505
506 runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
507 }
508 });
509
510 if (cssCodeHash) {
511 cssPromise = cssPromise.then(async () => {
512 const { sheet } = await this.cssCodeCache.get(cssCodeHash);
513 this.cssCodeCache.addDocument(cssCodeHash, window.document);
514
515 runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
516 });
517 }
518
519 // We're loading stylesheets via the stylesheet service, which means
520 // that the normal mechanism for blocking layout and onload for pending
521 // stylesheets aren't in effect (since there's no document to block). So
522 // we need to do something custom here, similar to what we do for
523 // scripts. Blocking parsing is overkill, since we really just want to
524 // block layout and onload. But we have an API to do the former and not
525 // the latter, so we do it that way. This hopefully isn't a performance
526 // problem since there are no network loads involved, and since we cache
527 // the stylesheets on first load. We should fix this up if it does becomes
528 // a problem.
529 if (this.css.length > 0) {
530 context.contentWindow.document.blockParsing(cssPromise, {
531 blockScriptCreated: false,
532 });
533 }
534 }
535 }
536
537 let scripts = this.getCompiledScripts(context);
538 if (scripts instanceof Promise) {
539 scripts = await scripts;
540 }
541
542 let result;
543
544 const { extension } = context;
545
546 // The evaluations below may throw, in which case the promise will be
547 // automatically rejected.
548 ExtensionTelemetry.contentScriptInjection.stopwatchStart(
549 extension,
550 context
551 );
552 try {
553 for (let script of scripts) {
554 result = script.executeInGlobal(context.cloneScope);
555 }
556
557 if (this.matcher.jsCode) {
558 result = Cu.evalInSandbox(
559 this.matcher.jsCode,
560 context.cloneScope,
561 "latest"
562 );
563 }
564 } finally {
565 ExtensionTelemetry.contentScriptInjection.stopwatchFinish(
566 extension,
567 context
568 );
569 }
570
571 await cssPromise;
572 return result;
573 }
574
575 /**
576 * Get the compiled scripts (if they are already precompiled and cached) or a promise which resolves
577 * to the precompiled scripts (once they have been compiled and cached).
578 *
579 * @param {BaseContext} context
580 * The document to block the parsing on, if the scripts are not yet precompiled and cached.
581 *
582 * @returns {Array<PreloadedScript> | Promise<Array<PreloadedScript>>}
583 * Returns an array of preloaded scripts if they are already available, or a promise which
584 * resolves to the array of the preloaded scripts once they are precompiled and cached.
585 */
586 getCompiledScripts(context) {
587 let scriptPromises = this.compileScripts();
588 let scripts = scriptPromises.map(promise => promise.script);
589
590 // If not all scripts are already available in the cache, block
591 // parsing and wait all promises to resolve.
592 if (!scripts.every(script => script)) {
593 let promise = Promise.all(scriptPromises);
594
595 // If we're supposed to inject at the start of the document load,
596 // and we haven't already missed that point, block further parsing
597 // until the scripts have been loaded.
598 const { document } = context.contentWindow;
599 if (
600 this.runAt === "document_start" &&
601 document.readyState !== "complete"
602 ) {
603 document.blockParsing(promise, { blockScriptCreated: false });
604 }
605
606 return promise;
607 }
608
609 return scripts;
610 }
611}
612
613// Represents a user script.
614class UserScript extends Script {
615 /**
616 * @param {BrowserExtensionContent} extension
617 * @param {WebExtensionContentScript|object} matcher
618 * An object with a "matchesWindow" method and content script execution
619 * details.
620 */
621 constructor(extension, matcher) {
622 super(extension, matcher);
623
624 // This is an opaque object that the extension provides, it is associated to
625 // the particular userScript and it is passed as a parameter to the custom
626 // userScripts APIs defined by the extension.
627 this.scriptMetadata = matcher.userScriptOptions.scriptMetadata;
628 this.apiScriptURL =
629 extension.manifest.user_scripts &&
630 extension.manifest.user_scripts.api_script;
631
632 // Add the apiScript to the js scripts to compile.
633 if (this.apiScriptURL) {
634 this.js = [this.apiScriptURL].concat(this.js);
635 }
636
637 // WeakMap<ContentScriptContextChild, Sandbox>
638 this.sandboxes = new DefaultWeakMap(context => {
639 return this.createSandbox(context);
640 });
641 }
642
643 async inject(context) {
644 const { extension } = context;
645
646 DocumentManager.lazyInit();
647
648 let scripts = this.getCompiledScripts(context);
649 if (scripts instanceof Promise) {
650 scripts = await scripts;
651 }
652
653 let apiScript, sandboxScripts;
654
655 if (this.apiScriptURL) {
656 [apiScript, ...sandboxScripts] = scripts;
657 } else {
658 sandboxScripts = scripts;
659 }
660
661 // Load and execute the API script once per context.
662 if (apiScript) {
663 context.executeAPIScript(apiScript);
664 }
665
666 // The evaluations below may throw, in which case the promise will be
667 // automatically rejected.
668 ExtensionTelemetry.userScriptInjection.stopwatchStart(extension, context);
669 try {
670 let userScriptSandbox = this.sandboxes.get(context);
671
672 context.callOnClose({
673 close: () => {
674 // Destroy the userScript sandbox when the related ContentScriptContextChild instance
675 // is being closed.
676 this.sandboxes.delete(context);
677 Cu.nukeSandbox(userScriptSandbox);
678 },
679 });
680
681 // Notify listeners subscribed to the userScripts.onBeforeScript API event,
682 // to allow extension API script to provide its custom APIs to the userScript.
683 if (apiScript) {
684 context.userScriptsEvents.emit(
685 "on-before-script",
686 this.scriptMetadata,
687 userScriptSandbox
688 );
689 }
690
691 for (let script of sandboxScripts) {
692 script.executeInGlobal(userScriptSandbox);
693 }
694 } finally {
695 ExtensionTelemetry.userScriptInjection.stopwatchFinish(
696 extension,
697 context
698 );
699 }
700 }
701
702 createSandbox(context) {
703 const { contentWindow } = context;
704 const contentPrincipal = contentWindow.document.nodePrincipal;
705 const ssm = Services.scriptSecurityManager;
706
707 let principal;
708 if (contentPrincipal.isSystemPrincipal) {
709 principal = ssm.createNullPrincipal(contentPrincipal.originAttributes);
710 } else {
711 principal = [contentPrincipal];
712 }
713
714 const sandbox = Cu.Sandbox(principal, {
715 sandboxName: `User Script registered by ${
716 this.extension.policy.debugName
717 }`,
718 sandboxPrototype: contentWindow,
719 sameZoneAs: contentWindow,
720 wantXrays: true,
721 wantGlobalProperties: ["XMLHttpRequest", "fetch"],
722 originAttributes: contentPrincipal.originAttributes,
723 metadata: {
724 "inner-window-id": context.innerWindowID,
725 addonId: this.extension.policy.id,
726 },
727 });
728
729 return sandbox;
730 }
731}
732
733var contentScripts = new DefaultWeakMap(matcher => {
734 const extension = ExtensionProcessScript.extensions.get(matcher.extension);
735
736 if ("userScriptOptions" in matcher) {
737 return new UserScript(extension, matcher);
738 }
739
740 return new Script(extension, matcher);
741});
742
743/**
744 * An execution context for semi-privileged extension content scripts.
745 *
746 * This is the child side of the ContentScriptContextParent class
747 * defined in ExtensionParent.jsm.
748 */
749class ContentScriptContextChild extends BaseContext {
750 constructor(extension, contentWindow) {
751 super("content_child", extension);
752
753 this.setContentWindow(contentWindow);
754
755 let frameId = WebNavigationFrames.getFrameId(contentWindow);
756 this.frameId = frameId;
757
758 this.scripts = [];
759
760 let contentPrincipal = contentWindow.document.nodePrincipal;
761 let ssm = Services.scriptSecurityManager;
762
763 // Copy origin attributes from the content window origin attributes to
764 // preserve the user context id.
765 let attrs = contentPrincipal.originAttributes;
766 let extensionPrincipal = ssm.createCodebasePrincipal(
767 this.extension.baseURI,
768 attrs
769 );
770
771 this.isExtensionPage = contentPrincipal.equals(extensionPrincipal);
772
773 if (this.isExtensionPage) {
774 // This is an iframe with content script API enabled and its principal
775 // should be the contentWindow itself. We create a sandbox with the
776 // contentWindow as principal and with X-rays disabled because it
777 // enables us to create the APIs object in this sandbox object and then
778 // copying it into the iframe's window. See bug 1214658.
779 this.sandbox = Cu.Sandbox(contentWindow, {
780 sandboxName: `Web-Accessible Extension Page ${
781 extension.policy.debugName
782 }`,
783 sandboxPrototype: contentWindow,
784 sameZoneAs: contentWindow,
785 wantXrays: false,
786 isWebExtensionContentScript: true,
787 });
788 } else {
789 let principal;
790 if (contentPrincipal.isSystemPrincipal) {
791 // Make sure we don't hand out the system principal by accident.
792 // Also make sure that the null principal has the right origin attributes.
793 principal = ssm.createNullPrincipal(attrs);
794 } else {
795 principal = [contentPrincipal, extensionPrincipal];
796 }
797 // This metadata is required by the Developer Tools, in order for
798 // the content script to be associated with both the extension and
799 // the tab holding the content page.
800 let metadata = {
801 "inner-window-id": this.innerWindowID,
802 addonId: extensionPrincipal.addonId,
803 };
804
805 this.sandbox = Cu.Sandbox(principal, {
806 metadata,
807 sandboxName: `Content Script ${extension.policy.debugName}`,
808 sandboxPrototype: contentWindow,
809 sameZoneAs: contentWindow,
810 wantXrays: true,
811 isWebExtensionContentScript: true,
812 wantExportHelpers: true,
813 wantGlobalProperties: ["XMLHttpRequest", "fetch"],
814 originAttributes: attrs,
815 });
816
817 // Preserve a copy of the original Error and Promise globals from the sandbox object,
818 // which are used in the WebExtensions internals (before any content script code had
819 // any chance to redefine them).
820 this.cloneScopePromise = this.sandbox.Promise;
821 this.cloneScopeError = this.sandbox.Error;
822
823 // Preserve a copy of the original window's XMLHttpRequest and fetch
824 // in a content object (fetch is manually binded to the window
825 // to prevent it from raising a TypeError because content object is not
826 // a real window).
827 Cu.evalInSandbox(
828 `
829 this.content = {
830 XMLHttpRequest: window.XMLHttpRequest,
831 fetch: window.fetch.bind(window),
832 };
833
834 window.JSON = JSON;
835 window.XMLHttpRequest = XMLHttpRequest;
836 window.fetch = fetch;
837 `,
838 this.sandbox
839 );
840 }
841
842 Object.defineProperty(this, "principal", {
843 value: Cu.getObjectPrincipal(this.sandbox),
844 enumerable: true,
845 configurable: true,
846 });
847
848 this.url = contentWindow.location.href;
849
850 defineLazyGetter(this, "chromeObj", () => {
851 let chromeObj = Cu.createObjectIn(this.sandbox);
852
853 this.childManager.inject(chromeObj);
854 return chromeObj;
855 });
856
857 Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
858 Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
859
860 // Keep track if the userScript API script has been already executed in this context
861 // (e.g. because there are more then one UserScripts that match the related webpage
862 // and so the UserScript apiScript has already been executed).
863 this.hasUserScriptAPIs = false;
864
865 // A lazy created EventEmitter related to userScripts-specific events.
866 defineLazyGetter(this, "userScriptsEvents", () => {
867 return new ExtensionCommon.EventEmitter();
868 });
869 }
870
871 injectAPI() {
872 if (!this.isExtensionPage) {
873 throw new Error("Cannot inject extension API into non-extension window");
874 }
875
876 // This is an iframe with content script API enabled (See Bug 1214658)
877 Schemas.exportLazyGetter(
878 this.contentWindow,
879 "browser",
880 () => this.chromeObj
881 );
882 Schemas.exportLazyGetter(
883 this.contentWindow,
884 "chrome",
885 () => this.chromeObj
886 );
887 }
888
889 get cloneScope() {
890 return this.sandbox;
891 }
892
893 async executeAPIScript(apiScript) {
894 // Execute the UserScript apiScript only once per context (e.g. more then one UserScripts
895 // match the same webpage and the apiScript has already been executed).
896 if (apiScript && !this.hasUserScriptAPIs) {
897 this.hasUserScriptAPIs = true;
898 apiScript.executeInGlobal(this.cloneScope);
899 }
900 }
901
902 addScript(script) {
903 if (script.requiresCleanup) {
904 this.scripts.push(script);
905 }
906 }
907
908 close() {
909 super.unload();
910
911 // Cleanup the scripts even if the contentWindow have been destroyed.
912 for (let script of this.scripts) {
913 script.cleanup(this.contentWindow);
914 }
915
916 if (this.contentWindow) {
917 // Overwrite the content script APIs with an empty object if the APIs objects are still
918 // defined in the content window (See Bug 1214658).
919 if (this.isExtensionPage) {
920 Cu.createObjectIn(this.contentWindow, { defineAs: "browser" });
921 Cu.createObjectIn(this.contentWindow, { defineAs: "chrome" });
922 }
923 }
924 Cu.nukeSandbox(this.sandbox);
925
926 this.sandbox = null;
927 }
928}
929
930defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
931 // The |sender| parameter is passed directly to the extension.
932 let sender = { id: this.extension.id, frameId: this.frameId, url: this.url };
933 let filter = { extensionId: this.extension.id };
934 let optionalFilter = { frameId: this.frameId };
935
936 return new Messenger(
937 this,
938 [this.messageManager],
939 sender,
940 filter,
941 optionalFilter
942 );
943});
944
945defineLazyGetter(
946 ContentScriptContextChild.prototype,
947 "childManager",
948 function() {
949 apiManager.lazyInit();
950
951 let localApis = {};
952 let can = new CanOfAPIs(this, apiManager, localApis);
953
954 let childManager = new ChildAPIManager(this, this.messageManager, can, {
955 envType: "content_parent",
956 url: this.url,
957 });
958
959 this.callOnClose(childManager);
960
961 return childManager;
962 }
963);
964
965// Responsible for creating ExtensionContexts and injecting content
966// scripts into them when new documents are created.
967DocumentManager = {
968 // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
969 contexts: new Map(),
970
971 initialized: false,
972
973 lazyInit() {
974 if (this.initialized) {
975 return;
976 }
977 this.initialized = true;
978
979 Services.obs.addObserver(this, "inner-window-destroyed");
980 Services.obs.addObserver(this, "memory-pressure");
981 },
982
983 uninit() {
984 Services.obs.removeObserver(this, "inner-window-destroyed");
985 Services.obs.removeObserver(this, "memory-pressure");
986 },
987
988 observers: {
989 "inner-window-destroyed"(subject, topic, data) {
990 let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
991
992 MessageChannel.abortResponses({ innerWindowID: windowId });
993
994 // Close any existent content-script context for the destroyed window.
995 if (this.contexts.has(windowId)) {
996 let extensions = this.contexts.get(windowId);
997 for (let context of extensions.values()) {
998 context.close();
999 }
1000
1001 this.contexts.delete(windowId);
1002 }
1003 },
1004 "memory-pressure"(subject, topic, data) {
1005 let timeout = data === "heap-minimize" ? 0 : undefined;
1006
1007 for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(
1008 scriptCaches
1009 )) {
1010 cache.clear(timeout);
1011 }
1012 },
1013 },
1014
1015 observe(subject, topic, data) {
1016 this.observers[topic].call(this, subject, topic, data);
1017 },
1018
1019 shutdownExtension(extension) {
1020 for (let extensions of this.contexts.values()) {
1021 let context = extensions.get(extension);
1022 if (context) {
1023 context.close();
1024 extensions.delete(extension);
1025 }
1026 }
1027 },
1028
1029 getContexts(window) {
1030 let winId = getInnerWindowID(window);
1031
1032 let extensions = this.contexts.get(winId);
1033 if (!extensions) {
1034 extensions = new Map();
1035 this.contexts.set(winId, extensions);
1036 }
1037
1038 return extensions;
1039 },
1040
1041 // For test use only.
1042 getContext(extensionId, window) {
1043 for (let [extension, context] of this.getContexts(window)) {
1044 if (extension.id === extensionId) {
1045 return context;
1046 }
1047 }
1048 },
1049
1050 getContentScriptGlobals(window) {
1051 let extensions = this.contexts.get(getInnerWindowID(window));
1052
1053 if (extensions) {
1054 return Array.from(extensions.values(), ctx => ctx.sandbox);
1055 }
1056
1057 return [];
1058 },
1059
1060 initExtensionContext(extension, window) {
1061 extension.getContext(window).injectAPI();
1062 },
1063};
1064
1065var ExtensionContent = {
1066 BrowserExtensionContent,
1067
1068 contentScripts,
1069
1070 shutdownExtension(extension) {
1071 DocumentManager.shutdownExtension(extension);
1072 },
1073
1074 // This helper is exported to be integrated in the devtools RDP actors,
1075 // that can use it to retrieve the existent WebExtensions ContentScripts
1076 // of a target window and be able to show the ContentScripts source in the
1077 // DevTools Debugger panel.
1078 getContentScriptGlobals(window) {
1079 return DocumentManager.getContentScriptGlobals(window);
1080 },
1081
1082 initExtensionContext(extension, window) {
1083 DocumentManager.initExtensionContext(extension, window);
1084 },
1085
1086 getContext(extension, window) {
1087 let extensions = DocumentManager.getContexts(window);
1088
1089 let context = extensions.get(extension);
1090 if (!context) {
1091 context = new ContentScriptContextChild(extension, window);
1092 extensions.set(extension, context);
1093 }
1094 return context;
1095 },
1096
1097 handleExtensionCapture(global, width, height, options) {
1098 let win = global.content;
1099
1100 const XHTML_NS = "http://www.w3.org/1999/xhtml";
1101 let canvas = win.document.createElementNS(XHTML_NS, "canvas");
1102 canvas.width = width;
1103 canvas.height = height;
1104 canvas.mozOpaque = true;
1105
1106 let ctx = canvas.getContext("2d");
1107
1108 // We need to scale the image to the visible size of the browser,
1109 // in order for the result to appear as the user sees it when
1110 // settings like full zoom come into play.
1111 ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight);
1112
1113 ctx.drawWindow(
1114 win,
1115 win.scrollX,
1116 win.scrollY,
1117 win.innerWidth,
1118 win.innerHeight,
1119 "#fff"
1120 );
1121
1122 return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
1123 },
1124
1125 handleDetectLanguage(global, target) {
1126 let doc = target.content.document;
1127
1128 return promiseDocumentReady(doc).then(() => {
1129 let elem = doc.documentElement;
1130
1131 let language =
1132 elem.getAttribute("xml:lang") ||
1133 elem.getAttribute("lang") ||
1134 doc.contentLanguage ||
1135 null;
1136
1137 // We only want the last element of the TLD here.
1138 // Only country codes have any effect on the results, but other
1139 // values cause no harm.
1140 let tld = doc.location.hostname.match(/[a-z]*$/)[0];
1141
1142 // The CLD2 library used by the language detector is capable of
1143 // analyzing raw HTML. Unfortunately, that takes much more memory,
1144 // and since it's hosted by emscripten, and therefore can't shrink
1145 // its heap after it's grown, it has a performance cost.
1146 // So we send plain text instead.
1147 let encoder = Cu.createDocumentEncoder("text/plain");
1148 encoder.init(
1149 doc,
1150 "text/plain",
1151 Ci.nsIDocumentEncoder.SkipInvisibleContent
1152 );
1153 let text = encoder.encodeToStringWithMaxLength(60 * 1024);
1154
1155 let encoding = doc.characterSet;
1156
1157 return LanguageDetector.detectLanguage({
1158 language,
1159 tld,
1160 text,
1161 encoding,
1162 }).then(result => (result.language === "un" ? "und" : result.language));
1163 });
1164 },
1165
1166 // Used to executeScript, insertCSS and removeCSS.
1167 async handleExtensionExecute(global, target, options, script) {
1168 let executeInWin = window => {
1169 if (script.matchesWindow(window)) {
1170 return script.injectInto(window);
1171 }
1172 return null;
1173 };
1174
1175 let promises;
1176 try {
1177 promises = Array.from(
1178 this.enumerateWindows(global.docShell),
1179 executeInWin
1180 ).filter(promise => promise);
1181 } catch (e) {
1182 Cu.reportError(e);
1183 return Promise.reject({ message: "An unexpected error occurred" });
1184 }
1185
1186 if (!promises.length) {
1187 if (options.frameID) {
1188 return Promise.reject({
1189 message: `Frame not found, or missing host permission`,
1190 });
1191 }
1192
1193 let frames = options.allFrames ? ", and any iframes" : "";
1194 return Promise.reject({
1195 message: `Missing host permission for the tab${frames}`,
1196 });
1197 }
1198 if (!options.allFrames && promises.length > 1) {
1199 return Promise.reject({
1200 message: `Internal error: Script matched multiple windows`,
1201 });
1202 }
1203
1204 let result = await Promise.all(promises);
1205
1206 try {
1207 // Make sure we can structured-clone the result value before
1208 // we try to send it back over the message manager.
1209 Cu.cloneInto(result, target);
1210 } catch (e) {
1211 const { jsPaths } = options;
1212 const fileName = jsPaths.length
1213 ? jsPaths[jsPaths.length - 1]
1214 : "<anonymous code>";
1215 const message = `Script '${fileName}' result is non-structured-clonable data`;
1216 return Promise.reject({ message, fileName });
1217 }
1218
1219 return result;
1220 },
1221
1222 handleWebNavigationGetFrame(global, { frameId }) {
1223 return WebNavigationFrames.getFrame(global.docShell, frameId);
1224 },
1225
1226 handleWebNavigationGetAllFrames(global) {
1227 return WebNavigationFrames.getAllFrames(global.docShell);
1228 },
1229
1230 async receiveMessage(global, name, target, data, recipient) {
1231 switch (name) {
1232 case "Extension:Capture":
1233 return this.handleExtensionCapture(
1234 global,
1235 data.width,
1236 data.height,
1237 data.options
1238 );
1239 case "Extension:DetectLanguage":
1240 return this.handleDetectLanguage(global, target);
1241 case "Extension:Execute":
1242 let policy = WebExtensionPolicy.getByID(recipient.extensionId);
1243
1244 let matcher = new WebExtensionContentScript(policy, data.options);
1245
1246 Object.assign(matcher, {
1247 wantReturnValue: data.options.wantReturnValue,
1248 removeCSS: data.options.removeCSS,
1249 cssOrigin: data.options.cssOrigin,
1250 jsCode: data.options.jsCode,
1251 });
1252
1253 let script = contentScripts.get(matcher);
1254
1255 // Add the cssCode to the script, so that it can be converted into a cached URL.
1256 await script.addCSSCode(data.options.cssCode);
1257 delete data.options.cssCode;
1258
1259 return this.handleExtensionExecute(
1260 global,
1261 target,
1262 data.options,
1263 script
1264 );
1265 case "WebNavigation:GetFrame":
1266 return this.handleWebNavigationGetFrame(global, data.options);
1267 case "WebNavigation:GetAllFrames":
1268 return this.handleWebNavigationGetAllFrames(global);
1269 }
1270 return null;
1271 },
1272
1273 // Helpers
1274
1275 *enumerateWindows(docShell) {
1276 let enum_ = docShell.getDocShellEnumerator(
1277 docShell.typeContent,
1278 docShell.ENUMERATE_FORWARDS
1279 );
1280
1281 for (let docShell of enum_) {
1282 try {
1283 yield docShell.domWindow;
1284 } catch (e) {
1285 // This can fail if the docShell is being destroyed, so just
1286 // ignore the error.
1287 }
1288 }
1289 },
1290};