Bug 1368102: Part 4 - Use WebExtensionContentScript to match content scripts. r=mixedpuppy,zombie
authorKris Maglione <maglione.k@gmail.com>
Sat, 03 Jun 2017 17:11:08 -0700
changeset 412734 417fd0cf65a8bddc78c59156cb317cc4156f9392
parent 412733 b9439245aa6bc591c4e138ea2f6978d7e3bab831
child 412735 32a3b7c392070cb1f9eb7b1ec1e177c6303d9d3f
push id1490
push usermtabara@mozilla.com
push dateMon, 31 Jul 2017 14:08:16 +0000
treeherdermozilla-release@70e32e6bf15e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, zombie
bugs1368102
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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/WebExtensionPolicy.cpp
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/WebExtensionPolicy.cpp
+++ b/toolkit/components/extensions/WebExtensionPolicy.cpp
@@ -348,21 +348,26 @@ WebExtensionContentScript::Matches(const
       return false;
     }
   }
 
   if (!mMatchAboutBlank && aDoc.URL().InheritsPrincipal()) {
     return false;
   }
 
-  if (!MatchesURI(aDoc.PrincipalURL())) {
-    return false;
+  // Top-level about:blank is a special case. We treat it as a match if
+  // matchAboutBlank is true and it has the null principal. In all other
+  // cases, we test the URL of the principal that it inherits.
+  if (mMatchAboutBlank && aDoc.IsTopLevel() &&
+      aDoc.URL().Spec().EqualsLiteral("about:blank") &&
+      aDoc.Principal()->GetIsNullPrincipal()) {
+    return true;
   }
 
-  return true;
+  return MatchesURI(aDoc.PrincipalURL());
 }
 
 bool
 WebExtensionContentScript::MatchesURI(const URLInfo& aURL) const
 {
   if (!mMatches->Matches(aURL)) {
     return false;
   }
@@ -440,18 +445,18 @@ uint64_t
 DocInfo::FrameID() const
 {
   if (mFrameID.isNothing()) {
     if (IsTopLevel()) {
       mFrameID.emplace(0);
     } else {
       struct Matcher
       {
-        uint64_t match(Window aWin) { return aWin->GetCurrentInnerWindow()->WindowID(); }
-        uint64_t match(LoadInfo aLoadInfo) { return aLoadInfo->GetInnerWindowID(); }
+        uint64_t match(Window aWin) { return aWin->WindowID(); }
+        uint64_t match(LoadInfo aLoadInfo) { return aLoadInfo->GetOuterWindowID(); }
       };
       mFrameID.emplace(mObj.match(Matcher()));
     }
   }
   return mFrameID.ref();
 }
 
 nsIPrincipal*
--- 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.exclude_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.css_origin,
+          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) {