Bug 1425104: Part 1a - Support unrestricted matching in MatchPattern. draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 18 Apr 2018 14:02:05 -0700
changeset 785432 12f8659f76fe5e23e42cb881eb7ed0b31de73864
parent 784547 be9efc84b58282ae957c674b2f9596c86a181257
child 785433 d62accbb0470e19df495c70f7397e2a7db12eda7
push id107230
push usermaglione.k@gmail.com
push dateFri, 20 Apr 2018 05:24:08 +0000
bugs1425104
milestone61.0a1
Bug 1425104: Part 1a - Support unrestricted matching in MatchPattern. MozReview-Commit-ID: 4aW2w81LDTI
dom/chrome-webidl/MatchPattern.webidl
toolkit/components/extensions/MatchPattern.cpp
toolkit/components/extensions/MatchPattern.h
toolkit/components/extensions/test/xpcshell/test_MatchPattern.js
--- a/dom/chrome-webidl/MatchPattern.webidl
+++ b/dom/chrome-webidl/MatchPattern.webidl
@@ -120,9 +120,15 @@ interface MatchPatternSet {
 };
 
 dictionary MatchPatternOptions {
   /**
    * If true, the path portion of the pattern is ignored, and replaced with a
    * wildcard. The `pattern` property is updated to reflect this.
    */
   boolean ignorePath = false;
+
+  /**
+   * If true, the set of schemes this pattern can match is restricted to
+   * those accessible by WebExtensions.
+   */
+  boolean restrictSchemes = true;
 };
