diff options
author | wolfbeast <mcwerewolf@wolfbeast.com> | 2019-03-06 12:30:20 +0100 |
---|---|---|
committer | wolfbeast <mcwerewolf@wolfbeast.com> | 2019-03-06 12:30:20 +0100 |
commit | e25ad543952b5afc13181aaebef9c5951fc27be0 (patch) | |
tree | 456a1359b3e5f810319edd423dd5cc5a1e22ada5 /toolkit/components/webextensions/ExtensionContent.jsm | |
parent | 74533a843277c3687c749989ed1522354d1054d2 (diff) | |
parent | 5dba1ebe8498286762873fff0016f35f3e14d2d5 (diff) | |
download | UXP-e25ad543952b5afc13181aaebef9c5951fc27be0.tar UXP-e25ad543952b5afc13181aaebef9c5951fc27be0.tar.gz UXP-e25ad543952b5afc13181aaebef9c5951fc27be0.tar.lz UXP-e25ad543952b5afc13181aaebef9c5951fc27be0.tar.xz UXP-e25ad543952b5afc13181aaebef9c5951fc27be0.zip |
Merge branch 'master' into Basilisk-releasev2019.03.08
Diffstat (limited to 'toolkit/components/webextensions/ExtensionContent.jsm')
-rw-r--r-- | toolkit/components/webextensions/ExtensionContent.jsm | 1050 |
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(); |