--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -39,16 +39,20 @@ XPCOMUtils.defineLazyModuleGetter(this,
"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");
const {
DefaultMap,
@@ -100,79 +104,103 @@ var apiManager = new class extends Schem
registerSchemaAPI(namespace, envType, getAPI) {
if (envType == "content_child") {
super.registerSchemaAPI(namespace, envType, getAPI);
}
}
}();
-const SCRIPT_EXPIRY_TIMEOUT_MS = 300000;
-const SCRIPT_CLEAR_TIMEOUT_MS = 5000;
+const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
+const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
+
+const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
const scriptCaches = new WeakSet();
-class ScriptCache extends DefaultMap {
- constructor(options) {
- super(url => ChromeUtils.compileScript(url, options));
+class CacheMap extends DefaultMap {
+ constructor(timeout, getter) {
+ super(getter);
- this.expiryTimeout = SCRIPT_EXPIRY_TIMEOUT_MS;
+ this.expiryTimeout = timeout;
scriptCaches.add(this);
}
get(url) {
- let script = super.get(url);
+ let promise = super.get(url);
- script.lastUsed = Date.now();
- if (script.timer) {
- script.timer.cancel();
+ promise.lastUsed = Date.now();
+ if (promise.timer) {
+ promise.timer.cancel();
}
- script.timer = Timer(this.delete.bind(this, url),
- this.expiryTimeout,
- Ci.nsITimer.TYPE_ONE_SHOT);
+ promise.timer = Timer(this.delete.bind(this, url),
+ this.expiryTimeout,
+ Ci.nsITimer.TYPE_ONE_SHOT);
- return script;
+ return promise;
}
delete(url) {
if (this.has(url)) {
super.get(url).timer.cancel();
}
super.delete(url);
}
clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) {
let now = Date.now();
- for (let [url, script] of this.entries()) {
- if (now - script.lastUsed >= timeout) {
+ 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};
+ });
+ });
+ }
+}
+
// Represents a content script.
function Script(extension, options, deferred = PromiseUtils.defer()) {
this.extension = extension;
this.options = options;
this.run_at = this.options.run_at;
this.js = this.options.js || [];
this.css = this.options.css || [];
this.remove_css = this.options.remove_css;
this.match_about_blank = this.options.match_about_blank;
this.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);
@@ -181,29 +209,18 @@ function Script(extension, options, defe
this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
}
Script.prototype = {
compileScripts() {
return this.js.map(url => this.scriptCache.get(url));
},
- get cssURLs() {
- // We can handle CSS urls (css) and CSS code (cssCode).
- let urls = [];
- for (let url of this.css) {
- urls.push(this.extension.baseURI.resolve(url));
- }
-
- if (this.options.cssCode) {
- let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
- urls.push(url);
- }
-
- return urls;
+ 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) {
@@ -315,29 +332,41 @@ Script.prototype = {
* given state exactly matches the state that triggered the
* change.
* @param {string} when
* The document's current load state, or if triggered by a
* document state change, the new document state that triggered
* the injection.
*/
tryInject(window, sandbox, shouldRun, when) {
- if (shouldRun("document_start")) {
- let {cssURLs} = this;
- if (cssURLs.length > 0) {
- let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
+ if (this.cssURLs.length && shouldRun("document_start")) {
+ let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let innerWindowID = winUtils.currentInnerWindowID;
- let method = this.remove_css ? winUtils.removeSheetUsingURIString : winUtils.loadSheetUsingURIString;
- let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
- for (let url of cssURLs) {
- runSafeSyncWithoutClone(method, url, type);
+ let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
+
+ if (this.remove_css) {
+ for (let url of this.cssURLs) {
+ 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 {sheet} of sheets) {
+ 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,
@@ -361,16 +390,28 @@ Script.prototype = {
}
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).
@@ -864,16 +905,17 @@ DocumentManager = {
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") {
@@ -975,16 +1017,24 @@ class BrowserExtensionContent extends Ev
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() {
Schemas.init();
ExtensionChild.initOnce();