--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -6,17 +6,16 @@
/* This content script contains code that requires a tab browser. */
/* eslint-env mozilla/frame-script */
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
@@ -1040,19 +1039,19 @@ var UserContextIdNotifier = {
let userContextId = loadContext.originAttributes.userContextId;
sendAsyncMessage("Browser:WindowCreated", { userContextId });
}
};
UserContextIdNotifier.init();
-ExtensionContent.init(this);
+Services.obs.notifyObservers(this, "tab-content-frameloader-created", "");
+
addEventListener("unload", () => {
- ExtensionContent.uninit(this);
RefreshBlocker.uninit();
});
addMessageListener("AllowScriptsToClose", () => {
content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.allowScriptsToClose();
});
--- a/devtools/server/actors/tab.js
+++ b/devtools/server/actors/tab.js
@@ -331,17 +331,17 @@ TabActor.prototype = {
/**
* Getter for the WebExtensions ContentScript globals related to the
* current tab content's DOM window.
*/
get webextensionsContentScriptGlobals() {
// Ignore xpcshell runtime which spawn TabActors without a window.
if (this.window) {
- return ExtensionContent.getContentScriptGlobalsForWindow(this.window);
+ return ExtensionContent.getContentScriptGlobals(this.window);
}
return [];
},
/**
* Getter for the list of all content DOM windows in this tabActor
* @return {Array}
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -62,17 +62,16 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "require",
"resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
Cu.import("resource://gre/modules/ExtensionParent.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -1094,24 +1094,16 @@ ExtensionChild = {
Port,
// Map<nsIContentFrameMessageManager, ContentGlobal>
contentGlobals: new Map(),
// Map<innerWindowId, ExtensionPageContextChild>
extensionContexts: new Map(),
- initOnce() {
- // This initializes the default message handler for messages targeted at
- // an addon process, in case the addon process receives a message before
- // its Messenger has been instantiated. For example, if a content script
- // sends a message while there is no background page.
- MessageChannel.setupMessageManagers([Services.cpmm]);
- },
-
init(global) {
if (!ExtensionManagement.isExtensionProcess) {
throw new Error("Cannot init extension page global in current process");
}
this.contentGlobals.set(global, new ContentGlobal(global));
},
@@ -1122,17 +1114,17 @@ ExtensionChild = {
/**
* Create a privileged context at document-element-inserted.
*
* @param {BrowserExtensionContent} extension
* The extension for which the context should be created.
* @param {nsIDOMWindow} contentWindow The global of the page.
*/
- createExtensionContext(extension, contentWindow) {
+ initExtensionContext(extension, contentWindow) {
if (!ExtensionManagement.isExtensionProcess) {
throw new Error("Cannot create an extension page context in current process");
}
let windowId = getInnerWindowID(contentWindow);
let context = this.extensionContexts.get(windowId);
if (context) {
if (context.extension !== extension) {
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -3,71 +3,57 @@
* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
-
+Cu.import("resource://gre/modules/Services.jsm");
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",
+XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
"resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
"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");
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");
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
DefaultMap,
DefaultWeakMap,
EventEmitter,
LocaleData,
defineLazyGetter,
- flushJarCache,
getInnerWindowID,
getWinUtils,
+ promiseDocumentLoaded,
promiseDocumentReady,
runSafeSyncWithoutClone,
} = ExtensionUtils;
const {
BaseContext,
CanOfAPIs,
SchemaAPIManager,
@@ -77,23 +63,16 @@ 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;
}
lazyInit() {
if (!this.initialized) {
@@ -194,314 +173,204 @@ class CSSCache extends CacheMap {
}
super.delete(url);
}
}
// Represents a content script.
class Script {
- constructor(extension, options, deferred = PromiseUtils.defer()) {
+ constructor(extension, options) {
this.extension = extension;
this.options = options;
- this.run_at = this.options.run_at;
+
+ this.runAt = 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.css_origin = this.options.css_origin;
- this.deferred = deferred;
-
this.cssCache = extension[this.css_origin === "user" ? "userCSS"
: "authorCSS"];
this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
: "staticScripts"];
if (options.wantReturnValue) {
this.compileScripts();
this.loadCSS();
}
- 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);
}
compileScripts() {
return this.js.map(url => this.scriptCache.get(url));
}
loadCSS() {
return this.cssURLs.map(url => this.cssCache.get(url));
}
- matchesLoadInfo(uri, loadInfo) {
- if (!this.matchesURI(uri)) {
- return false;
- }
-
- if (!this.options.all_frames && !loadInfo.isTopLevelLoad) {
- return false;
- }
-
- return true;
- }
-
- matchesURI(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;
- }
-
- return true;
- }
-
- 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) {
- // When matching top-level about:blank documents,
- // allow loading into any with a NullPrincipal.
- if (uri.spec === "about:blank" && window === window.top && principal.isNullPrincipal) {
- return true;
- }
-
- // When matching about:blank/srcdoc iframes, the checks below
- // need to be performed against the "owner" document's URI.
- if (["about:blank", "about:srcdoc"].includes(uri.spec)) {
- 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.matchesURI(uri)) {
- 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 && this.cssURLs.length) {
let winUtils = getWinUtils(window);
let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
for (let url of this.cssURLs) {
this.cssCache.deleteDocument(url, window.document);
runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
}
// Clear any sheets that were kept alive past their timeout as
// a result of living in this document.
this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
}
}
+ async injectInto(window) {
+ let context = this.extension.getContext(window);
+
+ if (this.runAt === "document_end") {
+ await promiseDocumentReady(window.document);
+ } else if (this.runAt === "document_idle") {
+ await promiseDocumentLoaded(window.document);
+ }
+
+ return this.inject(context);
+ }
+
/**
* 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.
+ * @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.
*/
- tryInject(window, sandbox, shouldRun, when) {
- if (this.cssURLs.length && shouldRun("document_start")) {
+ async inject(context) {
+ if (this.requiresCleanup) {
+ context.addScript(this);
+ }
+
+ let cssPromise;
+ if (this.cssURLs.length) {
+ let window = context.contentWindow;
let winUtils = getWinUtils(window);
- let innerWindowID = winUtils.currentInnerWindowID;
-
let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
if (this.remove_css) {
for (let url of this.cssURLs) {
this.cssCache.deleteDocument(url, window.document);
runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
}
+ } else {
+ cssPromise = Promise.all(this.loadCSS()).then(sheets => {
+ let window = context.contentWindow;
+ if (!window) {
+ return;
+ }
- this.deferred.resolve();
- } else {
- this.deferred.resolve(
- Promise.all(this.loadCSS()).then(sheets => {
- if (winUtils.currentInnerWindowID !== innerWindowID) {
- return;
- }
+ for (let {url, sheet} of sheets) {
+ this.cssCache.addDocument(url, window.document);
- for (let {url, sheet} of sheets) {
- this.cssCache.addDocument(url, window.document);
-
- runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
- }
- }));
+ runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
+ }
+ });
}
}
- let scheduled = this.run_at || "document_idle";
- if (shouldRun(scheduled)) {
- let scriptsPromise = Promise.all(this.compileScripts());
+ let scriptsPromise = Promise.all(this.compileScripts());
- // 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.
- if (this.run_at === "document_start" && when === "document_start") {
- window.document.blockParsing(scriptsPromise);
- }
+ // 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(scriptsPromise);
+ }
- this.deferred.resolve(scriptsPromise.then(scripts => {
- let result;
+ let scripts = await scriptsPromise;
+ let result;
- // The evaluations below may throw, in which case the promise will be
- // automatically rejected.
- for (let script of scripts) {
- result = script.executeInGlobal(sandbox);
- }
+ // The evaluations below may throw, in which case the promise will be
+ // automatically rejected.
+ for (let script of scripts) {
+ result = script.executeInGlobal(context.cloneScope);
+ }
- if (this.options.jsCode) {
- result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
- }
+ if (this.options.jsCode) {
+ result = Cu.evalInSandbox(this.options.jsCode, context.cloneScope, "latest");
+ }
- return result;
- }));
- }
+ await cssPromise;
+ return result;
}
}
defineLazyGetter(Script.prototype, "cssURLs", function() {
// We can handle CSS urls (css) and CSS code (cssCode).
let urls = this.css.slice();
if (this.options.cssCode) {
urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode));
}
return urls;
});
-
-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 = {}) {
+ constructor(extension, contentWindow) {
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.
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 (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)
+ 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, {
sandboxPrototype: contentWindow,
sameZoneAs: contentWindow,
wantXrays: false,
isWebExtensionContentScript: true,
});
} else {
// This metadata is required by the Developer Tools, in order for
@@ -542,63 +411,46 @@ class ContentScriptContextChild extends
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);
+ }
+
+ injectAPI() {
+ if (!this.isExtensionPage) {
+ throw new Error("Cannot inject extension API into non-extension window");
+ }
// 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);
- }
+ 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) {
+ addScript(script) {
+ if (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);
- }
+ 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"});
}
@@ -628,356 +480,129 @@ defineLazyGetter(ContentScriptContextChi
url: this.url,
});
this.callOnClose(childManager);
return childManager;
});
+// For test use only.
+var ExtensionManager = {
+ extensions: new Map(),
+};
+
// Responsible for creating ExtensionContexts and injecting content
// scripts into them when new documents are created.
DocumentManager = {
- extensionCount: 0,
+ // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
+ contexts: new Map(),
- // Map[windowId -> Map[extensionId -> ContentScriptContextChild]]
- contentScriptWindows: new Map(),
+ initialized: false,
- // Map[windowId -> ContentScriptContextChild]
- extensionPageWindows: new Map(),
+ lazyInit() {
+ if (this.initalized) {
+ return;
+ }
+ this.initialized = true;
- init() {
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- Services.obs.addObserver(this, "http-on-opening-request", false);
- }
- 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);
Services.obs.addObserver(this, "memory-pressure", false);
},
uninit() {
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- Services.obs.removeObserver(this, "http-on-opening-request");
- }
- Services.obs.removeObserver(this, "content-document-global-created");
- Services.obs.removeObserver(this, "document-element-inserted");
Services.obs.removeObserver(this, "inner-window-destroyed");
Services.obs.removeObserver(this, "memory-pressure");
},
- 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);
- }
- }
- }
- },
-
observers: {
- // For some types of documents (about:blank), we only see the first
- // notification, for others (data: URIs) we only observe the second.
- "content-document-global-created"(subject, topic, data) {
- this.observers["document-element-inserted"].call(this, subject.document, topic, data);
- },
- "document-element-inserted"(subject, topic, data) {
- let document = subject;
- let window = document && document.defaultView;
-
- 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 */
- },
"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.contentScriptWindows.has(windowId)) {
- let extensions = this.contentScriptWindows.get(windowId);
- for (let [, context] of extensions) {
+ if (this.contexts.has(windowId)) {
+ let extensions = this.contexts.get(windowId);
+ for (let context of extensions.values()) {
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);
+ this.contexts.delete(windowId);
}
ExtensionChild.destroyExtensionContext(windowId);
},
- "http-on-opening-request"(subject, topic, data) {
- let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
- if (loadInfo) {
- let {externalContentPolicyType: type} = loadInfo;
- if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
- type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
- this.preloadScripts(subject.URI, loadInfo);
- }
- }
- },
"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);
},
- handleEvent(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);
+ shutdownExtension(extension) {
+ for (let extensions of this.contexts.values()) {
+ let context = extensions.get(extension);
+ if (context) {
+ context.close();
+ extensions.delete(extension);
}
- 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;
- };
+ getContexts(window) {
+ let winId = getInnerWindowID(window);
- let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
- .filter(promise => promise);
-
- if (!promises.length) {
- if (options.frame_id) {
- return Promise.reject({message: `Frame not found, or missing host permission`});
- }
+ let extensions = this.contexts.get(winId);
+ if (!extensions) {
+ extensions = new Map();
+ this.contexts.set(winId, extensions);
+ }
- 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`});
- }
- return Promise.all(promises);
+ return extensions;
},
- 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);
+ // For test use only.
+ getContext(extensionId, window) {
+ for (let [extension, context] of this.getContexts(window)) {
+ if (extension.id === extensionId) {
+ return context;
+ }
}
},
- getContentScriptGlobalsForWindow(window) {
- let winId = getInnerWindowID(window);
- let extensions = this.contentScriptWindows.get(winId);
+ getContentScriptGlobals(window) {
+ let extensions = this.contexts.get(getInnerWindowID(window));
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();
- }
- },
-
- preloadScripts(uri, loadInfo) {
- for (let extension of ExtensionManager.extensions.values()) {
- for (let script of extension.scripts) {
- if (script.matchesLoadInfo(uri, loadInfo)) {
- script.loadCSS();
- script.compileScripts();
- }
- }
- }
- },
-
- 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);
- }
- }
+ initExtensionContext(extension, window) {
+ extension.getContext(window).injectAPI();
},
};
// Represents a browser extension in the content process.
class BrowserExtensionContent extends EventEmitter {
constructor(data) {
super();
+ this.data = data;
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);
defineLazyGetter(this, "scripts", () => {
return data.content_scripts.map(scriptData => new Script(this, scriptData));
});
@@ -994,23 +619,16 @@ class BrowserExtensionContent extends Ev
this.baseURI = Services.io.newURI(data.baseURL);
// Only used in addon processes.
this.views = new Set();
// Only used for devtools views.
this.devtoolsViews = new Set();
- let uri = Services.io.newURI(data.resourceURL);
-
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- // Extension.jsm takes care of this in the parent.
- ExtensionManagement.startupExtension(this.uuid, uri, this);
- }
-
/* eslint-disable mozilla/balanced-listeners */
this.on("add-permissions", (ignoreEvent, permissions) => {
if (permissions.permissions.length > 0) {
for (let perm of permissions.permissions) {
this.permissions.add(perm);
}
}
@@ -1028,24 +646,36 @@ class BrowserExtensionContent extends Ev
if (permissions.origins.length > 0) {
for (let origin of permissions.origins) {
this.whiteListedHosts.removeOne(origin);
}
}
});
/* eslint-enable mozilla/balanced-listeners */
+
+ ExtensionManager.extensions.set(this.id, this);
+ DocumentManager.lazyInit();
}
shutdown() {
+ ExtensionManager.extensions.delete(this.id);
+ DocumentManager.shutdownExtension(this);
Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+ }
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- ExtensionManagement.shutdownExtension(this.uuid);
+ getContext(window) {
+ let extensions = DocumentManager.getContexts(window);
+
+ let context = extensions.get(this);
+ if (!context) {
+ context = new ContentScriptContextChild(this, window);
+ extensions.set(this, context);
}
+ return context;
}
emit(event, ...args) {
Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
super.emit(event, ...args);
}
@@ -1083,115 +713,34 @@ defineLazyGetter(BrowserExtensionContent
defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", () => {
return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET);
});
defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
});
-ExtensionManager = {
- // Map[extensionId, BrowserExtensionContent]
- extensions: new Map(),
-
- init() {
- ExtensionChild.initOnce();
-
- Services.cpmm.addMessageListener("Extension:Startup", this);
- Services.cpmm.addMessageListener("Extension:Shutdown", this);
- Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
+this.ExtensionContent = {
+ BrowserExtensionContent,
+ Script,
- 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);
+ // 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);
},
- 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": {
- flushJarCache(data.path);
- Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
- break;
- }
- }
+ initExtensionContext(extension, window) {
+ DocumentManager.initExtensionContext(extension, window);
},
-};
-
-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 = getWinUtils(global.content).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;
+ 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");
@@ -1199,19 +748,19 @@ class ExtensionGlobal {
// 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) {
+ 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);
@@ -1220,73 +769,77 @@ class ExtensionGlobal {
// 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 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.
- 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) {
- 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});
+ async handleExtensionExecute(global, target, options, script) {
+ let executeInWin = (window) => {
+ if (script.matchesWindow(window)) {
+ return script.injectInto(window);
}
- return result;
- });
- }
+ return null;
+ };
+
+ let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
+ .filter(promise => promise);
+
+ 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);
- handleWebNavigationGetFrame({frameId}) {
- return WebNavigationFrames.getFrame(this.global.docShell, frameId);
- }
+ 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;
+ },
- handleWebNavigationGetAllFrames() {
- return WebNavigationFrames.getAllFrames(this.global.docShell);
- }
-}
+ handleWebNavigationGetFrame(global, {frameId}) {
+ return WebNavigationFrames.getFrame(global.docShell, frameId);
+ },
+
+ handleWebNavigationGetAllFrames(global) {
+ return WebNavigationFrames.getAllFrames(global.docShell);
+ },
-this.ExtensionContent = {
- globals: new Map(),
+ // Helpers
- init(global) {
- this.globals.set(global, new ExtensionGlobal(global));
- if (ExtensionManagement.isExtensionProcess) {
- ExtensionChild.init(global);
+ * enumerateWindows(docShell) {
+ let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
+ docShell.ENUMERATE_FORWARDS);
+
+ for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) {
+ yield docShell.getInterface(Ci.nsIDOMWindow);
}
},
-
- uninit(global) {
- if (ExtensionManagement.isExtensionProcess) {
- 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();
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -6,32 +6,37 @@
this.EXPORTED_SYMBOLS = ["ExtensionManagement"];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
-Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => {
let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {});
return UUIDMap;
});
+const {appinfo} = Services;
+const isParentProcess = appinfo.processType === appinfo.PROCESS_TYPE_DEFAULT;
+if (isParentProcess) {
+ Services.ppmm.loadProcessScript("chrome://extensions/content/extension-process-script.js", true);
+}
+
var ExtensionManagement;
/*
* This file should be kept short and simple since it's loaded even
* when no extensions are running.
*/
// Keep track of frame IDs for content windows. Mostly we can just use
@@ -324,19 +329,19 @@ Services.obs.addObserver(onCacheInvalida
ExtensionManagement = {
get cacheInvalidated() {
return cacheInvalidated;
},
get isExtensionProcess() {
if (this.useRemoteWebExtensions) {
- return Services.appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE;
+ return appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE;
}
- return Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
+ return isParentProcess;
},
startupExtension: Service.startupExtension.bind(Service),
shutdownExtension: Service.shutdownExtension.bind(Service),
registerAPI: APIs.register.bind(APIs),
unregisterAPI: APIs.unregister.bind(APIs),
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -260,21 +260,19 @@ GlobalManager = {
if (this.extensionMap.size == 0 && this.initialized) {
apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
this.initialized = false;
}
},
_onExtensionBrowser(type, browser, additionalData = {}) {
browser.messageManager.loadFrameScript(`data:,
- Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
- ExtensionContent.init(this);
- addEventListener("unload", function() {
- ExtensionContent.uninit(this);
- });
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ Services.obs.notifyObservers(this, "tab-content-frameloader-created", "");
`, false);
let viewType = browser.getAttribute("webextension-view-type");
if (viewType) {
let data = {viewType};
let {tabTracker} = apiManager.global;
Object.assign(data, tabTracker.getBrowserData(browser), additionalData);
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -50,22 +50,19 @@ let BASE_MANIFEST = Object.freeze({
"manifest_version": 2,
"name": "name",
"version": "0",
});
function frameScript() {
- Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
+ Components.utils.import("resource://gre/modules/Services.jsm");
- ExtensionContent.init(this);
- this.addEventListener("unload", () => { // eslint-disable-line mozilla/balanced-listeners
- ExtensionContent.uninit(this);
- });
+ Services.obs.notifyObservers(this, "tab-content-frameloader-created", "");
}
const FRAME_SCRIPT = `data:text/javascript,(${encodeURI(frameScript)}).call(this)`;
const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
`<?xml version="1.0"?>
<window id="documentElement"/>`);
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -2528,16 +2528,17 @@ this.Schemas = {
this.initialized = true;
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
let data = Services.cpmm.initialProcessData;
let schemas = data["Extension:Schemas"];
if (schemas) {
this.schemaJSON = schemas;
}
+
Services.cpmm.addMessageListener("Schema:Add", this);
}
this.flushSchemas();
},
receiveMessage(msg) {
switch (msg.name) {
copy from toolkit/components/extensions/ExtensionContent.jsm
copy to toolkit/components/extensions/extension-process-script.js
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -1,1166 +1,201 @@
/* 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.
+/**
+ * This script contains the minimum, skeleton content process code that we need
+ * in order to lazily load other extension modules when they are first
+ * necessary. Anything which is not likely to be needed immediately, or shortly
+ * after startup, in *every* browser process live outside of this file.
*/
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
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, "MatchGlobs",
+ "resource://gre/modules/MatchPattern.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");
-XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
- "@mozilla.org/content/style-sheet-service;1",
- "nsIStyleSheetService");
-
-const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
-
-Cu.import("resource://gre/modules/ExtensionChild.jsm");
-Cu.import("resource://gre/modules/ExtensionCommon.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+ "resource://gre/modules/ExtensionChild.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
+ "resource://gre/modules/ExtensionContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
+ "resource://gre/modules/ExtensionUtils.jsm");
-const {
- DefaultMap,
- DefaultWeakMap,
- EventEmitter,
- LocaleData,
- defineLazyGetter,
- flushJarCache,
- getInnerWindowID,
- getWinUtils,
- promiseDocumentReady,
- runSafeSyncWithoutClone,
-} = ExtensionUtils;
+XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
+XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID);
-const {
- BaseContext,
- CanOfAPIs,
- SchemaAPIManager,
-} = ExtensionCommon;
-
-const {
- ChildAPIManager,
- Messenger,
-} = ExtensionChild;
+const isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
-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;
- }
+class ScriptMatcher {
+ constructor(extension, options) {
+ this.extension = extension;
+ this.options = options;
- lazyInit() {
- if (!this.initialized) {
- this.initialized = true;
- 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;
+ this._script = null;
-const scriptCaches = new WeakSet();
-const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
-
-class CacheMap extends DefaultMap {
- constructor(timeout, getter) {
- super(getter);
-
- this.expiryTimeout = timeout;
-
- scriptCaches.add(this);
- }
+ this.allFrames = options.all_frames;
+ this.matchAboutBlank = options.match_about_blank;
+ this.frameId = options.frame_id;
+ this.runAt = options.run_at;
- 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;
+ this.matches = new MatchPattern(options.matches);
+ this.excludeMatches = new MatchPattern(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.matchesHost = new MatchPattern(options.matchesHost || null);
+ this.includeGlobs = options.include_globs && new MatchGlobs(options.include_globs);
+ this.excludeGlobs = new MatchGlobs(options.exclude_globs);
}
- 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()) {
- if (now - promise.lastUsed >= timeout) {
- this.delete(url);
- }
- }
- }
-}
-
-class ScriptCache extends CacheMap {
- constructor(options) {
- super(SCRIPT_EXPIRY_TIMEOUT_MS,
- url => ChromeUtils.compileScript(url, options));
- }
-}
-
-class CSSCache extends CacheMap {
- constructor(sheetType) {
- super(CSS_EXPIRY_TIMEOUT_MS, url => {
- let uri = Services.io.newURI(url);
- return styleSheetService.preloadSheetAsync(uri, sheetType).then(sheet => {
- return {url, sheet};
- });
- });
- }
-
- addDocument(url, document) {
- sheetCacheDocuments.get(this.get(url)).add(document);
- }
-
- deleteDocument(url, document) {
- sheetCacheDocuments.get(this.get(url)).delete(document);
+ toString() {
+ return `[Script {js: [${this.options.js}], matchAboutBlank: ${this.matchAboutBlank}, runAt: ${this.runAt}, matches: ${this.options.matches}}]`;
}
- delete(url) {
- if (this.has(url)) {
- let promise = this.get(url);
-
- // 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;
- }
+ get script() {
+ if (!this._script) {
+ this._script = new ExtensionContent.Script(this.extension.realExtension,
+ this.options);
}
-
- super.delete(url);
- }
-}
-
-// Represents a content script.
-class Script {
- constructor(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.css_origin = this.options.css_origin;
-
- this.deferred = deferred;
-
- this.cssCache = extension[this.css_origin === "user" ? "userCSS"
- : "authorCSS"];
- this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
- : "staticScripts"];
-
- if (options.wantReturnValue) {
- this.compileScripts();
- this.loadCSS();
- }
-
- 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);
+ return this._script;
}
- compileScripts() {
- return this.js.map(url => this.scriptCache.get(url));
- }
+ preload() {
+ let {script} = this;
- loadCSS() {
- return this.cssURLs.map(url => this.cssCache.get(url));
+ script.loadCSS();
+ script.compileScripts();
}
matchesLoadInfo(uri, loadInfo) {
if (!this.matchesURI(uri)) {
return false;
}
- if (!this.options.all_frames && !loadInfo.isTopLevelLoad) {
+ if (!this.allFrames && !loadInfo.isTopLevelLoad) {
return false;
}
return true;
}
matchesURI(uri) {
- if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
- return false;
- }
-
- if (this.exclude_matches_.matches(uri)) {
+ if (!(this.matches.matches(uri) || this.matchesHost.matchesIgnoringPath(uri))) {
return false;
}
- if (this.options.include_globs != null) {
- if (!this.include_globs_.matches(uri.spec)) {
- return false;
- }
+ if (this.excludeMatches.matches(uri)) {
+ return false;
}
- if (this.exclude_globs_.matches(uri.spec)) {
+ if (this.includeGlobs != null && !this.includeGlobs.matches(uri.spec)) {
+ return false;
+ }
+
+ if (this.excludeGlobs.matches(uri.spec)) {
return false;
}
return true;
}
- matches(window) {
+ matchesWindow(window) {
+ if (!this.allFrames && this.frameId == null && window.parent !== window) {
+ return false;
+ }
+
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) {
+ if (this.matchAboutBlank) {
// When matching top-level about:blank documents,
// allow loading into any with a NullPrincipal.
- if (uri.spec === "about:blank" && window === window.top && principal.isNullPrincipal) {
+ if (uri.spec === "about:blank" && window === window.parent && principal.isNullPrincipal) {
return true;
}
// When matching about:blank/srcdoc iframes, the checks below
// need to be performed against the "owner" document's URI.
if (["about:blank", "about:srcdoc"].includes(uri.spec)) {
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) {
+ if (!this.matchAboutBlank) {
return false;
}
uri = principal.URI;
}
if (!this.matchesURI(uri)) {
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) {
+ if (this.frameId != null && WebNavigationFrames.getFrameId(window) !== this.frameId) {
+ return false;
+ }
+
+ // If mozAddonManager is present on this page, don't allow
+ // content scripts.
+ if (window.navigator.mozAddonManager !== undefined) {
return false;
}
return true;
}
- cleanup(window) {
- if (!this.remove_css && this.cssURLs.length) {
- let winUtils = getWinUtils(window);
-
- let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
- for (let url of this.cssURLs) {
- this.cssCache.deleteDocument(url, window.document);
- runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
- }
-
- // Clear any sheets that were kept alive past their timeout as
- // a result of living in this document.
- this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
- }
- }
-
- /**
- * 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 (this.cssURLs.length && shouldRun("document_start")) {
- let winUtils = getWinUtils(window);
-
- let innerWindowID = winUtils.currentInnerWindowID;
-
- let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
-
- if (this.remove_css) {
- for (let url of this.cssURLs) {
- this.cssCache.deleteDocument(url, window.document);
-
- runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
- }
-
- this.deferred.resolve();
- } else {
- this.deferred.resolve(
- Promise.all(this.loadCSS()).then(sheets => {
- if (winUtils.currentInnerWindowID !== innerWindowID) {
- return;
- }
-
- for (let {url, sheet} of sheets) {
- this.cssCache.addDocument(url, window.document);
-
- runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
- }
- }));
- }
- }
-
- let scheduled = this.run_at || "document_idle";
- if (shouldRun(scheduled)) {
- let scriptsPromise = Promise.all(this.compileScripts());
-
- // 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.
- if (this.run_at === "document_start" && when === "document_start") {
- window.document.blockParsing(scriptsPromise);
- }
-
- this.deferred.resolve(scriptsPromise.then(scripts => {
- let result;
-
- // The evaluations below may throw, in which case the promise will be
- // automatically rejected.
- for (let script of scripts) {
- result = script.executeInGlobal(sandbox);
- }
-
- if (this.options.jsCode) {
- result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
- }
-
- return result;
- }));
- }
+ injectInto(window) {
+ return this.script.injectInto(window);
}
}
-defineLazyGetter(Script.prototype, "cssURLs", function() {
- // We can handle CSS urls (css) and CSS code (cssCode).
- let urls = this.css.slice();
-
- if (this.options.cssCode) {
- urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode));
- }
-
- return urls;
-});
-
-
-function getWindowMessageManager(contentWindow) {
- let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .QueryInterface(Ci.nsIInterfaceRequestor);
+function getMessageManager(contentWindow) {
+ let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor);
try {
- return ir.getInterface(Ci.nsIContentFrameMessageManager);
+ return docShell.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.
- let attrs = contentPrincipal.originAttributes;
- 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, {
- 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,
- 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() {
- 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 = {
- extensionCount: 0,
-
- // Map[windowId -> Map[extensionId -> ContentScriptContextChild]]
- contentScriptWindows: new Map(),
-
- // Map[windowId -> ContentScriptContextChild]
- extensionPageWindows: new Map(),
-
- init() {
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- Services.obs.addObserver(this, "http-on-opening-request", false);
- }
- 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);
- Services.obs.addObserver(this, "memory-pressure", false);
- },
-
- uninit() {
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- Services.obs.removeObserver(this, "http-on-opening-request");
- }
- Services.obs.removeObserver(this, "content-document-global-created");
- Services.obs.removeObserver(this, "document-element-inserted");
- Services.obs.removeObserver(this, "inner-window-destroyed");
- Services.obs.removeObserver(this, "memory-pressure");
- },
-
- 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);
- }
- }
- }
- },
-
- observers: {
- // For some types of documents (about:blank), we only see the first
- // notification, for others (data: URIs) we only observe the second.
- "content-document-global-created"(subject, topic, data) {
- this.observers["document-element-inserted"].call(this, subject.document, topic, data);
- },
- "document-element-inserted"(subject, topic, data) {
- let document = subject;
- let window = document && document.defaultView;
-
- 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 */
- },
- "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.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);
- },
- "http-on-opening-request"(subject, topic, data) {
- let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
- if (loadInfo) {
- let {externalContentPolicyType: type} = loadInfo;
- if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
- type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
- this.preloadScripts(subject.URI, loadInfo);
- }
- }
- },
- "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);
- },
-
- handleEvent(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) {
- 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`});
- }
- 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();
- }
- },
-
- preloadScripts(uri, loadInfo) {
- for (let extension of ExtensionManager.extensions.values()) {
- for (let script of extension.scripts) {
- if (script.matchesLoadInfo(uri, loadInfo)) {
- script.loadCSS();
- script.compileScripts();
- }
- }
- }
- },
-
- 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);
-
- defineLazyGetter(this, "scripts", () => {
- return 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.optionalPermissions = data.optionalPermissions;
- this.principal = data.principal;
-
- this.localeData = new LocaleData(data.localeData);
-
- this.manifest = data.manifest;
- this.baseURI = Services.io.newURI(data.baseURL);
-
- // Only used in addon processes.
- this.views = new Set();
-
- // Only used for devtools views.
- this.devtoolsViews = new Set();
-
- let uri = Services.io.newURI(data.resourceURL);
-
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- // Extension.jsm takes care of this in the parent.
- ExtensionManagement.startupExtension(this.uuid, uri, this);
- }
-
- /* eslint-disable mozilla/balanced-listeners */
- this.on("add-permissions", (ignoreEvent, permissions) => {
- if (permissions.permissions.length > 0) {
- for (let perm of permissions.permissions) {
- this.permissions.add(perm);
- }
- }
-
- if (permissions.origins.length > 0) {
- this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
- }
- });
-
- this.on("remove-permissions", (ignoreEvent, permissions) => {
- if (permissions.permissions.length > 0) {
- for (let perm of permissions.permissions) {
- this.permissions.delete(perm);
- }
- }
-
- if (permissions.origins.length > 0) {
- for (let origin of permissions.origins) {
- this.whiteListedHosts.removeOne(origin);
- }
- }
- });
- /* eslint-enable mozilla/balanced-listeners */
- }
-
- 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);
- }
-}
-
-defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
- return new ScriptCache({hasReturnValue: false});
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
- return new ScriptCache({hasReturnValue: true});
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", () => {
- return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET);
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
- return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
-});
-
-ExtensionManager = {
- // Map[extensionId, BrowserExtensionContent]
- extensions: new Map(),
-
- 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": {
- flushJarCache(data.path);
- 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 = getWinUtils(global.content).outerWindowID;
+ 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});
}
@@ -1168,125 +203,409 @@ class ExtensionGlobal {
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);
+ return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options);
case "Extension:DetectLanguage":
- return this.handleDetectLanguage(target);
+ return ExtensionContent.handleDetectLanguage(this.global, 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);
+ let extension = ExtensionManager.get(recipient.extensionId);
+ let script = new ScriptMatcher(extension, data.options);
- // 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) {
- 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({frameId}) {
- return WebNavigationFrames.getFrame(this.global.docShell, frameId);
- }
-
- handleWebNavigationGetAllFrames() {
- return WebNavigationFrames.getAllFrames(this.global.docShell);
+ return ExtensionContent.handleExtensionExecute(this.global, target, data.options, script);
+ case "WebNavigation:GetFrame":
+ return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options);
+ case "WebNavigation:GetAllFrames":
+ return ExtensionContent.handleWebNavigationGetAllFrames(this.global);
+ }
}
}
-this.ExtensionContent = {
+// Responsible for creating ExtensionContexts and injecting content
+// scripts into them when new documents are created.
+DocumentManager = {
globals: new Map(),
- init(global) {
+ // Initialize listeners that we need regardless of whether extensions are
+ // enabled.
+ earlyInit() {
+ Services.obs.addObserver(this, "tab-content-frameloader-created", false); // eslint-disable-line mozilla/balanced-listeners
+ },
+
+ // Initialize listeners that we need when any extension is enabled.
+ init() {
+ Services.obs.addObserver(this, "document-element-inserted", false);
+ },
+ uninit() {
+ Services.obs.removeObserver(this, "document-element-inserted");
+ },
+
+ // Initialize listeners that we need when any extension content script is
+ // enabled.
+ initMatchers() {
+ if (isContentProcess) {
+ Services.obs.addObserver(this, "http-on-opening-request", false);
+ }
+ },
+ uninitMatchers() {
+ if (isContentProcess) {
+ Services.obs.removeObserver(this, "http-on-opening-request");
+ }
+ },
+
+ // Initialize listeners that we need when any about:blank content script is
+ // enabled.
+ //
+ // Loads of about:blank are special, and do not trigger "document-element-inserted"
+ // observers. So if we have any scripts that match about:blank, we also need
+ // to observe "content-document-global-created".
+ initAboutBlankMatchers() {
+ Services.obs.addObserver(this, "content-document-global-created", false);
+ },
+ uninitAboutBlankMatchers() {
+ Services.obs.removeObserver(this, "content-document-global-created");
+ },
+
+ // Initialize a frame script global which extension contexts may be loaded
+ // into.
+ initGlobal(global) {
+ // Note: {once: true} does not work as expected here.
+ global.addEventListener("unload", event => { // eslint-disable-line mozilla/balanced-listeners
+ this.uninitGlobal(global);
+ });
+
this.globals.set(global, new ExtensionGlobal(global));
if (ExtensionManagement.isExtensionProcess) {
ExtensionChild.init(global);
}
},
-
- uninit(global) {
+ uninitGlobal(global) {
if (ExtensionManagement.isExtensionProcess) {
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);
+ initExtension(extension) {
+ if (this.extensionCount === 0) {
+ this.init();
+ }
+ this.extensionCount++;
+
+ for (let script of extension.scripts) {
+ this.addContentScript(script);
+ }
+
+ this.injectExtensionScripts(extension);
+ },
+ uninitExtension(extension) {
+ for (let script of extension.scripts) {
+ this.removeContentScript(script);
+ }
+
+ this.extensionCount--;
+ if (this.extensionCount === 0) {
+ this.uninit();
+ }
+ },
+
+
+ extensionCount: 0,
+ matchAboutBlankCount: 0,
+
+ contentScripts: new Set(),
+
+ addContentScript(script) {
+ if (this.contentScripts.size == 0) {
+ this.initMatchers();
+ }
+
+ if (script.matchAboutBlank) {
+ if (this.matchAboutBlankCount == 0) {
+ this.initAboutBlankMatchers();
+ }
+ this.matchAboutBlankCount++;
+ }
+
+ this.contentScripts.add(script);
+ },
+ removeContentScript(script) {
+ this.contentScripts.delete(script);
+
+ if (this.contentScripts.size == 0) {
+ this.uninitMatchers();
+ }
+
+ if (script.matchAboutBlank) {
+ this.matchAboutBlankCount--;
+ if (this.matchAboutBlankCount == 0) {
+ this.uninitAboutBlankMatchers();
+ }
+ }
+ },
+
+ // Listeners
+
+ observers: {
+ async "content-document-global-created"(window) {
+ // We only care about about:blank here, since it doesn't trigger
+ // "document-element-inserted".
+ if ((window.location && window.location.href !== "about:blank") ||
+ // Make sure we only load into frames that belong to tabs, or other
+ // special areas that we want to load content scripts into.
+ !this.globals.has(getMessageManager(window))) {
+ return;
+ }
+
+ // We can't tell for certain whether the final document will actually be
+ // about:blank at this point, though, so wait for the DOM to finish
+ // loading and check again before injecting scripts.
+ await new Promise(resolve => window.addEventListener(
+ "DOMContentLoaded", resolve, {once: true, capture: true}));
+
+ if (window.location.href === "about:blank") {
+ this.injectWindowScripts(window);
+ }
+ },
+
+ "document-element-inserted"(document) {
+ let window = document.defaultView;
+ if (!document.location || !window ||
+ // Make sure we only load into frames that belong to tabs, or other
+ // special areas that we want to load content scripts into.
+ !this.globals.has(getMessageManager(window))) {
+ return;
+ }
+
+ this.injectWindowScripts(window);
+ this.loadInto(window);
+ },
+
+ "http-on-opening-request"(subject, topic, data) {
+ // If this request is a docshell load, check whether any of our scripts
+ // are likely to be loaded into it, and begin preloading the ones that
+ // are.
+ let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
+ if (loadInfo) {
+ let {externalContentPolicyType: type} = loadInfo;
+ if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
+ type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
+ this.preloadScripts(subject.URI, loadInfo);
+ }
+ }
+ },
+
+ "tab-content-frameloader-created"(global) {
+ this.initGlobal(global);
+ },
+ },
+
+ observe(subject, topic, data) {
+ this.observers[topic].call(this, subject, topic, data);
+ },
+
+ // Script loading
+
+ injectExtensionScripts(extension) {
+ for (let window of this.enumerateWindows()) {
+ for (let script of extension.scripts) {
+ if (script.matchesWindow(window)) {
+ script.injectInto(window);
+ }
+ }
+ }
+ },
+
+ injectWindowScripts(window) {
+ for (let script of this.contentScripts) {
+ if (script.matchesWindow(window)) {
+ script.injectInto(window);
+ }
+ }
+ },
+
+ preloadScripts(uri, loadInfo) {
+ for (let script of this.contentScripts) {
+ if (script.matchesLoadInfo(uri, loadInfo)) {
+ script.preload();
+ }
+ }
+ },
+
+ loadInto(window) {
+ let extensionId = ExtensionManagement.getAddonIdForWindow(window);
+ if (!extensionId) {
+ return;
+ }
+
+ let extension = ExtensionManager.get(extensionId);
+ if (!extension) {
+ throw new Error(`No registered extension for ID ${extensionId}`);
+ }
+
+ let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
+ const levels = ExtensionManagement.API_LEVELS;
+
+ if (apiLevel === levels.CONTENTSCRIPT_PRIVILEGES) {
+ ExtensionContent.initExtensionContext(extension.realExtension, window);
+ } else if (apiLevel === levels.FULL_PRIVILEGES) {
+ ExtensionChild.initExtensionContext(extension.realExtension, window);
+ } else {
+ throw new Error(`Unexpected window with extension ID ${extensionId}`);
+ }
+ },
+
+ // Helpers
+
+ * enumerateWindows(docShell) {
+ if (docShell) {
+ let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
+ docShell.ENUMERATE_FORWARDS);
+
+ for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) {
+ yield docShell.getInterface(Ci.nsIDOMWindow);
+ }
+ } else {
+ for (let global of this.globals.keys()) {
+ yield* this.enumerateWindows(global.docShell);
+ }
+ }
},
};
+/**
+ * This class is a minimal stub extension object which loads and instantiates a
+ * real extension object when non-basic functionality is needed.
+ */
+class StubExtension {
+ constructor(data) {
+ this.data = data;
+ this.id = data.id;
+ this.uuid = data.uuid;
+ this.instanceId = data.instanceId;
+ this.manifest = data.manifest;
+
+ this.scripts = data.content_scripts.map(scriptData => new ScriptMatcher(this, scriptData));
+
+ this._realExtension = null;
+
+ this.startup();
+ }
+
+ startup() {
+ // Extension.jsm takes care of this in the parent.
+ if (isContentProcess) {
+ let uri = Services.io.newURI(this.data.resourceURL);
+ ExtensionManagement.startupExtension(this.uuid, uri, this);
+ }
+ }
+
+ shutdown() {
+ if (isContentProcess) {
+ ExtensionManagement.shutdownExtension(this.uuid);
+ }
+ if (this._realExtension) {
+ this._realExtension.shutdown();
+ }
+ }
+
+ // Lazily create the real extension object when needed.
+ get realExtension() {
+ if (!this._realExtension) {
+ this._realExtension = new ExtensionContent.BrowserExtensionContent(this.data);
+ }
+ return this._realExtension;
+ }
+
+ // Forward functions needed by ExtensionManagement.
+ hasPermission(...args) {
+ return this.realExtension.hasPermission(...args);
+ }
+ localize(...args) {
+ return this.realExtension.localize(...args);
+ }
+ get whiteListedHosts() {
+ return this.realExtension.whiteListedHosts;
+ }
+ get webAccessibleResources() {
+ return this.realExtension.webAccessibleResources;
+ }
+}
+
+ExtensionManager = {
+ // Map[extensionId -> StubExtension]
+ extensions: new Map(),
+
+ init() {
+ MessageChannel.setupMessageManagers([Services.cpmm]);
+
+ Services.cpmm.addMessageListener("Extension:Startup", this);
+ Services.cpmm.addMessageListener("Extension:Shutdown", this);
+ Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
+
+ let procData = Services.cpmm.initialProcessData || {};
+
+ for (let data of procData["Extension:Extensions"] || []) {
+ let extension = new StubExtension(data);
+ this.extensions.set(data.id, extension);
+ DocumentManager.initExtension(extension);
+ }
+
+ if (isContentProcess) {
+ // Make sure we handle new schema data until Schemas.jsm is loaded.
+ if (!procData["Extension:Schemas"]) {
+ procData["Extension:Schemas"] = new Map();
+ }
+ this.schemaJSON = procData["Extension:Schemas"];
+
+ Services.cpmm.addMessageListener("Schema:Add", this);
+ }
+ },
+
+ get(extensionId) {
+ return this.extensions.get(extensionId);
+ },
+
+ receiveMessage({name, data}) {
+ switch (name) {
+ case "Extension:Startup": {
+ let extension = new StubExtension(data);
+
+ this.extensions.set(data.id, extension);
+
+ DocumentManager.initExtension(extension);
+
+ Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
+ break;
+ }
+
+ case "Extension:Shutdown": {
+ let extension = this.extensions.get(data.id);
+ this.extensions.delete(data.id);
+
+ extension.shutdown();
+
+ DocumentManager.uninitExtension(extension);
+ break;
+ }
+
+ case "Extension:FlushJarCache": {
+ ExtensionUtils.flushJarCache(data.path);
+ Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
+ break;
+ }
+
+ case "Schema:Add": {
+ this.schemaJSON.set(data.url, data.schema);
+ break;
+ }
+ }
+ },
+};
+
+DocumentManager.earlyInit();
ExtensionManager.init();
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -1,14 +1,15 @@
# 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/.
toolkit.jar:
% content extensions %content/extensions/
+ content/extensions/extension-process-script.js
content/extensions/ext-alarms.js
content/extensions/ext-backgroundPage.js
content/extensions/ext-browser-content.js
content/extensions/ext-contextualIdentities.js
content/extensions/ext-cookies.js
content/extensions/ext-downloads.js
content/extensions/ext-extension.js
content/extensions/ext-geolocation.js
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
@@ -16,19 +16,24 @@
add_task(function* test_contentscript_cache() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [{
"matches": ["http://example.com/"],
"js": ["content_script.js"],
"run_at": "document_start",
}],
+
+ permissions: ["<all_urls>", "tabs"],
},
- background() {
+ async background() {
+ // Force our extension instance to be initialized for the current content process.
+ await browser.tabs.insertCSS({code: ""});
+
browser.test.sendMessage("origin", location.origin);
},
files: {
"content_script.js": function() {
browser.test.sendMessage("content-script-loaded");
},
},
@@ -57,17 +62,19 @@ add_task(function* test_contentscript_ca
let {appinfo} = SpecialPowers.Services;
if (appinfo.processType === appinfo.PROCESS_TYPE_CONTENT) {
/* globals addMessageListener, assert */
chromeScript = SpecialPowers.loadChromeScript(() => {
addMessageListener("check-script-cache", extensionId => {
let {ExtensionManager} = Components.utils.import("resource://gre/modules/ExtensionContent.jsm", {});
let ext = ExtensionManager.extensions.get(extensionId);
- assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
+ if (ext) {
+ assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
+ }
sendAsyncMessage("done");
});
});
chromeScript.sendAsyncMessage("check-script-cache", extension.id);
chromeScriptDone = chromeScript.promiseOneMessage("done");
}
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
@@ -45,17 +45,17 @@ add_task(function* test_contentscript_co
let win = window.open("http://example.com/");
yield extension.awaitMessage("content-script-ready");
yield extension.awaitMessage("content-script-show");
// Get the content script context and check that it points to the correct window.
let {DocumentManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {});
- let context = DocumentManager.getContentScriptContext(extension, win);
+ let context = DocumentManager.getContext(extension.id, win);
ok(context != null, "Got content script context");
is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct");
// Navigate so that the content page is hidden in the bfcache.
win.location = "http://example.org/";
yield extension.awaitMessage("content-script-hide");
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
@@ -55,27 +55,27 @@ add_task(function* test_contentscript_de
.currentInnerWindowID;
yield extension.awaitFinish("contentScript.executed");
const {ExtensionContent} = SpecialPowers.Cu.import(
"resource://gre/modules/ExtensionContent.jsm", {}
);
- let res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+ let res = ExtensionContent.getContentScriptGlobals(win);
is(res.length, 1, "Got the expected array of globals");
let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {};
is(metadata.addonId, extension.id, "Got the expected addonId");
is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id");
yield extension.unload();
info("extension unloaded");
- res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+ res = ExtensionContent.getContentScriptGlobals(win);
is(res.length, 0, "No content scripts globals found once the extension is unloaded");
win.close();
});
</script>
</body>
</html>
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -69,29 +69,24 @@ function convertDocShellToFrameDetail(do
url: window.location.href,
};
}
/**
* A generator function which iterates over a docShell tree, given a root docShell.
*
* @param {nsIDocShell} docShell - the root docShell object
- * @returns {Iterator<DocShell>} the FrameDetail JSON object which represents the docShell.
*/
function* iterateDocShellTree(docShell) {
let docShellsEnum = docShell.getDocShellEnumerator(
- Ci.nsIDocShellTreeItem.typeContent,
- Ci.nsIDocShell.ENUMERATE_FORWARDS
- );
+ docShell.typeContent, docShell.ENUMERATE_FORWARDS);
while (docShellsEnum.hasMoreElements()) {
yield docShellsEnum.getNext();
}
-
- return null;
}
/**
* Returns the frame ID of the given window. If the window is the
* top-level content window, its frame ID is 0. Otherwise, its frame ID
* is its outer window ID.
*
* @param {Window} window - The window to retrieve the frame ID for.