Bug 984048 - Patch 6 - Scope ordering and utility functions. r=ehsan,khuey
authorNikhil Marathe <nsm.nikhil@gmail.com>
Fri, 11 Jul 2014 11:52:19 -0700
changeset 215612 0bc1595ee11b05936341b254d604f503478de5f3
parent 215611 5a1db80a4c083d3b67544beacd80d892e874aa6c
child 215613 f98f0f5ba507095aac4335fa705e235484f2541a
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan, khuey
bugs984048
milestone33.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 984048 - Patch 6 - Scope ordering and utility functions. r=ehsan,khuey Added r=khuey via IRC to validate webidl DOM peer check.
dom/interfaces/base/nsIServiceWorkerManager.idl
dom/webidl/ServiceWorkerContainer.webidl
dom/workers/ServiceWorkerContainer.cpp
dom/workers/ServiceWorkerContainer.h
dom/workers/ServiceWorkerManager.cpp
dom/workers/ServiceWorkerManager.h
dom/workers/test/serviceworkers/mochitest.ini
dom/workers/test/serviceworkers/test_scopes.html
--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
+++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
@@ -1,21 +1,23 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "domstubs.idl"
 
-[uuid(d9539ecb-6665-452c-bae7-4e42f25d23aa)]
+[uuid(44ef0b7e-92c0-48a7-a092-5a49f2533792)]
 interface nsIServiceWorkerManager : nsISupports
 {
   // Returns a Promise
   nsISupports register(in nsIDOMWindow aWindow, in DOMString aScope, in DOMString aScriptURI);
 
   // Returns a Promise
   nsISupports unregister(in nsIDOMWindow aWindow, in DOMString aScope);
 
+  // Testing
+  DOMString getScopeForUrl(in DOMString path);
 };
 
 %{ C++
 #define SERVICEWORKERMANAGER_CONTRACTID "@mozilla.org/serviceworkers/manager;1"
 %}
--- a/dom/webidl/ServiceWorkerContainer.webidl
+++ b/dom/webidl/ServiceWorkerContainer.webidl
@@ -39,15 +39,19 @@ interface ServiceWorkerContainer {
   attribute EventHandler onerror;
 };
 
 // Testing only.
 [ChromeOnly, Pref="dom.serviceWorkers.testing.enabled"]
 partial interface ServiceWorkerContainer {
   [Throws]
   Promise clearAllServiceWorkerData();
+
+  [Throws]
+  DOMString getScopeForUrl(DOMString url);
+
   [Throws]
   DOMString getControllingWorkerScriptURLForPath(DOMString path);
 };
 
 dictionary RegistrationOptionList {
   DOMString scope = "/*";
 };
--- a/dom/workers/ServiceWorkerContainer.cpp
+++ b/dom/workers/ServiceWorkerContainer.cpp
@@ -132,16 +132,32 @@ already_AddRefed<Promise>
 ServiceWorkerContainer::ClearAllServiceWorkerData(ErrorResult& aRv)
 {
   aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
   return nullptr;
 }
 
 // Testing only.
 void
+ServiceWorkerContainer::GetScopeForUrl(const nsAString& aUrl,
+                                       nsString& aScope,
+                                       ErrorResult& aRv)
+{
+  nsresult rv;
+  nsCOMPtr<nsIServiceWorkerManager> swm = do_GetService(SERVICEWORKERMANAGER_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    aRv.Throw(rv);
+    return;
+  }
+
+  aRv = swm->GetScopeForUrl(aUrl, aScope);
+}
+
+// Testing only.
+void
 ServiceWorkerContainer::GetControllingWorkerScriptURLForPath(
                                                         const nsAString& aPath,
                                                         nsString& aScriptURL,
                                                         ErrorResult& aRv)
 {
   aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
 }
 } // namespace workers
--- a/dom/workers/ServiceWorkerContainer.h
+++ b/dom/workers/ServiceWorkerContainer.h
@@ -76,16 +76,20 @@ public:
   Ready();
 
   // Testing only.
   already_AddRefed<Promise>
   ClearAllServiceWorkerData(ErrorResult& aRv);
 
   // Testing only.
   void
+  GetScopeForUrl(const nsAString& aUrl, nsString& aScope, ErrorResult& aRv);
+
+  // Testing only.
+  void
   GetControllingWorkerScriptURLForPath(const nsAString& aPath,
                                        nsString& aScriptURL,
                                        ErrorResult& aRv);
 private:
   ~ServiceWorkerContainer()
   {
     // FIXME(nsm): Bug 983497. Unhook from events.
   }
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -362,18 +362,17 @@ public:
     nsCString domain;
     nsresult rv = mScriptURI->GetHost(domain);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       mPromise->MaybeReject(rv);
       return NS_OK;
     }
 
     nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
-    ServiceWorkerManager::ServiceWorkerDomainInfo* domainInfo =
-      swm->mDomainMap.Get(domain);
+    ServiceWorkerManager::ServiceWorkerDomainInfo* domainInfo;
     // XXXnsm: This pattern can be refactored if we end up using it
     // often enough.
     if (!swm->mDomainMap.Get(domain, &domainInfo)) {
       domainInfo = new ServiceWorkerManager::ServiceWorkerDomainInfo;
       swm->mDomainMap.Put(domain, domainInfo);
     }
 
     nsRefPtr<ServiceWorkerRegistration> registration =
@@ -982,16 +981,173 @@ ServiceWorkerManager::CreateServiceWorke
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   serviceWorker.forget(aServiceWorker);
   return rv;
 }
 
