Bug 1368102: Part 4 - Use WebExtensionContentScript to match content scripts. r?mixedpuppy,zombie draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 25 May 2017 21:39:13 -0700
changeset 585213 e9a2a61c3aa96da5f547f976f6ba8879091e4084
parent 585212 ac295ae8830960d2a7d0a487091329caa7e0a318
child 585214 760921ea68741aec84f97eb6c573d9507ac8aafc
push id61052
push usermaglione.k@gmail.com
push dateFri, 26 May 2017 17:14:32 +0000
reviewersmixedpuppy, zombie
bugs1368102
milestone55.0a1
Bug 1368102: Part 4 - Use WebExtensionContentScript to match content scripts. r?mixedpuppy,zombie MozReview-Commit-ID: 1Ga0259WjC
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/extension-process-script.js
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -191,52 +191,52 @@ defineLazyGetter(BrowserExtensionContent
 });
 
 defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
   return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
 });
 
 // Represents a content script.
 class Script {
-  constructor(extension, options) {
+  constructor(extension, matcher) {
     this.extension = extension;
-    this.options = options;
+    this.matcher = matcher;
 
-    this.runAt = this.options.run_at;
-    this.js = this.options.js || [];
-    this.css = this.options.css || [];
-    this.remove_css = this.options.remove_css;
-    this.css_origin = this.options.css_origin;
+    this.runAt = this.matcher.runAt;
+    this.js = this.matcher.jsPaths;
+    this.css = this.matcher.cssPaths;
+    this.removeCSS = this.matcher.removeCSS;
+    this.cssOrigin = this.matcher.cssOrigin;
 
-    this.cssCache = extension[this.css_origin === "user" ? "userCSS"
-                                                         : "authorCSS"];
-    this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
+    this.cssCache = extension[this.cssOrigin === "user" ? "userCSS"
+                                                        : "authorCSS"];
+    this.scriptCache = extension[matcher.wantReturnValue ? "dynamicScripts"
                                                          : "staticScripts"];
 
-    if (options.wantReturnValue) {
+    if (matcher.wantReturnValue) {
       this.compileScripts();
       this.loadCSS();
     }
 
-    this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
+    this.requiresCleanup = !this.removeCss && (this.css.length > 0 || matcher.cssCode);
   }
 
   compileScripts() {
     return this.js.map(url => this.scriptCache.get(url));
   }
 
   loadCSS() {
     return this.cssURLs.map(url => this.cssCache.get(url));
   }
 
   cleanup(window) {
-    if (!this.remove_css && this.cssURLs.length) {
+    if (!this.removeCss && this.cssURLs.length) {
       let winUtils = getWinUtils(window);
 
-      let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
+      let type = this.cssOrigin === "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);
@@ -271,19 +271,19 @@ class Script {
       context.addScript(this);
     }
 
     let cssPromise;
     if (this.cssURLs.length) {
       let window = context.contentWindow;
       let winUtils = getWinUtils(window);
 
-      let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
+      let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
 
-      if (this.remove_css) {
+      if (this.removeCSS) {
         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;
@@ -320,31 +320,31 @@ class Script {
     }
 
     // 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, context.cloneScope, "latest");
+    if (this.matcher.jsCode) {
+      result = Cu.evalInSandbox(this.matcher.jsCode, context.cloneScope, "latest");
     }
 
     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));
+  if (this.matcher.cssCode) {
+    urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.matcher.cssCode));
   }
 
   return urls;
 });
 
 /**
  * An execution context for semi-privileged extension content scripts.
  *
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -25,16 +25,33 @@ XPCOMUtils.defineLazyGetter(this, "UUIDM
 const {appinfo} = Services;
 const isParentProcess = appinfo.processType === appinfo.PROCESS_TYPE_DEFAULT;
 
 /*
  * This file should be kept short and simple since it's loaded even
  * when no extensions are running.
  */
 
+function parseScriptOptions(options) {
+  return {
+    allFrames: options.all_frames,
+    matchAboutBlank: options.match_about_blank,
+    frameID: options.frame_id,
+    runAt: options.run_at,
+
+    matches: new MatchPatternSet(options.matches),
+    excludeMatches: new MatchPatternSet(options.exclude_matches || []),
+    includeGlobs: options.include_globs && options.include_globs.map(glob => new MatchGlob(glob)),
+    excludeGlobs: options.include_globs && options.exclude_globs.map(glob => new MatchGlob(glob)),
+
+    jsPaths: options.js || [],
+    cssPaths: options.css || [],
+  };
+}
+
 var APIs = {
   apis: new Map(),
 
   register(namespace, schema, script) {
     if (this.apis.has(namespace)) {
       throw new Error(`API namespace already exists: ${namespace}`);
     }
 
@@ -89,16 +106,18 @@ var ExtensionManagement = {
       webAccessibleResources: extension.webAccessibleResources || [],
 
       contentSecurityPolicy: extension.manifest.content_security_policy,
 
       localizeCallback: extension.localize.bind(extension),
 
       backgroundScripts: (extension.manifest.background &&
                           extension.manifest.background.scripts),
+
+      contentScripts: (extension.manifest.content_scripts || []).map(parseScriptOptions),
     });
 
     extension.policy = policy;
     policy.active = true;
   },
 
   // Called when an extension is unloaded.
   shutdownExtension(extension) {
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -14,18 +14,16 @@ const {classes: Cc, interfaces: Ci, util
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
                                   "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
-                                  "resource://gre/modules/WebNavigationFrames.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
                                   "resource://gre/modules/ExtensionChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
                                   "resource://gre/modules/ExtensionContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPageChild",
                                   "resource://gre/modules/ExtensionPageChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
@@ -34,131 +32,66 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
 XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID);
 
 // We need to avoid touching Services.appinfo here in order to prevent
 // the wrong version from being cached during xpcshell test startup.
 const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
 const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT;
 
+function parseScriptOptions(options) {
+  return {
+    allFrames: options.all_frames,
+    matchAboutBlank: options.match_about_blank,
+    frameID: options.frame_id,
+    runAt: options.run_at,
+
+    matches: new MatchPatternSet(options.matches),
+    excludeMatches: new MatchPatternSet(options.exclude_matches || []),
+    includeGlobs: options.include_globs && options.include_globs.map(glob => new MatchGlob(glob)),
+    excludeGlobs: options.include_globs && options.exclude_globs.map(glob => new MatchGlob(glob)),
+
+    jsPaths: options.js || [],
+    cssPaths: options.css || [],
+  };
+}
 
 class ScriptMatcher {
-  constructor(extension, options) {
+  constructor(extension, matcher) {
     this.extension = extension;
-    this.options = options;
+    this.matcher = matcher;
 
     this._script = null;
-
-    this.allFrames = options.all_frames;
-    this.matchAboutBlank = options.match_about_blank;
-    this.frameId = options.frame_id;
-    this.runAt = options.run_at;
-
-    this.matches = new MatchPatternSet(options.matches);
-    this.excludeMatches = new MatchPatternSet(options.exclude_matches || []);
-    this.includeGlobs = options.include_globs && options.include_globs.map(glob => new MatchGlob(glob));
-    this.excludeGlobs = options.include_globs && options.exclude_globs.map(glob => new MatchGlob(glob));
   }
 
-  toString() {
-    return `[Script {js: [${this.options.js}], matchAboutBlank: ${this.matchAboutBlank}, runAt: ${this.runAt}, matches: ${this.options.matches}}]`;
+  get matchAboutBlank() {
+    return this.matcher.matchAboutBlank;
   }
 
   get script() {
     if (!this._script) {
       this._script = new ExtensionContent.Script(this.extension.realExtension,
-                                                 this.options);
+                                                 this.matcher);
     }
     return this._script;
   }
 
   preload() {
     let {script} = this;
 
     script.loadCSS();
     script.compileScripts();
   }
 
   matchesLoadInfo(uri, loadInfo) {
-    if (!this.matchesURI(uri)) {
-      return false;
-    }
-
-    if (!this.allFrames && !loadInfo.isTopLevelLoad) {
-      return false;
-    }
-
-    return true;
-  }
-
-  matchesURI(uri) {
-    if (!(this.matches.matches(uri))) {
-      return false;
-    }
-
-    if (this.excludeMatches.matches(uri)) {
-      return false;
-    }
-
-    if (this.includeGlobs && !this.includeGlobs.some(glob => glob.matches(uri.spec))) {
-      return false;
-    }
-
-    if (this.excludeGlobs && this.excludeGlobs.some(glob => glob.matches(uri.spec))) {
-      return false;
-    }
-
-    return true;
+    return this.matcher.matchesLoadInfo(uri, loadInfo);
   }
 
   matchesWindow(window) {
-    if (!this.allFrames && this.frameId == null && window.parent !== window) {
-      return false;
-    }
-
-    let uri = window.document.documentURIObject;
-    let principal = window.document.nodePrincipal;
-
-    if (this.matchAboutBlank) {
-      // When matching top-level about:blank documents,
-      // allow loading into any with a NullPrincipal.
-      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.matchAboutBlank) {
-        return false;
-      }
-      uri = principal.URI;
-    }
-
-    if (!this.matchesURI(uri)) {
-      return false;
-    }
-
-    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;
+    return this.matcher.matchesWindow(window);
   }
 
   injectInto(window) {
     return this.script.injectInto(window);
   }
 }
 
 function getMessageManager(window) {
@@ -197,17 +130,28 @@ class ExtensionGlobal {
   receiveMessage({target, messageName, recipient, data}) {
     switch (messageName) {
       case "Extension:Capture":
         return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options);
       case "Extension:DetectLanguage":
         return ExtensionContent.handleDetectLanguage(this.global, target);
       case "Extension:Execute":
         let extension = ExtensionManager.get(recipient.extensionId);
-        let script = new ScriptMatcher(extension, data.options);
+
+        let matcher = new WebExtensionContentScript(extension.policy, parseScriptOptions(data.options));
+
+        let options = Object.assign(matcher, {
+          wantReturnValue: data.options.wantReturnValue,
+          removeCSS: data.options.remove_css,
+          cssOrigin: data.options.cssOrigin,
+          cssCode: data.options.cssCode,
+          jsCode: data.options.jsCode,
+        });
+
+        let script = new ScriptMatcher(extension, options);
 
         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);
     }
   }
@@ -531,28 +475,30 @@ class StubExtension {
     this.id = data.id;
     this.uuid = data.uuid;
     this.instanceId = data.instanceId;
     this.manifest = data.manifest;
     this.permissions = data.permissions;
     this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts);
     this.webAccessibleResources = data.webAccessibleResources.map(path => new MatchGlob(path));
 
-    this.scripts = data.content_scripts.map(scriptData => new ScriptMatcher(this, scriptData));
-
     this._realExtension = null;
 
     this.startup();
+
+    this.scripts = this.policy.contentScripts.map(matcher => new ScriptMatcher(this, matcher));
   }
 
   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);
+    } else {
+      this.policy = WebExtensionPolicy.getByID(this.id);
     }
   }
 
   shutdown() {
     if (isContentProcess) {
       ExtensionManagement.shutdownExtension(this);
     }
     if (this._realExtension) {