Bug 1348442: Part 2a - Asynchronously load and cache content script CSS. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Sat, 18 Mar 2017 15:18:34 -0700
changeset 501330 e75396bb9ec071396dbef3c0e6b66f6a52f329fe
parent 501329 4c019869b9eaa5d8d4a7a1518c172a346df20e79
child 501331 2791d9758ecf7906eea334be29f9b38f7daa67d1
push id49931
push usermaglione.k@gmail.com
push dateSun, 19 Mar 2017 23:22:27 +0000
reviewersaswan
bugs1348442
milestone55.0a1
Bug 1348442: Part 2a - Asynchronously load and cache content script CSS. r?aswan MozReview-Commit-ID: 1BzLNbdXMfU
toolkit/components/extensions/ExtensionContent.jsm
--- 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();