mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-03 01:38:46 +02:00 
			
		
		
		
	Original Revision: https://phabricator.services.mozilla.com/D214296 Differential Revision: https://phabricator.services.mozilla.com/D214816
		
			
				
	
	
		
			1405 lines
		
	
	
	
		
			44 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1405 lines
		
	
	
	
		
			44 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | 
						|
/* vim: set sts=2 sw=2 et tw=80: */
 | 
						|
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | 
						|
 | 
						|
/** @type {Lazy} */
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  ExtensionProcessScript:
 | 
						|
    "resource://gre/modules/ExtensionProcessScript.sys.mjs",
 | 
						|
  ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
 | 
						|
  LanguageDetector:
 | 
						|
    "resource://gre/modules/translation/LanguageDetector.sys.mjs",
 | 
						|
  Schemas: "resource://gre/modules/Schemas.sys.mjs",
 | 
						|
  WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  lazy,
 | 
						|
  "styleSheetService",
 | 
						|
  "@mozilla.org/content/style-sheet-service;1",
 | 
						|
  "nsIStyleSheetService"
 | 
						|
);
 | 
						|
 | 
						|
const Timer = Components.Constructor(
 | 
						|
  "@mozilla.org/timer;1",
 | 
						|
  "nsITimer",
 | 
						|
  "initWithCallback"
 | 
						|
);
 | 
						|
 | 
						|
const ScriptError = Components.Constructor(
 | 
						|
  "@mozilla.org/scripterror;1",
 | 
						|
  "nsIScriptError",
 | 
						|
  "initWithWindowID"
 | 
						|
);
 | 
						|
 | 
						|
import {
 | 
						|
  ChildAPIManager,
 | 
						|
  ExtensionChild,
 | 
						|
  ExtensionActivityLogChild,
 | 
						|
  Messenger,
 | 
						|
} from "resource://gre/modules/ExtensionChild.sys.mjs";
 | 
						|
import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
 | 
						|
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | 
						|
 | 
						|
const {
 | 
						|
  DefaultMap,
 | 
						|
  DefaultWeakMap,
 | 
						|
  getInnerWindowID,
 | 
						|
  promiseDocumentIdle,
 | 
						|
  promiseDocumentLoaded,
 | 
						|
  promiseDocumentReady,
 | 
						|
} = ExtensionUtils;
 | 
						|
 | 
						|
const {
 | 
						|
  BaseContext,
 | 
						|
  CanOfAPIs,
 | 
						|
  SchemaAPIManager,
 | 
						|
  defineLazyGetter,
 | 
						|
  redefineGetter,
 | 
						|
  runSafeSyncWithoutClone,
 | 
						|
} = ExtensionCommon;
 | 
						|
 | 
						|
ChromeUtils.defineLazyGetter(lazy, "isContentScriptProcess", () => {
 | 
						|
  return (
 | 
						|
    Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT ||
 | 
						|
    !WebExtensionPolicy.useRemoteWebExtensions ||
 | 
						|
    // Thunderbird still loads some content in the parent process.
 | 
						|
    AppConstants.MOZ_APP_NAME == "thunderbird"
 | 
						|
  );
 | 
						|
});
 | 
						|
 | 
						|
var DocumentManager;
 | 
						|
 | 
						|
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 | 
						|
 | 
						|
