summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionContent.jsm
diff options
context:
space:
mode:
authorwolfbeast <mcwerewolf@wolfbeast.com>2019-03-06 12:30:20 +0100
committerwolfbeast <mcwerewolf@wolfbeast.com>2019-03-06 12:30:20 +0100
commite25ad543952b5afc13181aaebef9c5951fc27be0 (patch)
tree456a1359b3e5f810319edd423dd5cc5a1e22ada5 /toolkit/components/webextensions/ExtensionContent.jsm
parent74533a843277c3687c749989ed1522354d1054d2 (diff)
parent5dba1ebe8498286762873fff0016f35f3e14d2d5 (diff)
downloadUXP-2019.03.08.tar
UXP-2019.03.08.tar.gz
UXP-2019.03.08.tar.lz
UXP-2019.03.08.tar.xz
UXP-2019.03.08.zip
Merge branch 'master' into Basilisk-releasev2019.03.08
Diffstat (limited to 'toolkit/components/webextensions/ExtensionContent.jsm')
-rw-r--r--toolkit/components/webextensions/ExtensionContent.jsm1050
1 files changed, 0 insertions, 1050 deletions
diff --git a/toolkit/components/webextensions/ExtensionContent.jsm b/toolkit/components/webextensions/ExtensionContent.jsm
deleted file mode 100644
index 5f9b88f35..000000000
--- a/toolkit/components/webextensions/ExtensionContent.jsm
+++ /dev/null
@@ -1,1050 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["ExtensionContent"];
-
-/* globals ExtensionContent */
-
-/*
- * This file handles the content process side of extensions. It mainly
- * takes care of content script injection, content script APIs, and
- * messaging.
- *
- * This file is also the initial entry point for addon processes.
- * ExtensionChild.jsm is responsible for functionality specific to addon
- * processes.
- */
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/AppConstants.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
- "resource://gre/modules/ExtensionManagement.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
- "resource:///modules/translation/LanguageDetector.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
- "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
- "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
- "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
- "resource://gre/modules/PromiseUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
- "resource://gre/modules/Schemas.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
- "resource://gre/modules/WebNavigationFrames.jsm");
-
-Cu.import("resource://gre/modules/ExtensionChild.jsm");
-Cu.import("resource://gre/modules/ExtensionCommon.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
-const {
- EventEmitter,
- LocaleData,
- defineLazyGetter,
- flushJarCache,
- getInnerWindowID,
- promiseDocumentReady,
- runSafeSyncWithoutClone,
-} = ExtensionUtils;
-
-const {
- BaseContext,
- SchemaAPIManager,
-} = ExtensionCommon;
-
-const {
- ChildAPIManager,
- Messenger,
-} = ExtensionChild;
-
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
-
-const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
-
-function isWhenBeforeOrSame(when1, when2) {
- let table = {"document_start": 0,
- "document_end": 1,
- "document_idle": 2};
- return table[when1] <= table[when2];
-}
-
-var apiManager = new class extends SchemaAPIManager {
- constructor() {
- super("content");
- this.initialized = false;
- }
-
- generateAPIs(...args) {
- if (!this.initialized) {
- this.initialized = true;
- for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
- this.loadScript(value);
- }
- }
- return super.generateAPIs(...args);
- }
-
- registerSchemaAPI(namespace, envType, getAPI) {
- if (envType == "content_child") {
- super.registerSchemaAPI(namespace, envType, getAPI);
- }
- }
-}();
-
-// Represents a content script.
-function Script(extension, options, deferred = PromiseUtils.defer()) {
- this.extension = extension;
- this.options = options;
- this.run_at = this.options.run_at;
- this.js = this.options.js || [];
- this.css = this.options.css || [];
- this.remove_css = this.options.remove_css;
- this.match_about_blank = this.options.match_about_blank;
-
- this.deferred = deferred;
-
- this.matches_ = new MatchPattern(this.options.matches);
- this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
- // TODO: MatchPattern should pre-mangle host-only patterns so that we
- // don't need to call a separate match function.
- this.matches_host_ = new MatchPattern(this.options.matchesHost || null);
- this.include_globs_ = new MatchGlobs(this.options.include_globs);
- this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs);
-
- this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
-}
-
-Script.prototype = {
- get cssURLs() {
- // We can handle CSS urls (css) and CSS code (cssCode).
- let urls = [];
- for (let url of this.css) {
- urls.push(this.extension.baseURI.resolve(url));
- }
-
- if (this.options.cssCode) {
- let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
- urls.push(url);
- }
-
- return urls;
- },
-
- matches(window) {
- let uri = window.document.documentURIObject;
- let principal = window.document.nodePrincipal;
-
- // If mozAddonManager is present on this page, don't allow
- // content scripts.
- if (window.navigator.mozAddonManager !== undefined) {
- return false;
- }
-
- if (this.match_about_blank && ["about:blank", "about:srcdoc"].includes(uri.spec)) {
- // When matching about:blank/srcdoc documents, the checks below
- // need to be performed against the "owner" document's URI.
- uri = principal.URI;
- }
-
- // Documents from data: URIs also inherit the principal.
- if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
- if (!this.match_about_blank) {
- return false;
- }
- uri = principal.URI;
- }
-
- if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
- return false;
- }
-
- if (this.exclude_matches_.matches(uri)) {
- return false;
- }
-
- if (this.options.include_globs != null) {
- if (!this.include_globs_.matches(uri.spec)) {
- return false;
- }
- }
-
- if (this.exclude_globs_.matches(uri.spec)) {
- return false;
- }
-
- if (this.options.frame_id != null) {
- if (WebNavigationFrames.getFrameId(window) != this.options.frame_id) {
- return false;
- }
- } else if (!this.options.all_frames && window.top != window) {
- return false;
- }
-
- return true;
- },
-
- cleanup(window) {
- if (!this.remove_css) {
- let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
-
- for (let url of this.cssURLs) {
- runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
- }
- }
- },
-
- /**
- * Tries to inject this script into the given window and sandbox, if
- * there are pending operations for the window's current load state.
- *
- * @param {Window} window
- * The DOM Window to inject the scripts and CSS into.
- * @param {Sandbox} sandbox
- * A Sandbox inheriting from `window` in which to evaluate the
- * injected scripts.
- * @param {function} shouldRun
- * A function which, when passed the document load state that a
- * script is expected to run at, returns `true` if we should
- * currently be injecting scripts for that load state.
- *
- * For initial injection of a script, this function should
- * return true if the document is currently in or has already
- * passed through the given state. For injections triggered by
- * document state changes, it should only return true if the
- * given state exactly matches the state that triggered the
- * change.
- * @param {string} when
- * The document's current load state, or if triggered by a
- * document state change, the new document state that triggered
- * the injection.
- */
- tryInject(window, sandbox, shouldRun, when) {
- if (shouldRun("document_start")) {
- let {cssURLs} = this;
- if (cssURLs.length > 0) {
- let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
-
- let method = this.remove_css ? winUtils.removeSheetUsingURIString : winUtils.loadSheetUsingURIString;
- for (let url of cssURLs) {
- runSafeSyncWithoutClone(method, url, winUtils.AUTHOR_SHEET);
- }
-
- this.deferred.resolve();
- }
- }
-
- let result;
- let scheduled = this.run_at || "document_idle";
- if (shouldRun(scheduled)) {
- for (let [i, url] of this.js.entries()) {
- let options = {
- target: sandbox,
- charset: "UTF-8",
- // Inject the last script asynchronously unless we're expected to
- // inject before any page scripts have run, and we haven't already
- // missed that boat.
- async: (i === this.js.length - 1) &&
- (this.run_at !== "document_start" || when !== "document_start"),
- };
- try {
- result = Services.scriptloader.loadSubScriptWithOptions(url, options);
- } catch (e) {
- Cu.reportError(e);
- this.deferred.reject(e);
- }
- }
-
- if (this.options.jsCode) {
- try {
- result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
- } catch (e) {
- Cu.reportError(e);
- this.deferred.reject(e);
- }
- }
-
- this.deferred.resolve(result);
- }
- },
-};
-
-function getWindowMessageManager(contentWindow) {
- let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .QueryInterface(Ci.nsIInterfaceRequestor);
- try {
- return ir.getInterface(Ci.nsIContentFrameMessageManager);
- } catch (e) {
- // Some windows don't support this interface (hidden window).
- return null;
- }
-}
-
-var DocumentManager;
-var ExtensionManager;
-
-/**
- * An execution context for semi-privileged extension content scripts.
- *
- * This is the child side of the ContentScriptContextParent class
- * defined in ExtensionParent.jsm.
- */
-class ContentScriptContextChild extends BaseContext {
- constructor(extension, contentWindow, contextOptions = {}) {
- super("content_child", extension);
-
- let {isExtensionPage} = contextOptions;
-
- this.isExtensionPage = isExtensionPage;
-
- this.setContentWindow(contentWindow);
-
- let frameId = WebNavigationFrames.getFrameId(contentWindow);
- this.frameId = frameId;
-
- 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. overwrite the addonId.
- let attrs = contentPrincipal.originAttributes;
- attrs.addonId = this.extension.id;
- let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs);
-
- let principal;
- if (ssm.isSystemPrincipal(contentPrincipal)) {
- // 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];
- }
-
- if (isExtensionPage) {
- if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) {
- throw new Error("Invalid target window for this extension context");
- }
- // 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 for rationale)
- this.sandbox = Cu.Sandbox(contentWindow, {
- sandboxName: `Web-Accessible Extension Page ${this.extension.id}`,
- sandboxPrototype: contentWindow,
- sameZoneAs: contentWindow,
- wantXrays: false,
- isWebExtensionContentScript: true,
- });
- } else {
- // 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: attrs.addonId,
- };
-
- this.sandbox = Cu.Sandbox(principal, {
- metadata,
- sandboxName: `Content Script ${this.extension.id}`,
- sandboxPrototype: contentWindow,
- sameZoneAs: contentWindow,
- wantXrays: true,
- isWebExtensionContentScript: true,
- wantExportHelpers: true,
- wantGlobalProperties: ["XMLHttpRequest", "fetch"],
- originAttributes: attrs,
- });
-
- Cu.evalInSandbox(`
- window.JSON = JSON;
- window.XMLHttpRequest = XMLHttpRequest;
- window.fetch = fetch;
- `, this.sandbox);
- }
-
- Object.defineProperty(this, "principal", {
- value: Cu.getObjectPrincipal(this.sandbox),
- enumerable: true,
- configurable: true,
- });
-
- this.url = contentWindow.location.href;
-
- defineLazyGetter(this, "chromeObj", () => {
- let chromeObj = Cu.createObjectIn(this.sandbox);
-
- Schemas.inject(chromeObj, this.childManager);
- return chromeObj;
- });
-
- Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
- Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
-
- // This is an iframe with content script API enabled (bug 1214658)
- if (isExtensionPage) {
- Schemas.exportLazyGetter(this.contentWindow,
- "browser", () => this.chromeObj);
- Schemas.exportLazyGetter(this.contentWindow,
- "chrome", () => this.chromeObj);
- }
- }
-
- get cloneScope() {
- return this.sandbox;
- }
-
- execute(script, shouldRun, when) {
- script.tryInject(this.contentWindow, this.sandbox, shouldRun, when);
- }
-
- addScript(script, when) {
- let state = DocumentManager.getWindowState(this.contentWindow);
- this.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state), when);
-
- // Save the script in case it has pending operations in later load
- // states, but only if we're before document_idle, or require cleanup.
- if (state != "document_idle" || script.requiresCleanup) {
- this.scripts.push(script);
- }
- }
-
- triggerScripts(documentState) {
- for (let script of this.scripts) {
- this.execute(script, scheduled => scheduled == documentState, documentState);
- }
- if (documentState == "document_idle") {
- // Don't bother saving scripts after document_idle.
- this.scripts = this.scripts.filter(script => script.requiresCleanup);
- }
- }
-
- close() {
- super.unload();
-
- if (this.contentWindow) {
- for (let script of this.scripts) {
- if (script.requiresCleanup) {
- script.cleanup(this.contentWindow);
- }
- }
-
- // Overwrite the content script APIs with an empty object if the APIs objects are still
- // defined in the content window (bug 1214658).
- if (this.isExtensionPage) {
- Cu.createObjectIn(this.contentWindow, {defineAs: "browser"});
- Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
- }
- }
- Cu.nukeSandbox(this.sandbox);
- this.sandbox = null;
- }
-}
-
-defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
- // The |sender| parameter is passed directly to the extension.
- let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
- let filter = {extensionId: this.extension.id};
- let optionalFilter = {frameId: this.frameId};
-
- return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
-});
-
-defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() {
- let localApis = {};
- apiManager.generateAPIs(this, localApis);
-
- let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
- envType: "content_parent",
- url: this.url,
- });
-
- this.callOnClose(childManager);
-
- return childManager;
-});
-
-// Responsible for creating ExtensionContexts and injecting content
-// scripts into them when new documents are created.
-DocumentManager = {
- extensionCount: 0,
-
- // Map[windowId -> Map[extensionId -> ContentScriptContextChild]]
- contentScriptWindows: new Map(),
-
- // Map[windowId -> ContentScriptContextChild]
- extensionPageWindows: new Map(),
-
- init() {
- Services.obs.addObserver(this, "content-document-global-created", false);
- Services.obs.addObserver(this, "document-element-inserted", false);
- Services.obs.addObserver(this, "inner-window-destroyed", false);
- },
-
- uninit() {
- Services.obs.removeObserver(this, "content-document-global-created");
- Services.obs.removeObserver(this, "document-element-inserted");
- Services.obs.removeObserver(this, "inner-window-destroyed");
- },
-
- getWindowState(contentWindow) {
- let readyState = contentWindow.document.readyState;
- if (readyState == "complete") {
- return "document_idle";
- }
- if (readyState == "interactive") {
- return "document_end";
- }
- return "document_start";
- },
-
- loadInto(window) {
- // Enable the content script APIs should be available in subframes' window
- // if it is recognized as a valid addon id (see Bug 1214658 for rationale).
- const {
- NO_PRIVILEGES,
- CONTENTSCRIPT_PRIVILEGES,
- FULL_PRIVILEGES,
- } = ExtensionManagement.API_LEVELS;
- let extensionId = ExtensionManagement.getAddonIdForWindow(window);
- let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
-
- if (apiLevel != NO_PRIVILEGES) {
- let extension = ExtensionManager.get(extensionId);
- if (extension) {
- if (apiLevel == CONTENTSCRIPT_PRIVILEGES) {
- DocumentManager.getExtensionPageContext(extension, window);
- } else if (apiLevel == FULL_PRIVILEGES) {
- ExtensionChild.createExtensionContext(extension, window);
- }
- }
- }
- },
-
- observe: function(subject, topic, data) {
- // For some types of documents (about:blank), we only see the first
- // notification, for others (data: URIs) we only observe the second.
- if (topic == "content-document-global-created" || topic == "document-element-inserted") {
- let document = subject;
- let window = document && document.defaultView;
-
- if (topic == "content-document-global-created") {
- window = subject;
- document = window && window.document;
- }
-
- if (!document || !document.location || !window) {
- return;
- }
-
- // Make sure we only load into frames that ExtensionContent.init
- // was called on (i.e., not frames for social or sidebars).
- let mm = getWindowMessageManager(window);
- if (!mm || !ExtensionContent.globals.has(mm)) {
- return;
- }
-
- // Load on document-element-inserted, except for about:blank which doesn't
- // see it, and needs special late handling on DOMContentLoaded event.
- if (topic === "document-element-inserted") {
- this.loadInto(window);
- this.trigger("document_start", window);
- }
-
- /* eslint-disable mozilla/balanced-listeners */
- window.addEventListener("DOMContentLoaded", this, true);
- window.addEventListener("load", this, true);
- /* eslint-enable mozilla/balanced-listeners */
- } else if (topic == "inner-window-destroyed") {
- let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-
- MessageChannel.abortResponses({innerWindowID: windowId});
-
- // Close any existent content-script context for the destroyed window.
- if (this.contentScriptWindows.has(windowId)) {
- let extensions = this.contentScriptWindows.get(windowId);
- for (let [, context] of extensions) {
- context.close();
- }
-
- this.contentScriptWindows.delete(windowId);
- }
-
- // Close any existent iframe extension page context for the destroyed window.
- if (this.extensionPageWindows.has(windowId)) {
- let context = this.extensionPageWindows.get(windowId);
- context.close();
- this.extensionPageWindows.delete(windowId);
- }
-
- ExtensionChild.destroyExtensionContext(windowId);
- }
- },
-
- handleEvent: function(event) {
- let window = event.currentTarget;
- if (event.target != window.document) {
- // We use capturing listeners so we have precedence over content script
- // listeners, but only care about events targeted to the element we're
- // listening on.
- return;
- }
- window.removeEventListener(event.type, this, true);
-
- // Need to check if we're still on the right page? Greasemonkey does this.
-
- if (event.type == "DOMContentLoaded") {
- // By this time, we can be sure if this is an explicit about:blank
- // document, and if it needs special late loading and fake trigger.
- if (window.location.href === "about:blank") {
- this.loadInto(window);
- this.trigger("document_start", window);
- }
- this.trigger("document_end", window);
- } else if (event.type == "load") {
- this.trigger("document_idle", window);
- }
- },
-
- // Used to executeScript, insertCSS and removeCSS.
- executeScript(global, extensionId, options) {
- let extension = ExtensionManager.get(extensionId);
-
- let executeInWin = (window) => {
- let deferred = PromiseUtils.defer();
- let script = new Script(extension, options, deferred);
-
- if (script.matches(window)) {
- let context = this.getContentScriptContext(extension, window);
- context.addScript(script);
- return deferred.promise;
- }
- return null;
- };
-
- let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
- .filter(promise => promise);
-
- if (!promises.length) {
- let details = {};
- for (let key of ["all_frames", "frame_id", "matches_about_blank", "matchesHost"]) {
- if (key in options) {
- details[key] = options[key];
- }
- }
-
- return Promise.reject({message: `No window matching ${JSON.stringify(details)}`});
- }
- if (!options.all_frames && promises.length > 1) {
- return Promise.reject({message: `Internal error: Script matched multiple windows`});
- }
- return Promise.all(promises);
- },
-
- enumerateWindows: function* (docShell) {
- let window = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindow);
- yield window;
-
- for (let i = 0; i < docShell.childCount; i++) {
- let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
- yield* this.enumerateWindows(child);
- }
- },
-
- getContentScriptGlobalsForWindow(window) {
- let winId = getInnerWindowID(window);
- let extensions = this.contentScriptWindows.get(winId);
-
- if (extensions) {
- return Array.from(extensions.values(), ctx => ctx.sandbox);
- }
-
- return [];
- },
-
- getContentScriptContext(extension, window) {
- let winId = getInnerWindowID(window);
- if (!this.contentScriptWindows.has(winId)) {
- this.contentScriptWindows.set(winId, new Map());
- }
-
- let extensions = this.contentScriptWindows.get(winId);
- if (!extensions.has(extension.id)) {
- let context = new ContentScriptContextChild(extension, window);
- extensions.set(extension.id, context);
- }
-
- return extensions.get(extension.id);
- },
-
- getExtensionPageContext(extension, window) {
- let winId = getInnerWindowID(window);
-
- let context = this.extensionPageWindows.get(winId);
- if (!context) {
- let context = new ContentScriptContextChild(extension, window, {isExtensionPage: true});
- this.extensionPageWindows.set(winId, context);
- }
-
- return context;
- },
-
- startupExtension(extensionId) {
- if (this.extensionCount == 0) {
- this.init();
- }
- this.extensionCount++;
-
- let extension = ExtensionManager.get(extensionId);
- for (let global of ExtensionContent.globals.keys()) {
- // Note that we miss windows in the bfcache here. In theory we
- // could execute content scripts on a pageshow event for that
- // window, but that seems extreme.
- for (let window of this.enumerateWindows(global.docShell)) {
- for (let script of extension.scripts) {
- if (script.matches(window)) {
- let context = this.getContentScriptContext(extension, window);
- context.addScript(script);
- }
- }
- }
- }
- },
-
- shutdownExtension(extensionId) {
- // Clean up content-script contexts on extension shutdown.
- for (let [, extensions] of this.contentScriptWindows) {
- let context = extensions.get(extensionId);
- if (context) {
- context.close();
- extensions.delete(extensionId);
- }
- }
-
- // Clean up iframe extension page contexts on extension shutdown.
- for (let [winId, context] of this.extensionPageWindows) {
- if (context.extension.id == extensionId) {
- context.close();
- this.extensionPageWindows.delete(winId);
- }
- }
-
- ExtensionChild.shutdownExtension(extensionId);
-
- MessageChannel.abortResponses({extensionId});
-
- this.extensionCount--;
- if (this.extensionCount == 0) {
- this.uninit();
- }
- },
-
- trigger(when, window) {
- if (when === "document_start") {
- for (let extension of ExtensionManager.extensions.values()) {
- for (let script of extension.scripts) {
- if (script.matches(window)) {
- let context = this.getContentScriptContext(extension, window);
- context.addScript(script, when);
- }
- }
- }
- } else {
- let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map();
- for (let context of contexts.values()) {
- context.triggerScripts(when);
- }
- }
- },
-};
-
-// Represents a browser extension in the content process.
-class BrowserExtensionContent extends EventEmitter {
- constructor(data) {
- super();
-
- this.id = data.id;
- this.uuid = data.uuid;
- this.data = data;
- this.instanceId = data.instanceId;
-
- this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
- Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
-
- this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
- this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
- this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
- this.permissions = data.permissions;
- this.principal = data.principal;
-
- this.localeData = new LocaleData(data.localeData);
-
- this.manifest = data.manifest;
- this.baseURI = Services.io.newURI(data.baseURL, null, null);
-
- // Only used in addon processes.
- this.views = new Set();
-
- let uri = Services.io.newURI(data.resourceURL, null, null);
-
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- // Extension.jsm takes care of this in the parent.
- ExtensionManagement.startupExtension(this.uuid, uri, this);
- }
- }
-
- shutdown() {
- Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
-
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- ExtensionManagement.shutdownExtension(this.uuid);
- }
- }
-
- emit(event, ...args) {
- Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
-
- super.emit(event, ...args);
- }
-
- receiveMessage({name, data}) {
- if (name === this.MESSAGE_EMIT_EVENT) {
- super.emit(data.event, ...data.args);
- }
- }
-
- localizeMessage(...args) {
- return this.localeData.localizeMessage(...args);
- }
-
- localize(...args) {
- return this.localeData.localize(...args);
- }
-
- hasPermission(perm) {
- let match = /^manifest:(.*)/.exec(perm);
- if (match) {
- return this.manifest[match[1]] != null;
- }
- return this.permissions.has(perm);
- }
-}
-
-ExtensionManager = {
- // Map[extensionId, BrowserExtensionContent]
- extensions: new Map(),
-
- init() {
- Schemas.init();
- ExtensionChild.initOnce();
-
- Services.cpmm.addMessageListener("Extension:Startup", this);
- Services.cpmm.addMessageListener("Extension:Shutdown", this);
- Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
-
- if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
- let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
- for (let data of extensions) {
- this.extensions.set(data.id, new BrowserExtensionContent(data));
- DocumentManager.startupExtension(data.id);
- }
- }
- },
-
- get(extensionId) {
- return this.extensions.get(extensionId);
- },
-
- receiveMessage({name, data}) {
- let extension;
- switch (name) {
- case "Extension:Startup": {
- extension = new BrowserExtensionContent(data);
-
- this.extensions.set(data.id, extension);
-
- DocumentManager.startupExtension(data.id);
-
- Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
- break;
- }
-
- case "Extension:Shutdown": {
- extension = this.extensions.get(data.id);
- extension.shutdown();
-
- DocumentManager.shutdownExtension(data.id);
-
- this.extensions.delete(data.id);
- break;
- }
-
- case "Extension:FlushJarCache": {
- let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
- "initWithPath");
- let file = new nsIFile(data.path);
- flushJarCache(file);
- Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
- break;
- }
- }
- },
-};
-
-class ExtensionGlobal {
- constructor(global) {
- this.global = global;
-
- MessageChannel.addListener(global, "Extension:Capture", this);
- MessageChannel.addListener(global, "Extension:DetectLanguage", this);
- MessageChannel.addListener(global, "Extension:Execute", this);
- MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
- MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
-
- this.windowId = global.content
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils)
- .outerWindowID;
-
- global.sendAsyncMessage("Extension:TopWindowID", {windowId: this.windowId});
- }
-
- uninit() {
- this.global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId: this.windowId});
- }
-
- get messageFilterStrict() {
- return {
- innerWindowID: getInnerWindowID(this.global.content),
- };
- }
-
- receiveMessage({target, messageName, recipient, data}) {
- switch (messageName) {
- case "Extension:Capture":
- return this.handleExtensionCapture(data.width, data.height, data.options);
- case "Extension:DetectLanguage":
- return this.handleDetectLanguage(target);
- case "Extension:Execute":
- return this.handleExtensionExecute(target, recipient.extensionId, data.options);
- case "WebNavigation:GetFrame":
- return this.handleWebNavigationGetFrame(data.options);
- case "WebNavigation:GetAllFrames":
- return this.handleWebNavigationGetAllFrames();
- }
- }
-
- handleExtensionCapture(width, height, options) {
- let win = this.global.content;
-
- const XHTML_NS = "http://www.w3.org/1999/xhtml";
- let canvas = win.document.createElementNS(XHTML_NS, "canvas");
- canvas.width = width;
- canvas.height = height;
- canvas.mozOpaque = true;
-
- let ctx = canvas.getContext("2d");
-
- // We need to scale the image to the visible size of the browser,
- // in order for the result to appear as the user sees it when
- // settings like full zoom come into play.
- ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight);
-
- ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff");
-
- return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
- }
-
- handleDetectLanguage(target) {
- let doc = target.content.document;
-
- return promiseDocumentReady(doc).then(() => {
- let elem = doc.documentElement;
-
- let language = (elem.getAttribute("xml:lang") || elem.getAttribute("lang") ||
- doc.contentLanguage || null);
-
- // We only want the last element of the TLD here.
- // Only country codes have any effect on the results, but other
- // values cause no harm.
- let tld = doc.location.hostname.match(/[a-z]*$/)[0];
-
- // The CLD2 library used by the language detector is capable of
- // analyzing raw HTML. Unfortunately, that takes much more memory,
- // and since it's hosted by emscripten, and therefore can't shrink
- // its heap after it's grown, it has a performance cost.
- // So we send plain text instead.
- let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"].createInstance(Ci.nsIDocumentEncoder);
- encoder.init(doc, "text/plain", encoder.SkipInvisibleContent);
- let text = encoder.encodeToStringWithMaxLength(60 * 1024);
-
- let encoding = doc.characterSet;
-
- return LanguageDetector.detectLanguage({language, tld, text, encoding})
- .then(result => result.language === "un" ? "und" : result.language);
- });
- }
-
- // Used to executeScript, insertCSS and removeCSS.
- handleExtensionExecute(target, extensionId, options) {
- return DocumentManager.executeScript(target, extensionId, options).then(result => {
- try {
- // Make sure we can structured-clone the result value before
- // we try to send it back over the message manager.
- Cu.cloneInto(result, target);
- } catch (e) {
- return Promise.reject({message: "Script returned non-structured-clonable data"});
- }
- return result;
- });
- }
-
- handleWebNavigationGetFrame({frameId}) {
- return WebNavigationFrames.getFrame(this.global.docShell, frameId);
- }
-
- handleWebNavigationGetAllFrames() {
- return WebNavigationFrames.getAllFrames(this.global.docShell);
- }
-}
-
-this.ExtensionContent = {
- globals: new Map(),
-
- init(global) {
- this.globals.set(global, new ExtensionGlobal(global));
- ExtensionChild.init(global);
- },
-
- uninit(global) {
- ExtensionChild.uninit(global);
- this.globals.get(global).uninit();
- this.globals.delete(global);
- },
-
- // 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.
- getContentScriptGlobalsForWindow(window) {
- return DocumentManager.getContentScriptGlobalsForWindow(window);
- },
-};
-
-ExtensionManager.init();