forked from mirrors/gecko-dev
MozReview-Commit-ID: 91QNBYVQgQU --HG-- extra : rebase_source : c7c1507e823bbbbeb9c6b9fe26139b20a9c1dda8
927 lines
28 KiB
JavaScript
927 lines
28 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/. */
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["ExtensionContent"];
|
|
|
|
/* globals ExtensionContent */
|
|
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
LanguageDetector: "resource:///modules/translation/LanguageDetector.jsm",
|
|
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
|
|
Schemas: "resource://gre/modules/Schemas.jsm",
|
|
TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
|
|
WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
|
|
"@mozilla.org/content/style-sheet-service;1",
|
|
"nsIStyleSheetService");
|
|
|
|
const DocumentEncoder = Components.Constructor(
|
|
"@mozilla.org/layout/documentEncoder;1?type=text/plain",
|
|
"nsIDocumentEncoder", "init");
|
|
|
|
const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
|
|
|
|
ChromeUtils.import("resource://gre/modules/ExtensionChild.jsm");
|
|
ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
|
|
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
|
|
Cu.importGlobalProperties(["crypto", "TextEncoder"]);
|
|
|
|
const {
|
|
DefaultMap,
|
|
DefaultWeakMap,
|
|
defineLazyGetter,
|
|
getInnerWindowID,
|
|
getWinUtils,
|
|
promiseDocumentIdle,
|
|
promiseDocumentLoaded,
|
|
promiseDocumentReady,
|
|
runSafeSyncWithoutClone,
|
|
} = ExtensionUtils;
|
|
|
|
const {
|
|
BaseContext,
|
|
CanOfAPIs,
|
|
SchemaAPIManager,
|
|
} = ExtensionCommon;
|
|
|
|
const {
|
|
BrowserExtensionContent,
|
|
ChildAPIManager,
|
|
Messenger,
|
|
} = ExtensionChild;
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
|
|
|
|
|
|
var DocumentManager;
|
|
|
|
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
|
|
const CONTENT_SCRIPT_INJECTION_HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS";
|
|
|
|
var apiManager = new class extends SchemaAPIManager {
|
|
constructor() {
|
|
super("content", Schemas);
|
|
this.initialized = false;
|
|
}
|
|
|
|
lazyInit() {
|
|
if (!this.initialized) {
|
|
this.initialized = true;
|
|
this.initGlobal();
|
|
for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(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();
|
|
}
|
|
|
|
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, null, extension);
|
|
this.options = options;
|
|
}
|
|
|
|
defaultConstructor(url) {
|
|
let promise = ChromeUtils.compileScript(url, this.options);
|
|
promise.then(script => {
|
|
promise.script = script;
|
|
});
|
|
return promise;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
const uri = Services.io.newURI("data:text/css;charset=utf-8," + encodeURIComponent(cssCode));
|
|
const value = styleSheetService.preloadSheetAsync(uri, this.sheetType).then(sheet => {
|
|
return {sheet, uri};
|
|
});
|
|
|
|
super.set(hash, value);
|
|
}
|
|
}
|
|
|
|
defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", function() {
|
|
return new ScriptCache({hasReturnValue: false}, this);
|
|
});
|
|
|
|
defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", function() {
|
|
return new ScriptCache({hasReturnValue: true}, this);
|
|
});
|
|
|
|
defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", function() {
|
|
return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET, this);
|
|
});
|
|
|
|
defineLazyGetter(BrowserExtensionContent.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(BrowserExtensionContent.prototype, "userCSSCode", function() {
|
|
return new CSSCodeCache(Ci.nsIStyleSheetService.USER_SHEET, this);
|
|
});
|
|
|
|
defineLazyGetter(BrowserExtensionContent.prototype, "authorCSSCode", function() {
|
|
return new CSSCodeCache(Ci.nsIStyleSheetService.AUTHOR_SHEET, this);
|
|
});
|
|
|
|
// Represents a content script.
|
|
class Script {
|
|
constructor(extension, matcher) {
|
|
this.extension = extension;
|
|
this.matcher = matcher;
|
|
|
|
this.runAt = this.matcher.runAt;
|
|
this.js = this.matcher.jsPaths;
|
|
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"
|
|
];
|
|
this.scriptCache = extension[
|
|
matcher.wantReturnValue ? "dynamicScripts" : "staticScripts"
|
|
];
|
|
|
|
if (matcher.wantReturnValue) {
|
|
this.compileScripts();
|
|
this.loadCSS();
|
|
}
|
|
}
|
|
|
|
get requiresCleanup() {
|
|
return !this.removeCss && (this.css.length > 0 || 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);
|
|
}
|
|
|
|
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 winUtils = getWinUtils(window);
|
|
|
|
let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
|
|
|
|
for (let url of this.css) {
|
|
this.cssCache.deleteDocument(url, window.document);
|
|
runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
|
|
}
|
|
|
|
const {cssCodeHash} = this;
|
|
|
|
if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
|
|
this.cssCodeCache.get(cssCodeHash).then(({uri}) => {
|
|
runSafeSyncWithoutClone(winUtils.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);
|
|
}
|
|
}
|
|
|
|
matchesWindow(window) {
|
|
return this.matcher.matchesWindow(window);
|
|
}
|
|
|
|
async injectInto(window) {
|
|
let context = this.extension.getContext(window);
|
|
try {
|
|
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);
|
|
} 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 {BaseContext} context
|
|
* The content script context into which to inject the scripts.
|
|
* @returns {Promise<any>}
|
|
* Resolves to the last value in the evaluated script, when
|
|
* execution is complete.
|
|
*/
|
|
async inject(context) {
|
|
DocumentManager.lazyInit();
|
|
if (this.requiresCleanup) {
|
|
context.addScript(this);
|
|
}
|
|
|
|
const {cssCodeHash} = this;
|
|
|
|
let cssPromise;
|
|
if (this.css.length || cssCodeHash) {
|
|
let window = context.contentWindow;
|
|
let winUtils = getWinUtils(window);
|
|
|
|
let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
|
|
|
|
if (this.removeCSS) {
|
|
for (let url of this.css) {
|
|
this.cssCache.deleteDocument(url, window.document);
|
|
|
|
runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
|
|
}
|
|
|
|
if (cssCodeHash && this.cssCodeCache.has(cssCodeHash)) {
|
|
const {uri} = await this.cssCodeCache.get(cssCodeHash);
|
|
this.cssCodeCache.deleteDocument(cssCodeHash, window.document);
|
|
|
|
runSafeSyncWithoutClone(winUtils.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(winUtils.addSheet, sheet, type);
|
|
}
|
|
});
|
|
|
|
if (cssCodeHash) {
|
|
cssPromise = cssPromise.then(async () => {
|
|
const {sheet} = await this.cssCodeCache.get(cssCodeHash);
|
|
this.cssCodeCache.addDocument(cssCodeHash, window.document);
|
|
|
|
runSafeSyncWithoutClone(winUtils.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 > 0) {
|
|
context.contentWindow.document.blockParsing(cssPromise, {blockScriptCreated: false});
|
|
}
|
|
}
|
|
}
|
|
|
|
let scriptPromises = this.compileScripts();
|
|
|
|
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 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.
|
|
let {document} = context.contentWindow;
|
|
if (this.runAt === "document_start" && document.readyState !== "complete") {
|
|
document.blockParsing(promise, {blockScriptCreated: false});
|
|
}
|
|
|
|
scripts = await promise;
|
|
}
|
|
|
|
let result;
|
|
|
|
// The evaluations below may throw, in which case the promise will be
|
|
// automatically rejected.
|
|
TelemetryStopwatch.start(CONTENT_SCRIPT_INJECTION_HISTOGRAM, context);
|
|
try {
|
|
for (let script of scripts) {
|
|
result = script.executeInGlobal(context.cloneScope);
|
|
}
|
|
|
|
if (this.matcher.jsCode) {
|
|
result = Cu.evalInSandbox(this.matcher.jsCode, context.cloneScope, "latest");
|
|
}
|
|
} finally {
|
|
TelemetryStopwatch.finish(CONTENT_SCRIPT_INJECTION_HISTOGRAM, context);
|
|
}
|
|
|
|
await cssPromise;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
super("content_child", extension);
|
|
|
|
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.
|
|
let attrs = contentPrincipal.originAttributes;
|
|
let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs);
|
|
|
|
this.isExtensionPage = contentPrincipal.equals(extensionPrincipal);
|
|
|
|
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 if (this.isExtensionPage) {
|
|
principal = contentPrincipal;
|
|
} else {
|
|
principal = [contentPrincipal, 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 {
|
|
// 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,
|
|
};
|
|
|
|
this.sandbox = Cu.Sandbox(principal, {
|
|
metadata,
|
|
sandboxName: `Content Script ${extension.policy.debugName}`,
|
|
sandboxPrototype: contentWindow,
|
|
sameZoneAs: contentWindow,
|
|
wantXrays: true,
|
|
isWebExtensionContentScript: true,
|
|
wantExportHelpers: true,
|
|
wantGlobalProperties: ["XMLHttpRequest", "fetch"],
|
|
originAttributes: attrs,
|
|
});
|
|
|
|
// 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),
|
|
};
|
|
|
|
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);
|
|
|
|
this.childManager.inject(chromeObj);
|
|
return chromeObj;
|
|
});
|
|
|
|
Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
|
|
Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
|
|
}
|
|
|
|
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)
|
|
Schemas.exportLazyGetter(this.contentWindow,
|
|
"browser", () => this.chromeObj);
|
|
Schemas.exportLazyGetter(this.contentWindow,
|
|
"chrome", () => this.chromeObj);
|
|
}
|
|
|
|
get cloneScope() {
|
|
return this.sandbox;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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() {
|
|
apiManager.lazyInit();
|
|
|
|
let localApis = {};
|
|
let can = new CanOfAPIs(this, apiManager, localApis);
|
|
|
|
let childManager = new ChildAPIManager(this, this.messageManager, can, {
|
|
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 = {
|
|
// Map[windowId -> 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, topic, data) {
|
|
let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
|
|
|
MessageChannel.abortResponses({innerWindowID: windowId});
|
|
|
|
// 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);
|
|
}
|
|
},
|
|
},
|
|
|
|
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();
|
|
},
|
|
};
|
|
|
|
var ExtensionContent = {
|
|
BrowserExtensionContent,
|
|
Script,
|
|
|
|
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;
|
|
},
|
|
|
|
handleExtensionCapture(global, width, height, options) {
|
|
let win = 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(global, 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 = new DocumentEncoder(doc, "text/plain", Ci.nsIDocumentEncoder.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.
|
|
async handleExtensionExecute(global, target, options, script) {
|
|
let executeInWin = (window) => {
|
|
if (script.matchesWindow(window)) {
|
|
return script.injectInto(window);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
let promises;
|
|
try {
|
|
promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
|
|
.filter(promise => promise);
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
return Promise.reject({message: "An unexpected error occurred"});
|
|
}
|
|
|
|
if (!promises.length) {
|
|
if (options.frame_id) {
|
|
return Promise.reject({message: `Frame not found, or missing host permission`});
|
|
}
|
|
|
|
let frames = options.all_frames ? ", and any iframes" : "";
|
|
return Promise.reject({message: `Missing host permission for the tab${frames}`});
|
|
}
|
|
if (!options.all_frames && promises.length > 1) {
|
|
return Promise.reject({message: `Internal error: Script matched multiple windows`});
|
|
}
|
|
|
|
let result = await Promise.all(promises);
|
|
|
|
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) {
|
|
const {js} = options;
|
|
const fileName = js.length ? js[js.length - 1] : "<anonymous code>";
|
|
const message = `Script '${fileName}' result is non-structured-clonable data`;
|
|
return Promise.reject({message, fileName});
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
handleWebNavigationGetFrame(global, {frameId}) {
|
|
return WebNavigationFrames.getFrame(global.docShell, frameId);
|
|
},
|
|
|
|
handleWebNavigationGetAllFrames(global) {
|
|
return WebNavigationFrames.getAllFrames(global.docShell);
|
|
},
|
|
|
|
// Helpers
|
|
|
|
* enumerateWindows(docShell) {
|
|
let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
|
|
docShell.ENUMERATE_FORWARDS);
|
|
|
|
for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) {
|
|
try {
|
|
yield docShell.getInterface(Ci.nsIDOMWindow);
|
|
} catch (e) {
|
|
// This can fail if the docShell is being destroyed, so just
|
|
// ignore the error.
|
|
}
|
|
}
|
|
},
|
|
};
|