--- a/toolkit/components/extensions/MatchPattern.cpp
+++ b/toolkit/components/extensions/MatchPattern.cpp
@@ -269,25 +269,27 @@ const char* WILDCARD_SCHEMES[] = {"http"
 
 /* static */ already_AddRefed<MatchPattern>
 MatchPattern::Constructor(dom::GlobalObject& aGlobal,
                           const nsAString& aPattern,
                           const MatchPatternOptions& aOptions,
                           ErrorResult& aRv)
 {
   RefPtr<MatchPattern> pattern = new MatchPattern(aGlobal.GetAsSupports());
-  pattern->Init(aGlobal.Context(), aPattern, aOptions.mIgnorePath, aRv);
+  pattern->Init(aGlobal.Context(), aPattern, aOptions.mIgnorePath,
+                aOptions.mRestrictSchemes, aRv);
   if (aRv.Failed()) {
     return nullptr;
   }
   return pattern.forget();
 }
 
 void
-MatchPattern::Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath, ErrorResult& aRv)
+MatchPattern::Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath,
+                   bool aRestrictSchemes, ErrorResult& aRv)
 {
   RefPtr<AtomSet> permittedSchemes = AtomSet::Get<PERMITTED_SCHEMES>();
 
   mPattern = aPattern;
 
   if (aPattern.EqualsLiteral("<all_urls>")) {
     mSchemes = permittedSchemes;
     mMatchSubdomain = true;
@@ -305,57 +307,65 @@ MatchPattern::Init(JSContext* aCx, const
   if (index <= 0) {
     aRv.Throw(NS_ERROR_INVALID_ARG);
     return;
   }
 
   RefPtr<nsAtom> scheme = NS_AtomizeMainThread(StringHead(aPattern, index));
   if (scheme == nsGkAtoms::_asterisk) {
     mSchemes = AtomSet::Get<WILDCARD_SCHEMES>();
-  } else if (permittedSchemes->Contains(scheme) || scheme == nsGkAtoms::moz_extension) {
+  } else if (!aRestrictSchemes ||
+             permittedSchemes->Contains(scheme) ||
+             scheme == nsGkAtoms::moz_extension) {
     mSchemes = new AtomSet({scheme});
   } else {
     aRv.Throw(NS_ERROR_INVALID_ARG);
     return;
   }
 
   /***************************************************************************
    * Host
    ***************************************************************************/
   offset = index + 1;
   tail.Rebind(aPattern, offset);
 
-  if (!StringHead(tail, 2).EqualsLiteral("//")) {
-    aRv.Throw(NS_ERROR_INVALID_ARG);
-    return;
-  }
-
-  offset += 2;
-  tail.Rebind(aPattern, offset);
-  index = tail.FindChar('/');
-  if (index < 0) {
-    index = tail.Length();
-  }
-
-  auto host = StringHead(tail, index);
-  if (host.IsEmpty() && scheme != nsGkAtoms::file) {
-    aRv.Throw(NS_ERROR_INVALID_ARG);
-    return;
-  }
-
-  offset += index;
-  tail.Rebind(aPattern, offset);
-
-  if (host.EqualsLiteral("*")) {
-    mMatchSubdomain = true;
-  } else if (StringHead(host, 2).EqualsLiteral("*.")) {
-    mDomain = NS_ConvertUTF16toUTF8(Substring(host, 2));
+  if (scheme == nsGkAtoms::about) {
+    // about: URIs don't have hosts, so just treat the host as a wildcard and
+    // match on the path.
     mMatchSubdomain = true;
   } else {
-    mDomain = NS_ConvertUTF16toUTF8(host);
+    if (!StringHead(tail, 2).EqualsLiteral("//")) {
+      aRv.Throw(NS_ERROR_INVALID_ARG);
+      return;
+    }
+
+    offset += 2;
+    tail.Rebind(aPattern, offset);
+    index = tail.FindChar('/');
+    if (index < 0) {
+      index = tail.Length();
+    }
+
+    auto host = StringHead(tail, index);
+    if (host.IsEmpty() && scheme != nsGkAtoms::file) {
+      aRv.Throw(NS_ERROR_INVALID_ARG);
+      return;
+    }
+
+    offset += index;
+    tail.Rebind(aPattern, offset);
+
+    if (host.EqualsLiteral("*")) {
+      mMatchSubdomain = true;
+    } else if (StringHead(host, 2).EqualsLiteral("*.")) {
+      mDomain = NS_ConvertUTF16toUTF8(Substring(host, 2));
+      mMatchSubdomain = true;
+    } else {
+      mDomain = NS_ConvertUTF16toUTF8(host);
+    }
   }
 
   /***************************************************************************
    * Path
    ***************************************************************************/
   if (aIgnorePath) {
     mPattern.Truncate(offset);
     mPattern.AppendLiteral("/*");
--- a/toolkit/components/extensions/MatchPattern.h
+++ b/toolkit/components/extensions/MatchPattern.h
@@ -252,17 +252,18 @@ class MatchPattern final : public nsISup
   virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
 
 protected:
   virtual ~MatchPattern() = default;
 
 private:
   explicit MatchPattern(nsISupports* aParent) : mParent(aParent) {}
 
-  void Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath, ErrorResult& aRv);
+  void Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath,
+            bool aRestrictSchemes, ErrorResult& aRv);
 
   bool SubsumesDomain(const MatchPattern& aPattern) const;
 
 
   nsCOMPtr<nsISupports> mParent;
 
   // The normalized match pattern string that this object represents.
   nsString mPattern;
--- a/toolkit/components/extensions/test/xpcshell/test_MatchPattern.js
+++ b/toolkit/components/extensions/test/xpcshell/test_MatchPattern.js
@@ -1,45 +1,45 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(async function test_MatchPattern_matches() {
-  function test(url, pattern, normalized = pattern) {
+  function test(url, pattern, normalized = pattern, options = {}) {
     let uri = Services.io.newURI(url);
 
     pattern = Array.concat(pattern);
     normalized = Array.concat(normalized);
 
-    let patterns = pattern.map(pat => new MatchPattern(pat));
+    let patterns = pattern.map(pat => new MatchPattern(pat, options));
 
-    let set = new MatchPatternSet(pattern);
-    let set2 = new MatchPatternSet(patterns);
+    let set = new MatchPatternSet(pattern, options);
+    let set2 = new MatchPatternSet(patterns, options);
 
     deepEqual(set2.patterns, patterns, "Patterns in set should equal the input patterns");
 
     equal(set.matches(uri), set2.matches(uri), "Single pattern and pattern set should return the same match");
 
     for (let [i, pat] of patterns.entries()) {
       equal(pat.pattern, normalized[i], "Pattern property should contain correct normalized pattern value");
     }
 
     if (patterns.length == 1) {
       equal(patterns[0].matches(uri), set.matches(uri), "Single pattern and string set should return the same match");
     }
 
     return set.matches(uri);
   }
 
-  function pass({url, pattern, normalized}) {
-    ok(test(url, pattern, normalized), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
+  function pass({url, pattern, normalized, options}) {
+    ok(test(url, pattern, normalized, options), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
   }
 
-  function fail({url, pattern, normalized}) {
-    ok(!test(url, pattern, normalized), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
+  function fail({url, pattern, normalized, options}) {
+    ok(!test(url, pattern, normalized, options), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
   }
 
   function invalid({pattern}) {
     Assert.throws(() => new MatchPattern(pattern), /.*/,
                   `Invalid pattern '${pattern}' should throw`);
     Assert.throws(() => new MatchPatternSet([pattern]), /.*/,
                   `Invalid pattern '${pattern}' should throw`);
   }
@@ -106,16 +106,27 @@ add_task(async function test_MatchPatter
   // Multiple patterns.
   pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/"]});
   pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
   pass({url: "http://mozilla.com", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
   fail({url: "http://mozilla.biz", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
 
   // Match url with fragments.
   pass({url: "http://mozilla.org/base#some-fragment", pattern: "http://mozilla.org/base"});
+
+  // Privileged matchers:
+  invalid({pattern: "about:foo"});
+  invalid({pattern: "resource://foo/*"});
+
+  pass({url: "about:foo", pattern: ["about:foo", "about:foo*"], options: {restrictSchemes: false}});
+  pass({url: "about:foo", pattern: ["about:foo*"], options: {restrictSchemes: false}});
+  pass({url: "about:foobar", pattern: ["about:foo*"], options: {restrictSchemes: false}});
+  pass({url: "resource://foo/bar", pattern: ["resource://foo/bar"], options: {restrictSchemes: false}});
+  fail({url: "resource://fog/bar", pattern: ["resource://foo/bar"], options: {restrictSchemes: false}});
+  fail({url: "about:foo", pattern: ["about:meh"], options: {restrictSchemes: false}});
 });
 
 add_task(async function test_MatchPattern_overlaps() {
   function test(filter, hosts, optional) {
     filter = Array.concat(filter);
     hosts = Array.concat(hosts);
     optional = Array.concat(optional);