+already_AddRefed<ServiceWorkerRegistration>
+ServiceWorkerManager::GetServiceWorkerRegistration(nsPIDOMWindow* aWindow)
+{
+  nsCOMPtr<nsIURI> documentURI = aWindow->GetDocumentURI();
+  return GetServiceWorkerRegistration(documentURI);
+}
+
+already_AddRefed<ServiceWorkerRegistration>
+ServiceWorkerManager::GetServiceWorkerRegistration(nsIDocument* aDoc)
+{
+  nsCOMPtr<nsIURI> documentURI = aDoc->GetDocumentURI();
+  return GetServiceWorkerRegistration(documentURI);
+}
+
+already_AddRefed<ServiceWorkerRegistration>
+ServiceWorkerManager::GetServiceWorkerRegistration(nsIURI* aURI)
+{
+  if (!aURI) {
+    return nullptr;
+  }
+
+  nsCString domain;
+  nsresult rv = aURI->GetHost(domain);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return nullptr;
+  }
+
+  ServiceWorkerDomainInfo* domainInfo = mDomainMap.Get(domain);
+
+  // XXXnsm: This pattern can be refactored if we end up using it
+  // often enough.
+  if (!domainInfo) {
+    return nullptr;
+  }
+
+  nsCString spec;
+  rv = aURI->GetSpec(spec);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return nullptr;
+  }
+
+  nsCString scope = FindScopeForPath(domainInfo->mOrderedScopes, spec);
+  if (scope.IsEmpty()) {
+    return nullptr;
+  }
+
+  ServiceWorkerRegistration* registration;
+  domainInfo->mServiceWorkerRegistrations.Get(scope, &registration);
+  // ordered scopes and registrations better be in sync.
+  MOZ_ASSERT(registration);
+
+  return registration;
+}
+
+namespace {
+/*
+ * Returns string without trailing '*'.
+ */
+void ScopeWithoutStar(const nsACString& aScope, nsACString& out)
+{
+  if (aScope.Last() == '*') {
+    out.Assign(StringHead(aScope, aScope.Length() - 1));
+    return;
+  }
+
+  out.Assign(aScope);
+}
+}; // anonymous namespace
+
+/* static */ void
+ServiceWorkerManager::AddScope(nsTArray<nsCString>& aList, const nsACString& aScope)
+{
+  for (uint32_t i = 0; i < aList.Length(); ++i) {
+    const nsCString& current = aList[i];
+
+    // Perfect match!
+    if (aScope.Equals(current)) {
+      return;
+    }
+
+    nsCString withoutStar;
+    ScopeWithoutStar(current, withoutStar);
+    // Edge case of match without '*'.
+    // /foo should be sorted before /foo*.
+    if (aScope.Equals(withoutStar)) {
+      aList.InsertElementAt(i, aScope);
+      return;
+    }
+
+    // /foo/bar* should be before /foo/*
+    // Similarly /foo/b* is between the two.
+    // But is /foo* categorically different?
+    if (StringBeginsWith(aScope, withoutStar)) {
+      // If the new scope is a pattern and the old one is a path, the new one
+      // goes after.  This way Add(/foo) followed by Add(/foo*) ends up with
+      // [/foo, /foo*].
+      if (aScope.Last() == '*' &&
+          withoutStar.Equals(current)) {
+        aList.InsertElementAt(i+1, aScope);
+      } else {
+        aList.InsertElementAt(i, aScope);
+      }
+      return;
+    }
+  }
+
+  aList.AppendElement(aScope);
+}
+
+// aPath can have a '*' at the end, but it is treated literally.
+/* static */ nsCString
+ServiceWorkerManager::FindScopeForPath(nsTArray<nsCString>& aList, const nsACString& aPath)
+{
+  MOZ_ASSERT(aPath.FindChar('*') == -1);
+
+  nsCString match;
+
+  for (uint32_t i = 0; i < aList.Length(); ++i) {
+    const nsCString& current = aList[i];
+    nsCString withoutStar;
+    ScopeWithoutStar(current, withoutStar);
+    if (StringBeginsWith(aPath, withoutStar)) {
+      // If non-pattern match, then check equality.
+      if (current.Last() == '*' ||
+          aPath.Equals(current)) {
+        match = current;
+        break;
+      }
+    }
+  }
+
+  return match;
+}
+
+/* static */ void
+ServiceWorkerManager::RemoveScope(nsTArray<nsCString>& aList, const nsACString& aScope)
+{
+  aList.RemoveElement(aScope);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetScopeForUrl(const nsAString& aUrl, nsAString& aScope)
+{
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl, nullptr, nullptr);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsRefPtr<ServiceWorkerRegistration> r = GetServiceWorkerRegistration(uri);
+  if (!r) {
+      return NS_ERROR_FAILURE;
+  }
+
+  aScope = NS_ConvertUTF8toUTF16(r->mScope);
+  return NS_OK;
+}
 NS_IMETHODIMP
 ServiceWorkerManager::CreateServiceWorker(const nsACString& aScriptSpec,
                                           const nsACString& aScope,
                                           ServiceWorker** aServiceWorker)
 {
   AssertIsOnMainThread();
 
   WorkerPrivate::LoadInfo info;
--- a/dom/workers/ServiceWorkerManager.h
+++ b/dom/workers/ServiceWorkerManager.h
@@ -193,16 +193,25 @@ public:
 
   /*
    * This struct is used for passive ServiceWorker management.
    * Actively running ServiceWorkers use the SharedWorker infrastructure in
    * RuntimeService for execution and lifetime management.
    */
   struct ServiceWorkerDomainInfo
   {
+    // Ordered list of scopes for glob matching.
+    // Each entry is an absolute URL representing the scope.
+    //
+    // An array is used for now since the number of controlled scopes per
+    // domain is expected to be relatively low. If that assumption was proved
+    // wrong this should be replaced with a better structure to avoid the
+    // memmoves associated with inserting stuff in the middle of the array.
+    nsTArray<nsCString> mOrderedScopes;
+
     // Scope to registration.
     nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistration> mServiceWorkerRegistrations;
 
     ServiceWorkerDomainInfo()
     { }
 
     already_AddRefed<ServiceWorkerRegistration>
     GetRegistration(const nsCString& aScope) const
@@ -215,16 +224,17 @@ public:
     ServiceWorkerRegistration*
     CreateNewRegistration(const nsCString& aScope)
     {
       ServiceWorkerRegistration* registration =
         new ServiceWorkerRegistration(aScope);
       // From now on ownership of registration is with
       // mServiceWorkerRegistrations.
       mServiceWorkerRegistrations.Put(aScope, registration);
+      ServiceWorkerManager::AddScope(mOrderedScopes, aScope);
       return registration;
     }
   };
 
   nsClassHashtable<nsCStringHashKey, ServiceWorkerDomainInfo> mDomainMap;
 
   void
   ResolveRegisterPromises(ServiceWorkerRegistration* aRegistration,
@@ -280,16 +290,35 @@ private:
   CreateServiceWorker(const nsACString& aScriptSpec,
                       const nsACString& aScope,
                       ServiceWorker** aServiceWorker);
 
   static PLDHashOperator
   CleanupServiceWorkerInformation(const nsACString& aDomain,
                                   ServiceWorkerDomainInfo* aDomainInfo,
                                   void *aUnused);
+
+  already_AddRefed<ServiceWorkerRegistration>
+  GetServiceWorkerRegistration(nsPIDOMWindow* aWindow);
+
+  already_AddRefed<ServiceWorkerRegistration>
+  GetServiceWorkerRegistration(nsIDocument* aDoc);
+
+  already_AddRefed<ServiceWorkerRegistration>
+  GetServiceWorkerRegistration(nsIURI* aURI);
+
+  static void
+  AddScope(nsTArray<nsCString>& aList, const nsACString& aScope);
+
+  static nsCString
+  FindScopeForPath(nsTArray<nsCString>& aList, const nsACString& aPath);
+
+  static void
+  RemoveScope(nsTArray<nsCString>& aList, const nsACString& aScope);
+
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(ServiceWorkerManager,
                               NS_SERVICEWORKERMANAGER_IMPL_IID);
 
 } // namespace workers
 } // namespace dom
 } // namespace mozilla
--- a/dom/workers/test/serviceworkers/mochitest.ini
+++ b/dom/workers/test/serviceworkers/mochitest.ini
@@ -4,8 +4,9 @@ support-files =
   worker2.js
   worker3.js
   parse_error_worker.js
   install_event_worker.js
 
 [test_installation_simple.html]
 [test_install_event.html]
 [test_navigator.html]
+[test_scopes.html]
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_scopes.html
@@ -0,0 +1,69 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 984048 - Test scope glob matching.</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+  function registerWorkers() {
+    return Promise.all([
+      navigator.serviceWorker.register("worker.js", { scope: "*" }),
+      navigator.serviceWorker.register("worker.js", { scope: "sub/dir/*"}),
+      navigator.serviceWorker.register("worker.js", { scope: "sub/dir*" }),
+      navigator.serviceWorker.register("worker.js", { scope: "sub/dir" }),
+      navigator.serviceWorker.register("worker.js", { scope: "sub/dir/a*" }),
+      navigator.serviceWorker.register("worker.js", { scope: "sub*" }),
+    ]);
+  }
+
+  function testScopes() {
+    return new Promise(function(resolve, reject) {
+      var getScope = navigator.serviceWorker.getScopeForUrl.bind(navigator.serviceWorker);
+      var base = new URL(".", document.baseURI);
+
+      function p(s) {
+        return base + s;
+      }
+
+      ok(getScope(p("index.html")) === p("*"), "Scope should match");
+      ok(getScope(p("sua.html")) === p("*"), "Scope should match");
+      ok(getScope(p("sub.html")) === p("sub*"), "Scope should match");
+      ok(getScope(p("sub/dir.html")) === p("sub/dir*"), "Scope should match");
+      ok(getScope(p("sub/dir")) === p("sub/dir"), "Scope should match");
+      ok(getScope(p("sub/dir/foo")) === p("sub/dir/*"), "Scope should match");
+      ok(getScope(p("sub/dir/afoo")) === p("sub/dir/a*"), "Scope should match");
+      resolve(true);
+    });
+  }
+
+  function runTest() {
+    registerWorkers()
+      .then(testScopes)
+      .then(function() {
+        SimpleTest.finish();
+      }).catch(function(e) {
+        ok(false, "Some test failed with error " + e);
+        SimpleTest.finish();
+      });
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  SpecialPowers.pushPrefEnv({"set": [
+    ["dom.serviceWorkers.enabled", true],
+    ["dom.serviceWorkers.testing.enabled", true]
+  ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
+