var apiManager = new (class extends SchemaAPIManager {
 | 
						|
  constructor() {
 | 
						|
    super("content", lazy.Schemas);
 | 
						|
    this.initialized = false;
 | 
						|
  }
 | 
						|
 | 
						|
  lazyInit() {
 | 
						|
    if (!this.initialized) {
 | 
						|
      this.initialized = true;
 | 
						|
      this.initGlobal();
 | 
						|
      for (let { value } of Services.catMan.enumerateCategory(
 | 
						|
        CATEGORY_EXTENSION_SCRIPTS_CONTENT
 | 
						|
      )) {
 | 
						|
        this.loadScript(value);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
})();
 | 
						|
 | 
						|
const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
 | 
						|
const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
 | 
						|
 | 
						|
const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
 | 
						|
const CSSCODE_EXPIRY_TIMEOUT_MS = 10 * 60 * 1000;
 | 
						|
 | 
						|
const scriptCaches = new WeakSet();
 | 
						|
const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
 | 
						|
 | 
						|
class CacheMap extends DefaultMap {
 | 
						|
  constructor(timeout, getter, extension) {
 | 
						|
    super(getter);
 | 
						|
 | 
						|
    this.expiryTimeout = timeout;
 | 
						|
 | 
						|
    scriptCaches.add(this);
 | 
						|
 | 
						|
    // This ensures that all the cached scripts and stylesheets are deleted
 | 
						|
    // from the cache and the xpi is no longer actively used.
 | 
						|
    // See Bug 1435100 for rationale.
 | 
						|
    extension.once("shutdown", () => {
 | 
						|
      this.clear(-1);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  get(url) {
 | 
						|
    let promise = super.get(url);
 | 
						|
 | 
						|
    promise.lastUsed = Date.now();
 | 
						|
    if (promise.timer) {
 | 
						|
      promise.timer.cancel();
 | 
						|
    }
 | 
						|
    promise.timer = Timer(
 | 
						|
      this.delete.bind(this, url),
 | 
						|
      this.expiryTimeout,
 | 
						|
      Ci.nsITimer.TYPE_ONE_SHOT
 | 
						|
    );
 | 
						|
 | 
						|
    return promise;
 | 
						|
  }
 | 
						|
 | 
						|
  delete(url) {
 | 
						|
    if (this.has(url)) {
 | 
						|
      super.get(url).timer.cancel();
 | 
						|
    }
 | 
						|
 | 
						|
    return super.delete(url);
 | 
						|
  }
 | 
						|
 | 
						|
  clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) {
 | 
						|
    let now = Date.now();
 | 
						|
    for (let [url, promise] of this.entries()) {
 | 
						|
      // Delete the entry if expired or if clear has been called with timeout -1
 | 
						|
      // (which is used to force the cache to clear all the entries, e.g. when the
 | 
						|
      // extension is shutting down).
 | 
						|
      if (timeout === -1 || now - promise.lastUsed >= timeout) {
 | 
						|
        this.delete(url);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ScriptCache extends CacheMap {
 | 
						|
  constructor(options, extension) {
 | 
						|
    super(
 | 
						|
      SCRIPT_EXPIRY_TIMEOUT_MS,
 | 
						|
      url => {
 | 
						|
        /** @type {Promise<PrecompiledScript> & { script?: PrecompiledScript }} */
 | 
						|
        let promise = ChromeUtils.compileScript(url, options);
 | 
						|
        promise.then(script => {
 | 
						|
          promise.script = script;
 | 
						|
        });
 | 
						|
        return promise;
 | 
						|
      },
 | 
						|
      extension
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Shared base class for the two specialized CSS caches:
 | 
						|
 * CSSCache (for the "url"-based stylesheets) and CSSCodeCache
 | 
						|
 * (for the stylesheet defined by plain CSS content as a string).
 | 
						|
 */
 | 
						|
class BaseCSSCache extends CacheMap {
 | 
						|
  constructor(expiryTimeout, defaultConstructor, extension) {
 | 
						|
    super(expiryTimeout, defaultConstructor, extension);
 | 
						|
  }
 | 
						|
 | 
						|
  addDocument(key, document) {
 | 
						|
    sheetCacheDocuments.get(this.get(key)).add(document);
 | 
						|
  }
 | 
						|
 | 
						|
  deleteDocument(key, document) {
 | 
						|
    sheetCacheDocuments.get(this.get(key)).delete(document);
 | 
						|
  }
 | 
						|
 | 
						|
  delete(key) {
 | 
						|
    if (this.has(key)) {
 | 
						|
      let promise = this.get(key);
 | 
						|
 | 
						|
      // Never remove a sheet from the cache if it's still being used by a
 | 
						|
      // document. Rule processors can be shared between documents with the
 | 
						|
      // same preloaded sheet, so we only lose by removing them while they're
 | 
						|
      // still in use.
 | 
						|
      let docs = ChromeUtils.nondeterministicGetWeakSetKeys(
 | 
						|
        sheetCacheDocuments.get(promise)
 | 
						|
      );
 | 
						|
      if (docs.length) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return super.delete(key);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Cache of the preloaded stylesheet defined by url.
 | 
						|
 */
 | 
						|
class CSSCache extends BaseCSSCache {
 | 
						|
  constructor(sheetType, extension) {
 | 
						|
    super(
 | 
						|
      CSS_EXPIRY_TIMEOUT_MS,
 | 
						|
      url => {
 | 
						|
        let uri = Services.io.newURI(url);
 | 
						|
        return lazy.styleSheetService
 | 
						|
          .preloadSheetAsync(uri, sheetType)
 | 
						|
          .then(sheet => {
 | 
						|
            return { url, sheet };
 | 
						|
          });
 | 
						|
      },
 | 
						|
      extension
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Cache of the preloaded stylesheet defined by plain CSS content as a string,
 | 
						|
 * the key of the cached stylesheet is the hash of its "CSSCode" string.
 | 
						|
 */
 | 
						|
class CSSCodeCache extends BaseCSSCache {
 | 
						|
  constructor(sheetType, extension) {
 | 
						|
    super(
 | 
						|
      CSSCODE_EXPIRY_TIMEOUT_MS,
 | 
						|
      hash => {
 | 
						|
        if (!this.has(hash)) {
 | 
						|
          // Do not allow the getter to be used to lazily create the cached stylesheet,
 | 
						|
          // the cached CSSCode stylesheet has to be explicitly set.
 | 
						|
          throw new Error(
 | 
						|
            "Unexistent cached cssCode stylesheet: " + Error().stack
 | 
						|
          );
 | 
						|
        }
 | 
						|
 | 
						|
        return super.get(hash);
 | 
						|
      },
 | 
						|
      extension
 | 
						|
    );
 | 
						|
 | 
						|
    // Store the preferred sheetType (used to preload the expected stylesheet type in
 | 
						|
    // the addCSSCode method).
 | 
						|
    this.sheetType = sheetType;
 | 
						|
  }
 | 
						|
 | 
						|
  addCSSCode(hash, cssCode) {
 | 
						|
    if (this.has(hash)) {
 | 
						|
      // This cssCode have been already cached, no need to create it again.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // The `webext=style` portion is added metadata to help us distinguish
 | 
						|
    // different kinds of data URL loads that are triggered with the
 | 
						|
    // SystemPrincipal. It shall be removed with bug 1699425.
 | 
						|
    const uri = Services.io.newURI(
 | 
						|
      "data:text/css;extension=style;charset=utf-8," +
 | 
						|
        encodeURIComponent(cssCode)
 | 
						|
    );
 | 
						|
    const value = lazy.styleSheetService
 | 
						|
      .preloadSheetAsync(uri, this.sheetType)
 | 
						|
      .then(sheet => {
 | 
						|
        return { sheet, uri };
 | 
						|
      });
 | 
						|
 | 
						|
    super.set(hash, value);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "staticScripts", function () {
 | 
						|
  return new ScriptCache({ hasReturnValue: false }, this);
 | 
						|
});
 | 
						|
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "dynamicScripts", function () {
 | 
						|
  return new ScriptCache({ hasReturnValue: true }, this);
 | 
						|
});
 | 
						|
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "anonStaticScripts", function () {
 | 
						|
  // TODO bug 1651557: Use dynamic name to improve debugger experience.
 | 
						|
  const filename = "<anonymous code>";
 | 
						|
  return new ScriptCache({ filename, hasReturnValue: false }, this);
 | 
						|
});
 | 
						|
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "anonDynamicScripts", function () {
 | 
						|
  // TODO bug 1651557: Use dynamic name to improve debugger experience.
 | 
						|
  const filename = "<anonymous code>";
 | 
						|
  return new ScriptCache({ filename, hasReturnValue: true }, this);
 | 
						|
});
 | 
						|
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "userCSS", function () {
 | 
						|
  return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET, this);
 | 
						|
});
 | 
						|
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "authorCSS", function () {
 | 
						|
  return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
 | 
						|
});
 | 
						|
 | 
						|
// These two caches are similar to the above but specialized to cache the cssCode
 | 
						|
// using an hash computed from the cssCode string as the key (instead of the generated data
 | 
						|
// URI which can be pretty long for bigger injected cssCode).
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "userCSSCode", function () {
 | 
						|
  return new CSSCodeCache(Ci.nsIStyleSheetService.USER_SHEET, this);
 | 
						|
});
 | 
						|
 | 
						|
defineLazyGetter(ExtensionChild.prototype, "authorCSSCode", function () {
 | 
						|
  return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
 | 
						|
});
 | 
						|
 | 
						|
// Represents a content script.
 | 
						|
class Script {
 | 
						|
  /**
 | 
						|
   * @param {ExtensionChild} extension
 | 
						|
   * @param {WebExtensionContentScript|object} matcher
 | 
						|
   *        An object with a "matchesWindowGlobal" method and content script
 | 
						|
   *        execution details. This is usually a plain WebExtensionContentScript
 | 
						|
   *        except when the script is run via `tabs.executeScript` or
 | 
						|
   *        `scripting.executeScript`. In this case, the object may have some
 | 
						|
   *        extra properties: wantReturnValue, removeCSS, cssOrigin
 | 
						|
   */
 | 
						|
  constructor(extension, matcher) {
 | 
						|
    this.scriptType = "content_script";
 | 
						|
    this.extension = extension;
 | 
						|
    this.matcher = matcher;
 | 
						|
 | 
						|
    this.runAt = this.matcher.runAt;
 | 
						|
    this.world = this.matcher.world;
 | 
						|
    this.js = this.matcher.jsPaths;
 | 
						|
    this.jsCode = null; // tabs/scripting.executeScript + ISOLATED world.
 | 
						|
    this.jsCodeCompiledScript = null; // scripting.executeScript + MAIN world.
 | 
						|
    this.css = this.matcher.cssPaths.slice();
 | 
						|
    this.cssCodeHash = null;
 | 
						|
 | 
						|
    this.removeCSS = this.matcher.removeCSS;
 | 
						|
    this.cssOrigin = this.matcher.cssOrigin;
 | 
						|
 | 
						|
    this.cssCache =
 | 
						|
      extension[this.cssOrigin === "user" ? "userCSS" : "authorCSS"];
 | 
						|
    this.cssCodeCache =
 | 
						|
      extension[this.cssOrigin === "user" ? "userCSSCode" : "authorCSSCode"];
 | 
						|
    if (this.world === "MAIN") {
 | 
						|
      this.scriptCache = matcher.wantReturnValue
 | 
						|
        ? extension.anonDynamicScripts
 | 
						|
        : extension.anonStaticScripts;
 | 
						|
    } else {
 | 
						|
      this.scriptCache = matcher.wantReturnValue
 | 
						|
        ? extension.dynamicScripts
 | 
						|
        : extension.staticScripts;
 | 
						|
    }
 | 
						|
 | 
						|
    /** @type {WeakSet<Document>} A set of documents injected into. */
 | 
						|
    this.injectedInto = new WeakSet();
 | 
						|
 | 
						|
    if (matcher.wantReturnValue) {
 | 
						|
      this.compileScripts();
 | 
						|
      this.loadCSS();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  get requiresCleanup() {
 | 
						|
    return !this.removeCSS && (!!this.css.length || this.cssCodeHash);
 | 
						|
  }
 | 
						|
 | 
						|
  async addCSSCode(cssCode) {
 | 
						|
    if (!cssCode) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Store the hash of the cssCode.
 | 
						|
    const buffer = await crypto.subtle.digest(
 | 
						|
      "SHA-1",
 | 
						|
      new TextEncoder().encode(cssCode)
 | 
						|
    );
 | 
						|
    this.cssCodeHash = String.fromCharCode(...new Uint16Array(buffer));
 | 
						|
 | 
						|
    // Cache and preload the cssCode stylesheet.
 | 
						|
    this.cssCodeCache.addCSSCode(this.cssCodeHash, cssCode);
 | 
						|
  }
 | 
						|
 | 
						|
  addJSCode(jsCode) {
 | 
						|
    if (!jsCode) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (this.world === "MAIN") {
 | 
						|
      // To support the scripting.executeScript API, we would like to execute a
 | 
						|
      // string in the context of the web page in #injectIntoMainWorld().
 | 
						|
      // To do so without being blocked by the web page's CSP, we convert
 | 
						|
      // jsCode to a PrecompiledScript, which is then executed by the logic
 | 
						|
      // that is usually used for file-based execution.
 | 
						|
      const dataUrl = `data:text/javascript,${encodeURIComponent(jsCode)}`;
 | 
						|
      const options = {
 | 
						|
        hasReturnValue: this.matcher.wantReturnValue,
 | 
						|
        // Redact the file name to hide actual script content from web pages.
 | 
						|
        // TODO bug 1651557: Use dynamic name to improve debugger experience.
 | 
						|
        filename: "<anonymous code>",
 | 
						|
      };
 | 
						|
      // Note: this logic is similar to this.scriptCaches.get(...), but we are
 | 
						|
      // not using scriptCaches because we don't want the URL to be cached.
 | 
						|
      let promise = ChromeUtils.compileScript(dataUrl, options);
 | 
						|
      promise.then(script => {
 | 
						|
        promise.script = script;
 | 
						|
      });
 | 
						|
      this.jsCodeCompiledScript = promise;
 | 
						|
    } else {
 | 
						|
      // this.world === "ISOLATED".
 | 
						|
      this.jsCode = jsCode;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  compileScripts() {
 | 
						|
    return this.js.map(url => this.scriptCache.get(url));
 | 
						|
  }
 | 
						|
 | 
						|
  loadCSS() {
 | 
						|
    return this.css.map(url => this.cssCache.get(url));
 | 
						|
  }
 | 
						|
 | 
						|
  preload() {
 | 
						|
    this.loadCSS();
 | 
						|
    this.compileScripts();
 | 
						|
  }
 | 
						|
 | 
						|
  cleanup(window) {
 | 
						|
    if (this.requiresCleanup) {
 | 
						|
      if (window) {
 | 
						|
        let { windowUtils } = window;
 | 
						|
 | 
						|
        let type =
 | 
						|
          this.cssOrigin === "user"
 | 
						|
            ? windowUtils.USER_SHEET
 | 
						|
            : windowUtils.AUTHOR_SHEET;
 | 
						|
 | 
						|
        for (let url of this.css) {
 | 
						|
          this.cssCache.deleteDocument(url, window.document);
 | 
						|
 | 
						|
          if (!window.closed) {
 | 
						|
            runSafeSyncWithoutClone(
 | 
						|
              windowUtils.removeSheetUsingURIString,
 | 
						|
              url,
 | 
						|
              type
 | 
						|
            );
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        const { cssCodeHash } = this;
 | 
						|
 | 
						|
        if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
 | 
						|
          if (!window.closed) {
 | 
						|
            this.cssCodeCache.get(cssCodeHash).then(({ uri }) => {
 | 
						|
              runSafeSyncWithoutClone(windowUtils.removeSheet, uri, type);
 | 
						|
            });
 | 
						|
          }
 | 
						|
          this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      // Clear any sheets that were kept alive past their timeout as
 | 
						|
      // a result of living in this document.
 | 
						|
      this.cssCodeCache.clear(CSSCODE_EXPIRY_TIMEOUT_MS);
 | 
						|
      this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  matchesWindowGlobal(windowGlobal, ignorePermissions) {
 | 
						|
    return this.matcher.matchesWindowGlobal(windowGlobal, ignorePermissions);
 | 
						|
  }
 | 
						|
 | 
						|
  async injectInto(window, reportExceptions = true) {
 | 
						|
    if (
 | 
						|
      !lazy.isContentScriptProcess ||
 | 
						|
      this.injectedInto.has(window.document)
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.injectedInto.add(window.document);
 | 
						|
 | 
						|
    let context = this.extension.getContext(window);
 | 
						|
    for (let script of this.matcher.jsPaths) {
 | 
						|
      context.logActivity(this.scriptType, script, {
 | 
						|
        url: window.location.href,
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      // In case of initial about:blank documents, inject immediately without
 | 
						|
      // awaiting the runAt logic in the blocks below, to avoid getting stuck
 | 
						|
      // due to https://bugzilla.mozilla.org/show_bug.cgi?id=1900222#c7
 | 
						|
      // This is only relevant for dynamic code execution because declarative
 | 
						|
      // content scripts do not run on initial about:blank - bug 1415539).
 | 
						|
      if (!window.document.isInitialDocument) {
 | 
						|
        if (this.runAt === "document_end") {
 | 
						|
          await promiseDocumentReady(window.document);
 | 
						|
        } else if (this.runAt === "document_idle") {
 | 
						|
          await Promise.race([
 | 
						|
            promiseDocumentIdle(window),
 | 
						|
            promiseDocumentLoaded(window.document),
 | 
						|
          ]);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      return this.inject(context, reportExceptions);
 | 
						|
    } catch (e) {
 | 
						|
      return Promise.reject(context.normalizeError(e));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Tries to inject this script into the given window and sandbox, if
 | 
						|
   * there are pending operations for the window's current load state.
 | 
						|
   *
 | 
						|
   * @param {ContentScriptContextChild} context
 | 
						|
   *        The content script context into which to inject the scripts.
 | 
						|
   * @param {boolean} reportExceptions
 | 
						|
   *        Defaults to true and reports any exception directly to the console
 | 
						|
   *        and no exception will be thrown out of this function.
 | 
						|
   * @returns {Promise<any>}
 | 
						|
   *        Resolves to the last value in the evaluated script, when
 | 
						|
   *        execution is complete.
 | 
						|
   */
 | 
						|
  async inject(context, reportExceptions = true) {
 | 
						|
    // NOTE: Avoid unnecessary use of "await" in this function, because doing
 | 
						|
    // so can delay script execution beyond the scheduled point. In particular,
 | 
						|
    // document_start scripts should run "immediately" in most cases.
 | 
						|
 | 
						|
    DocumentManager.lazyInit();
 | 
						|
    if (this.requiresCleanup) {
 | 
						|
      context.addScript(this);
 | 
						|
    }
 | 
						|
 | 
						|
    const { cssCodeHash } = this;
 | 
						|
 | 
						|
    let cssPromise;
 | 
						|
    if (this.css.length || cssCodeHash) {
 | 
						|
      let window = context.contentWindow;
 | 
						|
      let { windowUtils } = window;
 | 
						|
 | 
						|
      let type =
 | 
						|
        this.cssOrigin === "user"
 | 
						|
          ? windowUtils.USER_SHEET
 | 
						|
          : windowUtils.AUTHOR_SHEET;
 | 
						|
 | 
						|
      if (this.removeCSS) {
 | 
						|
        for (let url of this.css) {
 | 
						|
          this.cssCache.deleteDocument(url, window.document);
 | 
						|
 | 
						|
          runSafeSyncWithoutClone(
 | 
						|
            windowUtils.removeSheetUsingURIString,
 | 
						|
            url,
 | 
						|
            type
 | 
						|
          );
 | 
						|
        }
 | 
						|
 | 
						|
        if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
 | 
						|
          const { uri } = await this.cssCodeCache.get(cssCodeHash);
 | 
						|
          this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
 | 
						|
 | 
						|
          runSafeSyncWithoutClone(windowUtils.removeSheet, uri, type);
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        cssPromise = Promise.all(this.loadCSS()).then(sheets => {
 | 
						|
          let window = context.contentWindow;
 | 
						|
          if (!window) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
 | 
						|
          for (let { url, sheet } of sheets) {
 | 
						|
            this.cssCache.addDocument(url, window.document);
 | 
						|
 | 
						|
            runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
 | 
						|
          }
 | 
						|
        });
 | 
						|
 | 
						|
        if (cssCodeHash) {
 | 
						|
          cssPromise = cssPromise.then(async () => {
 | 
						|
            const { sheet } = await this.cssCodeCache.get(cssCodeHash);
 | 
						|
            this.cssCodeCache.addDocument(cssCodeHash, window.document);
 | 
						|
 | 
						|
            runSafeSyncWithoutClone(windowUtils.addSheet, sheet, type);
 | 
						|
          });
 | 
						|
        }
 | 
						|
 | 
						|
        // We're loading stylesheets via the stylesheet service, which means
 | 
						|
        // that the normal mechanism for blocking layout and onload for pending
 | 
						|
        // stylesheets aren't in effect (since there's no document to block). So
 | 
						|
        // we need to do something custom here, similar to what we do for
 | 
						|
        // scripts. Blocking parsing is overkill, since we really just want to
 | 
						|
        // block layout and onload. But we have an API to do the former and not
 | 
						|
        // the latter, so we do it that way. This hopefully isn't a performance
 | 
						|
        // problem since there are no network loads involved, and since we cache
 | 
						|
        // the stylesheets on first load. We should fix this up if it does becomes
 | 
						|
        // a problem.
 | 
						|
        if (this.css.length) {
 | 
						|
          context.contentWindow.document.blockParsing(cssPromise, {
 | 
						|
            blockScriptCreated: false,
 | 
						|
          });
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    let scripts = this.getCompiledScripts(context);
 | 
						|
    if (scripts instanceof Promise) {
 | 
						|
      // Note: in theory, the following async await could result in script
 | 
						|
      // execution being scheduled too late. That would be an issue for
 | 
						|
      // document_start scripts. In practice, this is not a problem because the
 | 
						|
      // compiled script is cached in the process, and preloading to compile
 | 
						|
      // starts as soon as the network request for the document has been
 | 
						|
      // received (see ExtensionPolicyService::CheckRequest).
 | 
						|
      // getCompiledScripts() uses blockParsing() for document_start scripts to
 | 
						|
      // ensure that the DOM remains blocked when scripts are still compiling.
 | 
						|
      scripts = await scripts;
 | 
						|
    }
 | 
						|
 | 
						|
    if (cssPromise) {
 | 
						|
      // Make sure we've injected any related CSS before we run content scripts.
 | 
						|
      await cssPromise;
 | 
						|
    }
 | 
						|
 | 
						|
    const { extension } = context;
 | 
						|
 | 
						|
    // The evaluations below may throw, in which case the promise will be
 | 
						|
    // automatically rejected.
 | 
						|
    lazy.ExtensionTelemetry.contentScriptInjection.stopwatchStart(
 | 
						|
      extension,
 | 
						|
      context
 | 
						|
    );
 | 
						|
    try {
 | 
						|
      if (this.world === "MAIN") {
 | 
						|
        return this.#injectIntoMainWorld(context, scripts, reportExceptions);
 | 
						|
      }
 | 
						|
      return this.#injectIntoIsolatedWorld(context, scripts, reportExceptions);
 | 
						|
    } finally {
 | 
						|
      lazy.ExtensionTelemetry.contentScriptInjection.stopwatchFinish(
 | 
						|
        extension,
 | 
						|
        context
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #injectIntoIsolatedWorld(context, scripts, reportExceptions) {
 | 
						|
    let result;
 | 
						|
 | 
						|
    // Note: every script execution can potentially destroy the context, in
 | 
						|
    // which case context.cloneScope becomes null (bug 1403505).
 | 
						|
    for (let script of scripts) {
 | 
						|
      result = script.executeInGlobal(context.cloneScope, { reportExceptions });
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.jsCode) {
 | 
						|
      result = Cu.evalInSandbox(
 | 
						|
        this.jsCode,
 | 
						|
        context.cloneScope,
 | 
						|
        "latest",
 | 
						|
        // TODO bug 1651557: Use dynamic name to improve debugger experience.
 | 
						|
        "sandbox eval code",
 | 
						|
        1
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return result;
 | 
						|
  }
 | 
						|
 | 
						|
  #injectIntoMainWorld(context, scripts, reportExceptions) {
 | 
						|
    let result;
 | 
						|
 | 
						|
    // Note: every script execution can potentially destroy the context or
 | 
						|
    // navigate the window, in which case context.contentWindow will be null,
 | 
						|
    // which would cause an error to be thrown (bug 1403505).
 | 
						|
    for (let script of scripts) {
 | 
						|
      result = script.executeInGlobal(context.contentWindow, {
 | 
						|
        reportExceptions,
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    // Note: string-based code execution (=our implementation of func+args in
 | 
						|
    // scripting.executeScript) is not handled here, because we compile it in
 | 
						|
    // addJSCode() and include it in the scripts array via getCompiledScripts().
 | 
						|
    // We cannot use context.contentWindow.eval() here because the web page's
 | 
						|
    // CSP may block it.
 | 
						|
 | 
						|
    return result;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   *  Get the compiled scripts (if they are already precompiled and cached) or a promise which resolves
 | 
						|
   *  to the precompiled scripts (once they have been compiled and cached).
 | 
						|
   *
 | 
						|
   * @param {ContentScriptContextChild} context
 | 
						|
   *        The document to block the parsing on, if the scripts are not yet precompiled and cached.
 | 
						|
   *
 | 
						|
   * @returns {PrecompiledScript[] | Promise<PrecompiledScript[]>}
 | 
						|
   *          Returns an array of preloaded scripts if they are already available, or a promise which
 | 
						|
   *          resolves to the array of the preloaded scripts once they are precompiled and cached.
 | 
						|
   */
 | 
						|
  getCompiledScripts(context) {
 | 
						|
    let scriptPromises = this.compileScripts();
 | 
						|
    if (this.jsCodeCompiledScript) {
 | 
						|
      scriptPromises.push(this.jsCodeCompiledScript);
 | 
						|
    }
 | 
						|
    let scripts = scriptPromises.map(promise => promise.script);
 | 
						|
 | 
						|
    // If not all scripts are already available in the cache, block
 | 
						|
    // parsing and wait all promises to resolve.
 | 
						|
    if (!scripts.every(script => script)) {
 | 
						|
      let promise = Promise.all(scriptPromises);
 | 
						|
 | 
						|
      // If there is any syntax error, the script promises will be rejected.
 | 
						|
      //
 | 
						|
      // Notify the exception directly to the console so that it can
 | 
						|
      // be displayed in the web console by flagging the error with the right
 | 
						|
      // innerWindowID.
 | 
						|
      for (const p of scriptPromises) {
 | 
						|
        p.catch(error => {
 | 
						|
          Services.console.logMessage(
 | 
						|
            new ScriptError(
 | 
						|
              error.toString(),
 | 
						|
              error.fileName,
 | 
						|
              null,
 | 
						|
              error.lineNumber,
 | 
						|
              error.columnNumber,
 | 
						|
              Ci.nsIScriptError.errorFlag,
 | 
						|
              "content javascript",
 | 
						|
              context.innerWindowID
 | 
						|
            )
 | 
						|
          );
 | 
						|
        });
 | 
						|
      }
 | 
						|
 | 
						|
      // If we're supposed to inject at the start of the document load,
 | 
						|
      // and we haven't already missed that point, block further parsing
 | 
						|
      // until the scripts have been loaded.
 | 
						|
      const { document } = context.contentWindow;
 | 
						|
      if (
 | 
						|
        this.runAt === "document_start" &&
 | 
						|
        document.readyState !== "complete"
 | 
						|
      ) {
 | 
						|
        document.blockParsing(promise, { blockScriptCreated: false });
 | 
						|
      }
 | 
						|
 | 
						|
      return promise;
 | 
						|
    }
 | 
						|
 | 
						|
    return scripts;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// Represents a user script.
 | 
						|
class UserScript extends Script {
 | 
						|
  /**
 | 
						|
   * @param {ExtensionChild} extension
 | 
						|
   * @param {WebExtensionContentScript|object} matcher
 | 
						|
   *        An object with a "matchesWindowGlobal" method and content script
 | 
						|
   *        execution details.
 | 
						|
   */
 | 
						|
  constructor(extension, matcher) {
 | 
						|
    super(extension, matcher);
 | 
						|
    this.scriptType = "user_script";
 | 
						|
 | 
						|
    // This is an opaque object that the extension provides, it is associated to
 | 
						|
    // the particular userScript and it is passed as a parameter to the custom
 | 
						|
    // userScripts APIs defined by the extension.
 | 
						|
    this.scriptMetadata = matcher.userScriptOptions.scriptMetadata;
 | 
						|
    this.apiScriptURL =
 | 
						|
      extension.manifest.user_scripts &&
 | 
						|
      extension.manifest.user_scripts.api_script;
 | 
						|
 | 
						|
    // Add the apiScript to the js scripts to compile.
 | 
						|
    if (this.apiScriptURL) {
 | 
						|
      this.js = [this.apiScriptURL].concat(this.js);
 | 
						|
    }
 | 
						|
 | 
						|
    // WeakMap<ContentScriptContextChild, Sandbox>
 | 
						|
    this.sandboxes = new DefaultWeakMap(context => {
 | 
						|
      return this.createSandbox(context);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  async inject(context) {
 | 
						|
    DocumentManager.lazyInit();
 | 
						|
 | 
						|
    let scripts = this.getCompiledScripts(context);
 | 
						|
    if (scripts instanceof Promise) {
 | 
						|
      scripts = await scripts;
 | 
						|
    }
 | 
						|
 | 
						|
    let apiScript, sandboxScripts;
 | 
						|
 | 
						|
    if (this.apiScriptURL) {
 | 
						|
      [apiScript, ...sandboxScripts] = scripts;
 | 
						|
    } else {
 | 
						|
      sandboxScripts = scripts;
 | 
						|
    }
 | 
						|
 | 
						|
    // Load and execute the API script once per context.
 | 
						|
    if (apiScript) {
 | 
						|
      context.executeAPIScript(apiScript);
 | 
						|
    }
 | 
						|
 | 
						|
    let userScriptSandbox = this.sandboxes.get(context);
 | 
						|
 | 
						|
    context.callOnClose({
 | 
						|
      close: () => {
 | 
						|
        // Destroy the userScript sandbox when the related ContentScriptContextChild instance
 | 
						|
        // is being closed.
 | 
						|
        this.sandboxes.delete(context);
 | 
						|
        Cu.nukeSandbox(userScriptSandbox);
 | 
						|
      },
 | 
						|
    });
 | 
						|
 | 
						|
    // Notify listeners subscribed to the userScripts.onBeforeScript API event,
 | 
						|
    // to allow extension API script to provide its custom APIs to the userScript.
 | 
						|
    if (apiScript) {
 | 
						|
      context.userScriptsEvents.emit(
 | 
						|
        "on-before-script",
 | 
						|
        this.scriptMetadata,
 | 
						|
        userScriptSandbox
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    for (let script of sandboxScripts) {
 | 
						|
      script.executeInGlobal(userScriptSandbox);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  createSandbox(context) {
 | 
						|
    const { contentWindow } = context;
 | 
						|
    const contentPrincipal = contentWindow.document.nodePrincipal;
 | 
						|
    const ssm = Services.scriptSecurityManager;
 | 
						|
 | 
						|
    let principal;
 | 
						|
    if (contentPrincipal.isSystemPrincipal) {
 | 
						|
      principal = ssm.createNullPrincipal(contentPrincipal.originAttributes);
 | 
						|
    } else {
 | 
						|
      principal = [contentPrincipal];
 | 
						|
    }
 | 
						|
 | 
						|
    const sandbox = Cu.Sandbox(principal, {
 | 
						|
      sandboxName: `User Script registered by ${this.extension.policy.debugName}`,
 | 
						|
      sandboxPrototype: contentWindow,
 | 
						|
      sameZoneAs: contentWindow,
 | 
						|
      wantXrays: true,
 | 
						|
      wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
 | 
						|
      originAttributes: contentPrincipal.originAttributes,
 | 
						|
      metadata: {
 | 
						|
        "inner-window-id": context.innerWindowID,
 | 
						|
        addonId: this.extension.policy.id,
 | 
						|
      },
 | 
						|
    });
 | 
						|
 | 
						|
    return sandbox;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
var contentScripts = new DefaultWeakMap(matcher => {
 | 
						|
  const extension = lazy.ExtensionProcessScript.extensions.get(
 | 
						|
    matcher.extension
 | 
						|
  );
 | 
						|
 | 
						|
  if ("userScriptOptions" in matcher) {
 | 
						|
    return new UserScript(extension, matcher);
 | 
						|
  }
 | 
						|
 | 
						|
  return new Script(extension, matcher);
 | 
						|
});
 | 
						|
 | 
						|
/**
 | 
						|
 * An execution context for semi-privileged extension content scripts.
 | 
						|
 *
 | 
						|
 * This is the child side of the ContentScriptContextParent class
 | 
						|
 * defined in ExtensionParent.sys.mjs.
 | 
						|
 */
 | 
						|
class ContentScriptContextChild extends BaseContext {
 | 
						|
  constructor(extension, contentWindow) {
 | 
						|
    super("content_child", extension);
 | 
						|
 | 
						|
    this.setContentWindow(contentWindow);
 | 
						|
 | 
						|
    let frameId = lazy.WebNavigationFrames.getFrameId(contentWindow);
 | 
						|
    this.frameId = frameId;
 | 
						|
 | 
						|
    this.browsingContextId = contentWindow.docShell.browsingContext.id;
 | 
						|
 | 
						|
    this.scripts = [];
 | 
						|
 | 
						|
    let contentPrincipal = contentWindow.document.nodePrincipal;
 | 
						|
    let ssm = Services.scriptSecurityManager;
 | 
						|
 | 
						|
    // Copy origin attributes from the content window origin attributes to
 | 
						|
    // preserve the user context id.
 | 
						|
    let attrs = contentPrincipal.originAttributes;
 | 
						|
    let extensionPrincipal = ssm.createContentPrincipal(
 | 
						|
      this.extension.baseURI,
 | 
						|
      attrs
 | 
						|
    );
 | 
						|
 | 
						|
    this.isExtensionPage = contentPrincipal.equals(extensionPrincipal);
 | 
						|
 | 
						|
    if (this.isExtensionPage) {
 | 
						|
      // This is an iframe with content script API enabled and its principal
 | 
						|
      // should be the contentWindow itself. We create a sandbox with the
 | 
						|
      // contentWindow as principal and with X-rays disabled because it
 | 
						|
      // enables us to create the APIs object in this sandbox object and then
 | 
						|
      // copying it into the iframe's window.  See bug 1214658.
 | 
						|
      this.sandbox = Cu.Sandbox(contentWindow, {
 | 
						|
        sandboxName: `Web-Accessible Extension Page ${extension.policy.debugName}`,
 | 
						|
        sandboxPrototype: contentWindow,
 | 
						|
        sameZoneAs: contentWindow,
 | 
						|
        wantXrays: false,
 | 
						|
        isWebExtensionContentScript: true,
 | 
						|
      });
 | 
						|
    } else {
 | 
						|
      let principal;
 | 
						|
      if (contentPrincipal.isSystemPrincipal) {
 | 
						|
        // Make sure we don't hand out the system principal by accident.
 | 
						|
        // Also make sure that the null principal has the right origin attributes.
 | 
						|
        principal = ssm.createNullPrincipal(attrs);
 | 
						|
      } else {
 | 
						|
        principal = [contentPrincipal, extensionPrincipal];
 | 
						|
      }
 | 
						|
      // This metadata is required by the Developer Tools, in order for
 | 
						|
      // the content script to be associated with both the extension and
 | 
						|
      // the tab holding the content page.
 | 
						|
      let metadata = {
 | 
						|
        "inner-window-id": this.innerWindowID,
 | 
						|
        addonId: extensionPrincipal.addonId,
 | 
						|
      };
 | 
						|
 | 
						|
      let isMV2 = extension.manifestVersion == 2;
 | 
						|
      let wantGlobalProperties;
 | 
						|
      if (isMV2) {
 | 
						|
        // In MV2, fetch/XHR support cross-origin requests.
 | 
						|
        // WebSocket was also included to avoid CSP effects (bug 1676024).
 | 
						|
        wantGlobalProperties = ["XMLHttpRequest", "fetch", "WebSocket"];
 | 
						|
      } else {
 | 
						|
        // In MV3, fetch/XHR have the same capabilities as the web page.
 | 
						|
        wantGlobalProperties = [];
 | 
						|
      }
 | 
						|
      this.sandbox = Cu.Sandbox(principal, {
 | 
						|
        metadata,
 | 
						|
        sandboxName: `Content Script ${extension.policy.debugName}`,
 | 
						|
        sandboxPrototype: contentWindow,
 | 
						|
        sameZoneAs: contentWindow,
 | 
						|
        wantXrays: true,
 | 
						|
        isWebExtensionContentScript: true,
 | 
						|
        wantExportHelpers: true,
 | 
						|
        wantGlobalProperties,
 | 
						|
        originAttributes: attrs,
 | 
						|
      });
 | 
						|
 | 
						|
      // Preserve a copy of the original Error and Promise globals from the sandbox object,
 | 
						|
      // which are used in the WebExtensions internals (before any content script code had
 | 
						|
      // any chance to redefine them).
 | 
						|
      this.cloneScopePromise = this.sandbox.Promise;
 | 
						|
      this.cloneScopeError = this.sandbox.Error;
 | 
						|
 | 
						|
      if (isMV2) {
 | 
						|
        // Preserve a copy of the original window's XMLHttpRequest and fetch
 | 
						|
        // in a content object (fetch is manually binded to the window
 | 
						|
        // to prevent it from raising a TypeError because content object is not
 | 
						|
        // a real window).
 | 
						|
        Cu.evalInSandbox(
 | 
						|
          `
 | 
						|
          this.content = {
 | 
						|
            XMLHttpRequest: window.XMLHttpRequest,
 | 
						|
            fetch: window.fetch.bind(window),
 | 
						|
            WebSocket: window.WebSocket,
 | 
						|
          };
 | 
						|
 | 
						|
          window.JSON = JSON;
 | 
						|
          window.XMLHttpRequest = XMLHttpRequest;
 | 
						|
          window.fetch = fetch;
 | 
						|
          window.WebSocket = WebSocket;
 | 
						|
        `,
 | 
						|
          this.sandbox
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        // The sandbox's JSON API can deal with values from the sandbox and the
 | 
						|
        // contentWindow, but window.JSON cannot (and it could potentially be
 | 
						|
        // spoofed by the web page). jQuery.parseJSON relies on window.JSON.
 | 
						|
        Cu.evalInSandbox("window.JSON = JSON;", this.sandbox);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    Object.defineProperty(this, "principal", {
 | 
						|
      value: Cu.getObjectPrincipal(this.sandbox),
 | 
						|
      enumerable: true,
 | 
						|
      configurable: true,
 | 
						|
    });
 | 
						|
 | 
						|
    this.url = contentWindow.location.href;
 | 
						|
 | 
						|
    lazy.Schemas.exportLazyGetter(
 | 
						|
      this.sandbox,
 | 
						|
      "browser",
 | 
						|
      () => this.chromeObj
 | 
						|
    );
 | 
						|
    lazy.Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
 | 
						|
 | 
						|
    // Keep track if the userScript API script has been already executed in this context
 | 
						|
    // (e.g. because there are more then one UserScripts that match the related webpage
 | 
						|
    // and so the UserScript apiScript has already been executed).
 | 
						|
    this.hasUserScriptAPIs = false;
 | 
						|
 | 
						|
    // A lazy created EventEmitter related to userScripts-specific events.
 | 
						|
    defineLazyGetter(this, "userScriptsEvents", () => {
 | 
						|
      return new ExtensionCommon.EventEmitter();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  injectAPI() {
 | 
						|
    if (!this.isExtensionPage) {
 | 
						|
      throw new Error("Cannot inject extension API into non-extension window");
 | 
						|
    }
 | 
						|
 | 
						|
    // This is an iframe with content script API enabled (See Bug 1214658)
 | 
						|
    lazy.Schemas.exportLazyGetter(
 | 
						|
      this.contentWindow,
 | 
						|
      "browser",
 | 
						|
      () => this.chromeObj
 | 
						|
    );
 | 
						|
    lazy.Schemas.exportLazyGetter(
 | 
						|
      this.contentWindow,
 | 
						|
      "chrome",
 | 
						|
      () => this.chromeObj
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async logActivity(type, name, data) {
 | 
						|
    ExtensionActivityLogChild.log(this, type, name, data);
 | 
						|
  }
 | 
						|
 | 
						|
  get cloneScope() {
 | 
						|
    return this.sandbox;
 | 
						|
  }
 | 
						|
 | 
						|
  async executeAPIScript(apiScript) {
 | 
						|
    // Execute the UserScript apiScript only once per context (e.g. more then one UserScripts
 | 
						|
    // match the same webpage and the apiScript has already been executed).
 | 
						|
    if (apiScript && !this.hasUserScriptAPIs) {
 | 
						|
      this.hasUserScriptAPIs = true;
 | 
						|
      apiScript.executeInGlobal(this.cloneScope);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  addScript(script) {
 | 
						|
    if (script.requiresCleanup) {
 | 
						|
      this.scripts.push(script);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  close() {
 | 
						|
    super.unload();
 | 
						|
 | 
						|
    // Cleanup the scripts even if the contentWindow have been destroyed.
 | 
						|
    for (let script of this.scripts) {
 | 
						|
      script.cleanup(this.contentWindow);
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.contentWindow) {
 | 
						|
      // Overwrite the content script APIs with an empty object if the APIs objects are still
 | 
						|
      // defined in the content window (See Bug 1214658).
 | 
						|
      if (this.isExtensionPage) {
 | 
						|
        Cu.createObjectIn(this.contentWindow, { defineAs: "browser" });
 | 
						|
        Cu.createObjectIn(this.contentWindow, { defineAs: "chrome" });
 | 
						|
      }
 | 
						|
    }
 | 
						|
    Cu.nukeSandbox(this.sandbox);
 | 
						|
 | 
						|
    this.sandbox = null;
 | 
						|
  }
 | 
						|
 | 
						|
  get childManager() {
 | 
						|
    apiManager.lazyInit();
 | 
						|
    let can = new CanOfAPIs(this, apiManager, {});
 | 
						|
    let childManager = new ChildAPIManager(this, this.messageManager, can, {
 | 
						|
      envType: "content_parent",
 | 
						|
      url: this.url,
 | 
						|
    });
 | 
						|
    this.callOnClose(childManager);
 | 
						|
    return redefineGetter(this, "childManager", childManager);
 | 
						|
  }
 | 
						|
 | 
						|
  get chromeObj() {
 | 
						|
    let chromeObj = Cu.createObjectIn(this.sandbox);
 | 
						|
    this.childManager.inject(chromeObj);
 | 
						|
    return redefineGetter(this, "chromeObj", chromeObj);
 | 
						|
  }
 | 
						|
 | 
						|
  get messenger() {
 | 
						|
    return redefineGetter(this, "messenger", new Messenger(this));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// Responsible for creating ExtensionContexts and injecting content
 | 
						|
// scripts into them when new documents are created.
 | 
						|
DocumentManager = {
 | 
						|
  /** @type {Map<number, Map<ExtensionChild, ContentScriptContextChild>>} */
 | 
						|
  contexts: new Map(),
 | 
						|
 | 
						|
  initialized: false,
 | 
						|
 | 
						|
  lazyInit() {
 | 
						|
    if (this.initialized) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.initialized = true;
 | 
						|
 | 
						|
    Services.obs.addObserver(this, "inner-window-destroyed");
 | 
						|
    Services.obs.addObserver(this, "memory-pressure");
 | 
						|
  },
 | 
						|
 | 
						|
  uninit() {
 | 
						|
    Services.obs.removeObserver(this, "inner-window-destroyed");
 | 
						|
    Services.obs.removeObserver(this, "memory-pressure");
 | 
						|
  },
 | 
						|
 | 
						|
  observers: {
 | 
						|
    "inner-window-destroyed"(subject) {
 | 
						|
      let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
 | 
						|
 | 
						|
      // Close any existent content-script context for the destroyed window.
 | 
						|
      if (this.contexts.has(windowId)) {
 | 
						|
        let extensions = this.contexts.get(windowId);
 | 
						|
        for (let context of extensions.values()) {
 | 
						|
          context.close();
 | 
						|
        }
 | 
						|
 | 
						|
        this.contexts.delete(windowId);
 | 
						|
      }
 | 
						|
    },
 | 
						|
    "memory-pressure"(subject, topic, data) {
 | 
						|
      let timeout = data === "heap-minimize" ? 0 : undefined;
 | 
						|
 | 
						|
      for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(
 | 
						|
        scriptCaches
 | 
						|
      )) {
 | 
						|
        cache.clear(timeout);
 | 
						|
      }
 | 
						|
    },
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {object} subject
 | 
						|
   * @param {keyof typeof DocumentManager.observers} topic
 | 
						|
   * @param {any} data
 | 
						|
   */
 | 
						|
  observe(subject, topic, data) {
 | 
						|
    this.observers[topic].call(this, subject, topic, data);
 | 
						|
  },
 | 
						|
 | 
						|
  shutdownExtension(extension) {
 | 
						|
    for (let extensions of this.contexts.values()) {
 | 
						|
      let context = extensions.get(extension);
 | 
						|
      if (context) {
 | 
						|
        context.close();
 | 
						|
        extensions.delete(extension);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  getContexts(window) {
 | 
						|
    let winId = getInnerWindowID(window);
 | 
						|
 | 
						|
    let extensions = this.contexts.get(winId);
 | 
						|
    if (!extensions) {
 | 
						|
      extensions = new Map();
 | 
						|
      this.contexts.set(winId, extensions);
 | 
						|
    }
 | 
						|
 | 
						|
    return extensions;
 | 
						|
  },
 | 
						|
 | 
						|
  // For test use only.
 | 
						|
  getContext(extensionId, window) {
 | 
						|
    for (let [extension, context] of this.getContexts(window)) {
 | 
						|
      if (extension.id === extensionId) {
 | 
						|
        return context;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  getContentScriptGlobals(window) {
 | 
						|
    let extensions = this.contexts.get(getInnerWindowID(window));
 | 
						|
 | 
						|
    if (extensions) {
 | 
						|
      return Array.from(extensions.values(), ctx => ctx.sandbox);
 | 
						|
    }
 | 
						|
 | 
						|
    return [];
 | 
						|
  },
 | 
						|
 | 
						|
  initExtensionContext(extension, window) {
 | 
						|
    extension.getContext(window).injectAPI();
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
export var ExtensionContent = {
 | 
						|
  contentScripts,
 | 
						|
 | 
						|
  shutdownExtension(extension) {
 | 
						|
    DocumentManager.shutdownExtension(extension);
 | 
						|
  },
 | 
						|
 | 
						|
  // This helper is exported to be integrated in the devtools RDP actors,
 | 
						|
  // that can use it to retrieve the existent WebExtensions ContentScripts
 | 
						|
  // of a target window and be able to show the ContentScripts source in the
 | 
						|
  // DevTools Debugger panel.
 | 
						|
  getContentScriptGlobals(window) {
 | 
						|
    return DocumentManager.getContentScriptGlobals(window);
 | 
						|
  },
 | 
						|
 | 
						|
  initExtensionContext(extension, window) {
 | 
						|
    DocumentManager.initExtensionContext(extension, window);
 | 
						|
  },
 | 
						|
 | 
						|
  getContext(extension, window) {
 | 
						|
    let extensions = DocumentManager.getContexts(window);
 | 
						|
 | 
						|
    let context = extensions.get(extension);
 | 
						|
    if (!context) {
 | 
						|
      context = new ContentScriptContextChild(extension, window);
 | 
						|
      extensions.set(extension, context);
 | 
						|
    }
 | 
						|
    return context;
 | 
						|
  },
 | 
						|
 | 
						|
  // For test use only.
 | 
						|
  getContextByExtensionId(extensionId, window) {
 | 
						|
    return DocumentManager.getContext(extensionId, window);
 | 
						|
  },
 | 
						|
 | 
						|
  async handleDetectLanguage({ windows }) {
 | 
						|
    let wgc = WindowGlobalChild.getByInnerWindowId(windows[0]);
 | 
						|
    let doc = wgc.browsingContext.window.document;
 | 
						|
    await promiseDocumentReady(doc);
 | 
						|
 | 
						|
    // The CLD2 library can analyze HTML, but that uses more memory, and
 | 
						|
    // emscripten can't shrink its heap, so we use plain text instead.
 | 
						|
    let encoder = Cu.createDocumentEncoder("text/plain");
 | 
						|
    encoder.init(doc, "text/plain", Ci.nsIDocumentEncoder.SkipInvisibleContent);
 | 
						|
 | 
						|
    let result = await lazy.LanguageDetector.detectLanguage({
 | 
						|
      language:
 | 
						|
        doc.documentElement.getAttribute("xml:lang") ||
 | 
						|
        doc.documentElement.getAttribute("lang") ||
 | 
						|
        doc.contentLanguage ||
 | 
						|
        null,
 | 
						|
      tld: doc.location.hostname.match(/[a-z]*$/)[0],
 | 
						|
      text: encoder.encodeToStringWithMaxLength(60 * 1024),
 | 
						|
      encoding: doc.characterSet,
 | 
						|
    });
 | 
						|
    return result.language === "un" ? "und" : result.language;
 | 
						|
  },
 | 
						|
 | 
						|
  // Activate MV3 content scripts in all same-origin frames for this tab.
 | 
						|
  handleActivateScripts({ options, windows }) {
 | 
						|
    let policy = WebExtensionPolicy.getByID(options.id);
 | 
						|
 | 
						|
    // Order content scripts by run_at timing.
 | 
						|
    let runAt = { document_start: [], document_end: [], document_idle: [] };
 | 
						|
    for (let matcher of policy.contentScripts) {
 | 
						|
      runAt[matcher.runAt].push(this.contentScripts.get(matcher));
 | 
						|
    }
 | 
						|
 | 
						|
    // If we got here, checks in TabManagerBase.activateScripts assert:
 | 
						|
    // 1) this is a MV3 extension, with Origin Controls,
 | 
						|
    // 2) with a host permission (or content script) for the tab's top origin,
 | 
						|
    // 3) and that host permission hasn't been granted yet.
 | 
						|
 | 
						|
    // We treat the action click as implicit user's choice to activate the
 | 
						|
    // extension on the current site, so we can safely run (matching) content
 | 
						|
    // scripts in all sameOriginWithTop frames while ignoring host permission.
 | 
						|
 | 
						|
    let { browsingContext } = WindowGlobalChild.getByInnerWindowId(windows[0]);
 | 
						|
    for (let bc of browsingContext.getAllBrowsingContextsInSubtree()) {
 | 
						|
      let wgc = bc.currentWindowContext.windowGlobalChild;
 | 
						|
      if (wgc?.sameOriginWithTop) {
 | 
						|
        // This is TOCTOU safe: if a frame navigated after same-origin check,
 | 
						|
        // wgc.isClosed would be true and .matchesWindowGlobal() would fail.
 | 
						|
        const runScript = cs => {
 | 
						|
          if (cs.matchesWindowGlobal(wgc, /* ignorePermissions */ true)) {
 | 
						|
            return cs.injectInto(bc.window);
 | 
						|
          }
 | 
						|
        };
 | 
						|
 | 
						|
        // Inject all matching content scripts in proper run_at order.
 | 
						|
        Promise.all(runAt.document_start.map(runScript))
 | 
						|
          .then(() => Promise.all(runAt.document_end.map(runScript)))
 | 
						|
          .then(() => Promise.all(runAt.document_idle.map(runScript)));
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // Used to executeScript, insertCSS and removeCSS.
 | 
						|
  async handleActorExecute({ options, windows }) {
 | 
						|
    let policy = WebExtensionPolicy.getByID(options.extensionId);
 | 
						|
    // `WebExtensionContentScript` uses `MozDocumentMatcher::Matches` to ensure
 | 
						|
    // that a script can be run in a document. That requires either `frameId`
 | 
						|
    // or `allFrames` to be set. When `frameIds` (plural) is used, we force
 | 
						|
    // `allFrames` to be `true` in order to match any frame. This is OK because
 | 
						|
    // `executeInWin()` below looks up the window for the given `frameIds`
 | 
						|
    // immediately before `script.injectInto()`. Due to this, we won't run
 | 
						|
    // scripts in windows with non-matching `frameId`, despite `allFrames`
 | 
						|
    // being set to `true`.
 | 
						|
    if (options.frameIds) {
 | 
						|
      options.allFrames = true;
 | 
						|
    }
 | 
						|
    let matcher = new WebExtensionContentScript(policy, options);
 | 
						|
 | 
						|
    Object.assign(matcher, {
 | 
						|
      wantReturnValue: options.wantReturnValue,
 | 
						|
      removeCSS: options.removeCSS,
 | 
						|
      cssOrigin: options.cssOrigin,
 | 
						|
    });
 | 
						|
    let script = contentScripts.get(matcher);
 | 
						|
 | 
						|
    if (options.jsCode) {
 | 
						|
      script.addJSCode(options.jsCode);
 | 
						|
      delete options.jsCode;
 | 
						|
    }
 | 
						|
 | 
						|
    // Add the cssCode to the script, so that it can be converted into a cached URL.
 | 
						|
    await script.addCSSCode(options.cssCode);
 | 
						|
    delete options.cssCode;
 | 
						|
 | 
						|
    const executeInWin = innerId => {
 | 
						|
      let wg = WindowGlobalChild.getByInnerWindowId(innerId);
 | 
						|
      if (wg?.isCurrentGlobal && script.matchesWindowGlobal(wg)) {
 | 
						|
        let bc = wg.browsingContext;
 | 
						|
 | 
						|
        return {
 | 
						|
          frameId: bc.parent ? bc.id : 0,
 | 
						|
          // Disable exception reporting directly to the console
 | 
						|
          // in order to pass the exceptions back to the callsite.
 | 
						|
          promise: script.injectInto(bc.window, false),
 | 
						|
        };
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    let promisesWithFrameIds = windows.map(executeInWin).filter(obj => obj);
 | 
						|
 | 
						|
    let result = await Promise.all(
 | 
						|
      promisesWithFrameIds.map(async ({ frameId, promise }) => {
 | 
						|
        if (!options.returnResultsWithFrameIds) {
 | 
						|
          return promise;
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
          const result = await promise;
 | 
						|
 | 
						|
          return { frameId, result };
 | 
						|
        } catch (error) {
 | 
						|
          return { frameId, error };
 | 
						|
        }
 | 
						|
      })
 | 
						|
    ).catch(
 | 
						|
      // This is useful when we do not return results/errors with frame IDs in
 | 
						|
      // the promises above.
 | 
						|
      e => Promise.reject({ message: e.message })
 | 
						|
    );
 | 
						|
 | 
						|
    try {
 | 
						|
      // Check if the result can be structured-cloned before sending back.
 | 
						|
      return Cu.cloneInto(result, this);
 | 
						|
    } catch (e) {
 | 
						|
      let path = options.jsPaths.slice(-1)[0] ?? "<anonymous code>";
 | 
						|
      let message = `Script '${path}' result is non-structured-clonable data`;
 | 
						|
      return Promise.reject({ message, fileName: path });
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Child side of the ExtensionContent process actor, handles some tabs.* APIs.
 | 
						|
 */
 | 
						|
export class ExtensionContentChild extends JSProcessActorChild {
 | 
						|
  receiveMessage({ name, data }) {
 | 
						|
    if (!lazy.isContentScriptProcess) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    switch (name) {
 | 
						|
      case "DetectLanguage":
 | 
						|
        return ExtensionContent.handleDetectLanguage(data);
 | 
						|
      case "Execute":
 | 
						|
        return ExtensionContent.handleActorExecute(data);
 | 
						|
      case "ActivateScripts":
 | 
						|
        return ExtensionContent.handleActivateScripts(data);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |