· 6 years ago · Oct 03, 2019, 05:28 PM
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
12ChromeUtils.import("resource://gre/modules/Services.jsm");
13ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
14
15XPCOMUtils.defineLazyModuleGetters(this, {
16 LanguageDetector: "resource:///modules/translation/LanguageDetector.jsm",
17 MessageChannel: "resource://gre/modules/MessageChannel.jsm",
18 Schemas: "resource://gre/modules/Schemas.jsm",
19 TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
20 WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
21});
22
23XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
24 "@mozilla.org/content/style-sheet-service;1",
25 "nsIStyleSheetService");
26
27const DocumentEncoder = Components.Constructor(
28 "@mozilla.org/layout/documentEncoder;1?type=text/plain",
29 "nsIDocumentEncoder", "init");
30
31const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
32
33ChromeUtils.import("resource://gre/modules/ExtensionChild.jsm");
34ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
35ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
36
37Cu.importGlobalProperties(["crypto", "TextDecoder", "TextEncoder"]);
38
39const {
40 DefaultMap,
41 DefaultWeakMap,
42 defineLazyGetter,
43 getInnerWindowID,
44 getWinUtils,
45 promiseDocumentIdle,
46 promiseDocumentLoaded,
47 promiseDocumentReady,
48 runSafeSyncWithoutClone,
49} = ExtensionUtils;
50
51const {
52 BaseContext,
53 CanOfAPIs,
54 SchemaAPIManager,
55} = ExtensionCommon;
56
57const {
58 BrowserExtensionContent,
59 ChildAPIManager,
60 Messenger,
61} = ExtensionChild;
62
63XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
64
65
66var DocumentManager;
67
68const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
69const CONTENT_SCRIPT_INJECTION_HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS";
70
71var apiManager = new class extends SchemaAPIManager {
72 constructor() {
73 super("content", Schemas);
74 this.initialized = false;
75 }
76
77 lazyInit() {
78 if (!this.initialized) {
79 this.initialized = true;
80 this.initGlobal();
81 for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
82 this.loadScript(value);
83 }
84 }
85 }
86}();
87
88const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
89const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
90
91const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
92const CSSCODE_EXPIRY_TIMEOUT_MS = 10 * 60 * 1000;
93
94const scriptCaches = new WeakSet();
95const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
96
97class CacheMap extends DefaultMap {
98 constructor(timeout, getter, extension) {
99 super(getter);
100
101 this.expiryTimeout = timeout;
102
103 scriptCaches.add(this);
104
105 // This ensures that all the cached scripts and stylesheets are deleted
106 // from the cache and the xpi is no longer actively used.
107 // See Bug 1435100 for rationale.
108 extension.once("shutdown", () => {
109 this.clear(-1);
110 });
111 }
112
113 get(url) {
114 let promise = super.get(url);
115
116 promise.lastUsed = Date.now();
117 if (promise.timer) {
118 promise.timer.cancel();
119 }
120 promise.timer = Timer(this.delete.bind(this, url),
121 this.expiryTimeout,
122 Ci.nsITimer.TYPE_ONE_SHOT);
123
124 return promise;
125 }
126
127 delete(url) {
128 if (this.has(url)) {
129 super.get(url).timer.cancel();
130 }
131
132 super.delete(url);
133 }
134
135 clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) {
136 let now = Date.now();
137 for (let [url, promise] of this.entries()) {
138 // Delete the entry if expired or if clear has been called with timeout -1
139 // (which is used to force the cache to clear all the entries, e.g. when the
140 // extension is shutting down).
141 if (timeout === -1 || (now - promise.lastUsed >= timeout)) {
142 this.delete(url);
143 }
144 }
145 }
146}
147
148class ScriptCache extends CacheMap {
149 constructor(options, extension) {
150 super(SCRIPT_EXPIRY_TIMEOUT_MS, null, extension);
151 this.options = options;
152 }
153
154 defaultConstructor(url) {
155 let promise = ChromeUtils.compileScript(url, this.options);
156 promise.then(script => {
157 promise.script = script;
158 });
159 return promise;
160 }
161}
162
163/**
164 * Shared base class for the two specialized CSS caches:
165 * CSSCache (for the "url"-based stylesheets) and CSSCodeCache
166 * (for the stylesheet defined by plain CSS content as a string).
167 */
168class BaseCSSCache extends CacheMap {
169 constructor(expiryTimeout, defaultConstructor, extension) {
170 super(expiryTimeout, defaultConstructor, extension);
171 }
172
173 addDocument(key, document) {
174 sheetCacheDocuments.get(this.get(key)).add(document);
175 }
176
177 deleteDocument(key, document) {
178 sheetCacheDocuments.get(this.get(key)).delete(document);
179 }
180
181 delete(key) {
182 if (this.has(key)) {
183 let promise = this.get(key);
184
185 // Never remove a sheet from the cache if it's still being used by a
186 // document. Rule processors can be shared between documents with the
187 // same preloaded sheet, so we only lose by removing them while they're
188 // still in use.
189 let docs = ChromeUtils.nondeterministicGetWeakSetKeys(sheetCacheDocuments.get(promise));
190 if (docs.length) {
191 return;
192 }
193 }
194
195 super.delete(key);
196 }
197}
198
199/**
200 * Cache of the preloaded stylesheet defined by url.
201 */
202class CSSCache extends BaseCSSCache {
203 constructor(sheetType, extension) {
204 super(CSS_EXPIRY_TIMEOUT_MS, url => {
205 let uri = Services.io.newURI(url);
206 return styleSheetService.preloadSheetAsync(uri, sheetType).then(sheet => {
207 return {url, sheet};
208 });
209 }, extension);
210 }
211}
212
213/**
214 * Cache of the preloaded stylesheet defined by plain CSS content as a string,
215 * the key of the cached stylesheet is the hash of its "CSSCode" string.
216 */
217class CSSCodeCache extends BaseCSSCache {
218 constructor(sheetType, extension) {
219 super(CSSCODE_EXPIRY_TIMEOUT_MS, (hash) => {
220 if (!this.has(hash)) {
221 // Do not allow the getter to be used to lazily create the cached stylesheet,
222 // the cached CSSCode stylesheet has to be explicitly set.
223 throw new Error("Unexistent cached cssCode stylesheet: " + Error().stack);
224 }
225
226 return super.get(hash);
227 }, extension);
228
229 // Store the preferred sheetType (used to preload the expected stylesheet type in
230 // the addCSSCode method).
231 this.sheetType = sheetType;
232 }
233
234 addCSSCode(hash, cssCode) {
235 if (this.has(hash)) {
236 // This cssCode have been already cached, no need to create it again.
237 return;
238 }
239 const uri = Services.io.newURI("data:text/css;charset=utf-8," + encodeURIComponent(cssCode));
240 const value = styleSheetService.preloadSheetAsync(uri, this.sheetType).then(sheet => {
241 return {sheet, uri};
242 });
243
244 super.set(hash, value);
245 }
246}
247
248defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", function() {
249 return new ScriptCache({hasReturnValue: false}, this);
250});
251
252defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", function() {
253 return new ScriptCache({hasReturnValue: true}, this);
254});
255
256defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", function() {
257 return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET, this);
258});
259
260defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", function() {
261 return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
262});
263
264// These two caches are similar to the above but specialized to cache the cssCode
265// using an hash computed from the cssCode string as the key (instead of the generated data
266// URI which can be pretty long for bigger injected cssCode).
267defineLazyGetter(BrowserExtensionContent.prototype, "userCSSCode", function() {
268 return new CSSCodeCache(Ci.nsIStyleSheetService.USER_SHEET, this);
269});
270
271defineLazyGetter(BrowserExtensionContent.prototype, "authorCSSCode", function() {
272 return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
273});
274
275// Represents a content script.
276class Script {
277 constructor(extension, matcher) {
278 this.extension = extension;
279 this.matcher = matcher;
280
281 this.runAt = this.matcher.runAt;
282 this.js = this.matcher.jsPaths;
283 this.css = this.matcher.cssPaths.slice();
284 this.cssCodeHash = null;
285
286 this.removeCSS = this.matcher.removeCSS;
287 this.cssOrigin = this.matcher.cssOrigin;
288
289 this.cssCache = extension[
290 this.cssOrigin === "user" ? "userCSS" : "authorCSS"
291 ];
292 this.cssCodeCache = extension[
293 this.cssOrigin === "user" ? "userCSSCode" : "authorCSSCode"
294 ];
295 this.scriptCache = extension[
296 matcher.wantReturnValue ? "dynamicScripts" : "staticScripts"
297 ];
298
299 if (matcher.wantReturnValue) {
300 this.compileScripts();
301 this.loadCSS();
302 }
303 }
304
305 get requiresCleanup() {
306 return !this.removeCss && (this.css.length > 0 || this.cssCodeHash);
307 }
308
309 async addCSSCode(cssCode) {
310 if (!cssCode) {
311 return;
312 }
313
314 // Store the hash of the cssCode.
315 const buffer = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(cssCode));
316 this.cssCodeHash = new TextDecoder().decode(buffer);
317
318 // Cache and preload the cssCode stylesheet.
319 this.cssCodeCache.addCSSCode(this.cssCodeHash, cssCode);
320 }
321
322 compileScripts() {
323 return this.js.map(url => this.scriptCache.get(url));
324 }
325
326 loadCSS() {
327 return this.css.map(url => this.cssCache.get(url));
328 }
329
330 preload() {
331 this.loadCSS();
332 this.compileScripts();
333 }
334
335 cleanup(window) {
336 if (this.requiresCleanup) {
337 if (window) {
338 let winUtils = getWinUtils(window);
339
340 let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
341
342 for (let url of this.css) {
343 this.cssCache.deleteDocument(url, window.document);
344 runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
345 }
346
347 const {cssCodeHash} = this;
348
349 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
350 this.cssCodeCache.get(cssCodeHash).then(({uri}) => {
351 runSafeSyncWithoutClone(winUtils.removeSheet, uri, type);
352 });
353 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
354 }
355 }
356
357 // Clear any sheets that were kept alive past their timeout as
358 // a result of living in this document.
359 this.cssCodeCache.clear(CSSCODE_EXPIRY_TIMEOUT_MS);
360 this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
361 }
362 }
363
364 matchesWindow(window) {
365 return this.matcher.matchesWindow(window);
366 }
367
368 async injectInto(window) {
369 let context = this.extension.getContext(window);
370 try {
371 if (this.runAt === "document_end") {
372 await promiseDocumentReady(window.document);
373 } else if (this.runAt === "document_idle") {
374 await Promise.race([
375 promiseDocumentIdle(window),
376 promiseDocumentLoaded(window.document),
377 ]);
378 }
379
380 return this.inject(context);
381 } catch (e) {
382 return Promise.reject(context.normalizeError(e));
383 }
384 }
385
386 /**
387 * Tries to inject this script into the given window and sandbox, if
388 * there are pending operations for the window's current load state.
389 *
390 * @param {BaseContext} context
391 * The content script context into which to inject the scripts.
392 * @returns {Promise<any>}
393 * Resolves to the last value in the evaluated script, when
394 * execution is complete.
395 */
396 async inject(context) {
397 DocumentManager.lazyInit();
398 if (this.requiresCleanup) {
399 context.addScript(this);
400 }
401
402 const {cssCodeHash} = this;
403
404 let cssPromise;
405 if (this.css.length || cssCodeHash) {
406 let window = context.contentWindow;
407 let winUtils = getWinUtils(window);
408
409 let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
410
411 if (this.removeCSS) {
412 for (let url of this.css) {
413 this.cssCache.deleteDocument(url, window.document);
414
415 runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
416 }
417
418 if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
419 const {uri} = await this.cssCodeCache.get(cssCodeHash);
420 this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
421
422 runSafeSyncWithoutClone(winUtils.removeSheet, uri, type);
423 }
424 } else {
425 cssPromise = Promise.all(this.loadCSS()).then(sheets => {
426 let window = context.contentWindow;
427 if (!window) {
428 return;
429 }
430
431 for (let {url, sheet} of sheets) {
432 this.cssCache.addDocument(url, window.document);
433
434 runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
435 }
436 });
437
438 if (cssCodeHash) {
439 cssPromise = cssPromise.then(async () => {
440 const {sheet} = await this.cssCodeCache.get(cssCodeHash);
441 this.cssCodeCache.addDocument(cssCodeHash, window.document);
442
443 runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
444 });
445 }
446
447 // We're loading stylesheets via the stylesheet service, which means
448 // that the normal mechanism for blocking layout and onload for pending
449 // stylesheets aren't in effect (since there's no document to block). So
450 // we need to do something custom here, similar to what we do for
451 // scripts. Blocking parsing is overkill, since we really just want to
452 // block layout and onload. But we have an API to do the former and not
453 // the latter, so we do it that way. This hopefully isn't a performance
454 // problem since there are no network loads involved, and since we cache
455 // the stylesheets on first load. We should fix this up if it does becomes
456 // a problem.
457 if (this.css.length > 0) {
458 context.contentWindow.document.blockParsing(cssPromise, {blockScriptCreated: false});
459 }
460 }
461 }
462
463 let scriptPromises = this.compileScripts();
464
465 let scripts = scriptPromises.map(promise => promise.script);
466 // If not all scripts are already available in the cache, block
467 // parsing and wait all promises to resolve.
468 if (!scripts.every(script => script)) {
469 let promise = Promise.all(scriptPromises);
470
471 // If we're supposed to inject at the start of the document load,
472 // and we haven't already missed that point, block further parsing
473 // until the scripts have been loaded.
474 let {document} = context.contentWindow;
475 if (this.runAt === "document_start" && document.readyState !== "complete") {
476 document.blockParsing(promise, {blockScriptCreated: false});
477 }
478
479 scripts = await promise;
480 }
481
482 let result;
483
484 // The evaluations below may throw, in which case the promise will be
485 // automatically rejected.
486 TelemetryStopwatch.start(CONTENT_SCRIPT_INJECTION_HISTOGRAM, context);
487 try {
488 for (let script of scripts) {
489 result = script.executeInGlobal(context.cloneScope);
490 }
491
492 if (this.matcher.jsCode) {
493 result = Cu.evalInSandbox(this.matcher.jsCode, context.cloneScope, "latest");
494 }
495 } finally {
496 TelemetryStopwatch.finish(CONTENT_SCRIPT_INJECTION_HISTOGRAM, context);
497 }
498
499 await cssPromise;
500 return result;
501 }
502}
503
504/**
505 * An execution context for semi-privileged extension content scripts.
506 *
507 * This is the child side of the ContentScriptContextParent class
508 * defined in ExtensionParent.jsm.
509 */
510class ContentScriptContextChild extends BaseContext {
511 constructor(extension, contentWindow) {
512 super("content_child", extension);
513
514 this.setContentWindow(contentWindow);
515
516 let frameId = WebNavigationFrames.getFrameId(contentWindow);
517 this.frameId = frameId;
518
519 this.scripts = [];
520
521 let contentPrincipal = contentWindow.document.nodePrincipal;
522 let ssm = Services.scriptSecurityManager;
523
524 // Copy origin attributes from the content window origin attributes to
525 // preserve the user context id.
526 let attrs = contentPrincipal.originAttributes;
527 let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs);
528
529 this.isExtensionPage = contentPrincipal.equals(extensionPrincipal);
530
531 let principal;
532 if (ssm.isSystemPrincipal(contentPrincipal)) {
533 // Make sure we don't hand out the system principal by accident.
534 // also make sure that the null principal has the right origin attributes
535 principal = ssm.createNullPrincipal(attrs);
536 } else if (this.isExtensionPage) {
537 principal = contentPrincipal;
538 } else {
539 principal = [contentPrincipal, extensionPrincipal];
540 }
541
542 if (this.isExtensionPage) {
543 // This is an iframe with content script API enabled and its principal
544 // should be the contentWindow itself. We create a sandbox with the
545 // contentWindow as principal and with X-rays disabled because it
546 // enables us to create the APIs object in this sandbox object and then
547 // copying it into the iframe's window. See bug 1214658.
548 this.sandbox = Cu.Sandbox(contentWindow, {
549 sandboxName: `Web-Accessible Extension Page ${extension.policy.debugName}`,
550 sandboxPrototype: contentWindow,
551 sameZoneAs: contentWindow,
552 wantXrays: false,
553 isWebExtensionContentScript: true,
554 });
555 } else {
556 // This metadata is required by the Developer Tools, in order for
557 // the content script to be associated with both the extension and
558 // the tab holding the content page.
559 let metadata = {
560 "inner-window-id": this.innerWindowID,
561 addonId: extensionPrincipal.addonId,
562 };
563
564 this.sandbox = Cu.Sandbox(principal, {
565 metadata,
566 sandboxName: `Content Script ${extension.policy.debugName}`,
567 sandboxPrototype: contentWindow,
568 sameZoneAs: contentWindow,
569 wantXrays: true,
570 isWebExtensionContentScript: true,
571 wantExportHelpers: true,
572 wantGlobalProperties: ["XMLHttpRequest", "fetch"],
573 originAttributes: attrs,
574 });
575
576 // Preserve a copy of the original window's XMLHttpRequest and fetch
577 // in a content object (fetch is manually binded to the window
578 // to prevent it from raising a TypeError because content object is not
579 // a real window).
580 Cu.evalInSandbox(`
581 this.content = {
582 XMLHttpRequest: window.XMLHttpRequest,
583 fetch: window.fetch.bind(window),
584 };
585
586 window.JSON = JSON;
587 window.XMLHttpRequest = XMLHttpRequest;
588 window.fetch = fetch;
589 `, this.sandbox);
590 }
591
592 Object.defineProperty(this, "principal", {
593 value: Cu.getObjectPrincipal(this.sandbox),
594 enumerable: true,
595 configurable: true,
596 });
597
598 this.url = contentWindow.location.href;
599
600 defineLazyGetter(this, "chromeObj", () => {
601 let chromeObj = Cu.createObjectIn(this.sandbox);
602
603 this.childManager.inject(chromeObj);
604 return chromeObj;
605 });
606
607 Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
608 Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
609 }
610
611 injectAPI() {
612 if (!this.isExtensionPage) {
613 throw new Error("Cannot inject extension API into non-extension window");
614 }
615
616 // This is an iframe with content script API enabled (See Bug 1214658)
617 Schemas.exportLazyGetter(this.contentWindow,
618 "browser", () => this.chromeObj);
619 Schemas.exportLazyGetter(this.contentWindow,
620 "chrome", () => this.chromeObj);
621 }
622
623 get cloneScope() {
624 return this.sandbox;
625 }
626
627 addScript(script) {
628 if (script.requiresCleanup) {
629 this.scripts.push(script);
630 }
631 }
632
633 close() {
634 super.unload();
635
636 // Cleanup the scripts even if the contentWindow have been destroyed.
637 for (let script of this.scripts) {
638 script.cleanup(this.contentWindow);
639 }
640
641 if (this.contentWindow) {
642 // Overwrite the content script APIs with an empty object if the APIs objects are still
643 // defined in the content window (See Bug 1214658).
644 if (this.isExtensionPage) {
645 Cu.createObjectIn(this.contentWindow, {defineAs: "browser"});
646 Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
647 }
648 }
649 Cu.nukeSandbox(this.sandbox);
650 this.sandbox = null;
651 }
652}
653
654defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
655 // The |sender| parameter is passed directly to the extension.
656 let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
657 let filter = {extensionId: this.extension.id};
658 let optionalFilter = {frameId: this.frameId};
659
660 return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
661});
662
663defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() {
664 apiManager.lazyInit();
665
666 let localApis = {};
667 let can = new CanOfAPIs(this, apiManager, localApis);
668
669 let childManager = new ChildAPIManager(this, this.messageManager, can, {
670 envType: "content_parent",
671 url: this.url,
672 });
673
674 this.callOnClose(childManager);
675
676 return childManager;
677});
678
679// Responsible for creating ExtensionContexts and injecting content
680// scripts into them when new documents are created.
681DocumentManager = {
682 // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
683 contexts: new Map(),
684
685 initialized: false,
686
687 lazyInit() {
688 if (this.initialized) {
689 return;
690 }
691 this.initialized = true;
692
693 Services.obs.addObserver(this, "inner-window-destroyed");
694 Services.obs.addObserver(this, "memory-pressure");
695 },
696
697 uninit() {
698 Services.obs.removeObserver(this, "inner-window-destroyed");
699 Services.obs.removeObserver(this, "memory-pressure");
700 },
701
702 observers: {
703 "inner-window-destroyed"(subject, topic, data) {
704 let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
705
706 MessageChannel.abortResponses({innerWindowID: windowId});
707
708 // Close any existent content-script context for the destroyed window.
709 if (this.contexts.has(windowId)) {
710 let extensions = this.contexts.get(windowId);
711 for (let context of extensions.values()) {
712 context.close();
713 }
714
715 this.contexts.delete(windowId);
716 }
717 },
718 "memory-pressure"(subject, topic, data) {
719 let timeout = data === "heap-minimize" ? 0 : undefined;
720
721 for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
722 cache.clear(timeout);
723 }
724 },
725 },
726
727 observe(subject, topic, data) {
728 this.observers[topic].call(this, subject, topic, data);
729 },
730
731 shutdownExtension(extension) {
732 for (let extensions of this.contexts.values()) {
733 let context = extensions.get(extension);
734 if (context) {
735 context.close();
736 extensions.delete(extension);
737 }
738 }
739 },
740
741 getContexts(window) {
742 let winId = getInnerWindowID(window);
743
744 let extensions = this.contexts.get(winId);
745 if (!extensions) {
746 extensions = new Map();
747 this.contexts.set(winId, extensions);
748 }
749
750 return extensions;
751 },
752
753 // For test use only.
754 getContext(extensionId, window) {
755 for (let [extension, context] of this.getContexts(window)) {
756 if (extension.id === extensionId) {
757 return context;
758 }
759 }
760 },
761
762 getContentScriptGlobals(window) {
763 let extensions = this.contexts.get(getInnerWindowID(window));
764
765 if (extensions) {
766 return Array.from(extensions.values(), ctx => ctx.sandbox);
767 }
768
769 return [];
770 },
771
772 initExtensionContext(extension, window) {
773 extension.getContext(window).injectAPI();
774 },
775};
776
777var ExtensionContent = {
778 BrowserExtensionContent,
779 Script,
780
781 shutdownExtension(extension) {
782 DocumentManager.shutdownExtension(extension);
783 },
784
785 // This helper is exported to be integrated in the devtools RDP actors,
786 // that can use it to retrieve the existent WebExtensions ContentScripts
787 // of a target window and be able to show the ContentScripts source in the
788 // DevTools Debugger panel.
789 getContentScriptGlobals(window) {
790 return DocumentManager.getContentScriptGlobals(window);
791 },
792
793 initExtensionContext(extension, window) {
794 DocumentManager.initExtensionContext(extension, window);
795 },
796
797 getContext(extension, window) {
798 let extensions = DocumentManager.getContexts(window);
799
800 let context = extensions.get(extension);
801 if (!context) {
802 context = new ContentScriptContextChild(extension, window);
803 extensions.set(extension, context);
804 }
805 return context;
806 },
807
808 handleExtensionCapture(global, width, height, options) {
809 let win = global.content;
810
811 const XHTML_NS = "http://www.w3.org/1999/xhtml";
812 let canvas = win.document.createElementNS(XHTML_NS, "canvas");
813 canvas.width = width;
814 canvas.height = height;
815 canvas.mozOpaque = true;
816
817 let ctx = canvas.getContext("2d");
818
819 // We need to scale the image to the visible size of the browser,
820 // in order for the result to appear as the user sees it when
821 // settings like full zoom come into play.
822 ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight);
823
824 ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff");
825
826 return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
827 },
828
829 handleDetectLanguage(global, target) {
830 let doc = target.content.document;
831
832 return promiseDocumentReady(doc).then(() => {
833 let elem = doc.documentElement;
834
835 let language = (elem.getAttribute("xml:lang") || elem.getAttribute("lang") ||
836 doc.contentLanguage || null);
837
838 // We only want the last element of the TLD here.
839 // Only country codes have any effect on the results, but other
840 // values cause no harm.
841 let tld = doc.location.hostname.match(/[a-z]*$/)[0];
842
843 // The CLD2 library used by the language detector is capable of
844 // analyzing raw HTML. Unfortunately, that takes much more memory,
845 // and since it's hosted by emscripten, and therefore can't shrink
846 // its heap after it's grown, it has a performance cost.
847 // So we send plain text instead.
848 let encoder = new DocumentEncoder(doc, "text/plain", Ci.nsIDocumentEncoder.SkipInvisibleContent);
849 let text = encoder.encodeToStringWithMaxLength(60 * 1024);
850
851 let encoding = doc.characterSet;
852
853 return LanguageDetector.detectLanguage({language, tld, text, encoding})
854 .then(result => result.language === "un" ? "und" : result.language);
855 });
856 },
857
858 // Used to executeScript, insertCSS and removeCSS.
859 async handleExtensionExecute(global, target, options, script) {
860 let executeInWin = (window) => {
861 if (script.matchesWindow(window)) {
862 return script.injectInto(window);
863 }
864 return null;
865 };
866
867 let promises;
868 try {
869 promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
870 .filter(promise => promise);
871 } catch (e) {
872 Cu.reportError(e);
873 return Promise.reject({message: "An unexpected error occurred"});
874 }
875
876 if (!promises.length) {
877 if (options.frame_id) {
878 return Promise.reject({message: `Frame not found, or missing host permission`});
879 }
880
881 let frames = options.all_frames ? ", and any iframes" : "";
882 return Promise.reject({message: `Missing host permission for the tab${frames}`});
883 }
884 if (!options.all_frames && promises.length > 1) {
885 return Promise.reject({message: `Internal error: Script matched multiple windows`});
886 }
887
888 let result = await Promise.all(promises);
889
890 try {
891 // Make sure we can structured-clone the result value before
892 // we try to send it back over the message manager.
893 Cu.cloneInto(result, target);
894 } catch (e) {
895 const {js} = options;
896 const fileName = js.length ? js[js.length - 1] : "<anonymous code>";
897 const message = `Script '${fileName}' result is non-structured-clonable data`;
898 return Promise.reject({message, fileName});
899 }
900
901 return result;
902 },
903
904 handleWebNavigationGetFrame(global, {frameId}) {
905 return WebNavigationFrames.getFrame(global.docShell, frameId);
906 },
907
908 handleWebNavigationGetAllFrames(global) {
909 return WebNavigationFrames.getAllFrames(global.docShell);
910 },
911
912 // Helpers
913
914 * enumerateWindows(docShell) {
915 let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
916 docShell.ENUMERATE_FORWARDS);
917
918 for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) {
919 try {
920 yield docShell.getInterface(Ci.nsIDOMWindow);
921 } catch (e) {
922 // This can fail if the docShell is being destroyed, so just
923 // ignore the error.
924 }
925 }
926 },
927};