Bug 1388428 - Extend browsingData to restrict removing localStorage to a given list of hostnames; r=bsilverberg,janv draft
authorThomas Wisniewski <wisniewskit@gmail.com>
Tue, 08 Aug 2017 12:51:42 -0400
changeset 642669 89f621f0f1fb558eb19bbd75223220b5146488b2
parent 642518 a921bfb8a2cf3db4d9edebe9b35799a3f9d035da
child 725069 226c99d5c0ed8f296798c78d7effc12e719ab131
push id72834
push userbmo:wisniewskit@gmail.com
push dateTue, 08 Aug 2017 16:51:55 +0000
reviewersbsilverberg, janv
bugs1388428
milestone57.0a1
Bug 1388428 - Extend browsingData to restrict removing localStorage to a given list of hostnames; r=bsilverberg,janv MozReview-Commit-ID: 5TYPjyZusfd
browser/components/extensions/ext-browsingData.js
browser/components/extensions/schemas/browsing_data.json
browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
dom/storage/LocalStorageManager.cpp
dom/storage/StorageObserver.cpp
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -79,17 +79,22 @@ const clearFormData = options => {
   return sanitizer.items.formdata.clear(makeRange(options));
 };
 
 const clearHistory = options => {
   return sanitizer.items.history.clear(makeRange(options));
 };
 
 const clearLocalStorage = async function(options) {
-  Services.obs.notifyObservers(null, "extension:purge-localStorage");
+  if (options.since) {
+    return Promise.reject(
+      {message: "Firefox does not support clearing localStorage with 'since'."});
+  }
+  let hosts = (options.hostnames || []).join("^");
+  Services.obs.notifyObservers(null, "extension:purge-localStorage", hosts);
 };
 
 const clearPasswords = async function(options) {
   let loginManager = Services.logins;
   let yieldCounter = 0;
 
   if (options.since) {
     // Iterate through the logins and delete any updated after our cutoff.
--- a/browser/components/extensions/schemas/browsing_data.json
+++ b/browser/components/extensions/schemas/browsing_data.json
@@ -31,17 +31,17 @@
             "$ref": "extensionTypes.Date",
             "optional": true,
             "description": "Remove data accumulated on or after this date, represented in milliseconds since the epoch (accessible via the <code>getTime</code> method of the JavaScript <code>Date</code> object). If absent, defaults to 0 (which would remove all browsing data)."
           },
           "hostnames": {
             "type": "array",
             "items": {"type": "string", "format": "hostname"},
             "optional": true,
-            "description": "Only remove data associated with these hostnames (only applies to cookies)."
+            "description": "Only remove data associated with these hostnames (only applies to cookies and localStorage)."
           },
           "originTypes": {
             "type": "object",
             "optional": true,
             "description": "An object whose properties specify which origin types ought to be cleared. If this object isn't specified, it defaults to clearing only \"unprotected\" origins. Please ensure that you <em>really</em> want to remove application data before adding 'protectedWeb' or 'extensions'.",
             "properties": {
               "unprotectedWeb": {
                 "type": "boolean",
--- a/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
@@ -34,16 +34,27 @@ add_task(async function testLocalStorage
 
     function sendMessageToTabs(tabs, message) {
       return Promise.all(
         tabs.map(tab => { return browser.tabs.sendMessage(tab.id, message); }));
     }
 
     let tabs = await openTabs();
 
+    browser.test.assertRejects(
+      browser.browsingData.removeLocalStorage({since: Date.now()}),
+      "Firefox does not support clearing localStorage with 'since'.",
+      "Expected error received when using unimplemented parameter 'since'."
+    );
+
+    await sendMessageToTabs(tabs, "resetLocalStorage");
+    await browser.browsingData.removeLocalStorage({hostnames: ["example.com"]});
+    await browser.tabs.sendMessage(tabs[0].id, "checkLocalStorageCleared");
+    await browser.tabs.sendMessage(tabs[1].id, "checkLocalStorageSet");
+
     await sendMessageToTabs(tabs, "resetLocalStorage");
     await sendMessageToTabs(tabs, "checkLocalStorageSet");
     await browser.browsingData.removeLocalStorage({});
     await sendMessageToTabs(tabs, "checkLocalStorageCleared");
 
     await sendMessageToTabs(tabs, "resetLocalStorage");
     await sendMessageToTabs(tabs, "checkLocalStorageSet");
     await browser.browsingData.remove({}, {localStorage: true});
--- a/dom/storage/LocalStorageManager.cpp
+++ b/dom/storage/LocalStorageManager.cpp
@@ -342,37 +342,50 @@ NS_IMETHODIMP
 LocalStorageManager::GetLocalStorageForPrincipal(nsIPrincipal* aPrincipal,
                                                  const nsAString& aDocumentURI,
                                                  bool aPrivate,
                                                  nsIDOMStorage** aRetval)
 {
   return CreateStorage(nullptr, aPrincipal, aDocumentURI, aPrivate, aRetval);
 }
 
+inline bool
+OriginScopesContain(const nsACString& aString, const nsCSubstringSplitter& aScopes)
+{
+  for (const nsACString& scope : aScopes) {
+    if (StringBeginsWith(aString, scope)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 void
 LocalStorageManager::ClearCaches(uint32_t aUnloadFlags,
                                  const OriginAttributesPattern& aPattern,
-                                 const nsACString& aOriginScope)
+                                 const nsACString& aOriginScopes)
 {
+  nsCSubstringSplitter scopes = aOriginScopes.Split('^');
+
   for (auto iter1 = mCaches.Iter(); !iter1.Done(); iter1.Next()) {
     OriginAttributes oa;
     DebugOnly<bool> rv = oa.PopulateFromSuffix(iter1.Key());
     MOZ_ASSERT(rv);
     if (!aPattern.Matches(oa)) {
       // This table doesn't match the given origin attributes pattern
       continue;
     }
 
     CacheOriginHashtable* table = iter1.Data();
 
     for (auto iter2 = table->Iter(); !iter2.Done(); iter2.Next()) {
       LocalStorageCache* cache = iter2.Get()->cache();
 
-      if (aOriginScope.IsEmpty() ||
-          StringBeginsWith(cache->OriginNoSuffix(), aOriginScope)) {
+      if (aOriginScopes.IsEmpty() ||
+          OriginScopesContain(cache->OriginNoSuffix(), scopes)) {
         cache->UnloadItems(aUnloadFlags);
       }
     }
   }
 }
 
 nsresult
 LocalStorageManager::Observe(const char* aTopic,
@@ -383,17 +396,17 @@ LocalStorageManager::Observe(const char*
   if (!pattern.Init(aOriginAttributesPattern)) {
     NS_ERROR("Cannot parse origin attributes pattern");
     return NS_ERROR_FAILURE;
   }
 
   // Clear everything, caches + database
   if (!strcmp(aTopic, "cookie-cleared") ||
       !strcmp(aTopic, "extension:purge-localStorage-caches")) {
-    ClearCaches(LocalStorageCache::kUnloadComplete, pattern, EmptyCString());
+    ClearCaches(LocalStorageCache::kUnloadComplete, pattern, aOriginScope);
     return NS_OK;
   }
 
   // Clear from caches everything that has been stored
   // while in session-only mode
   if (!strcmp(aTopic, "session-only-cleared")) {
     ClearCaches(LocalStorageCache::kUnloadSession, pattern, aOriginScope);
     return NS_OK;
--- a/dom/storage/StorageObserver.cpp
+++ b/dom/storage/StorageObserver.cpp
@@ -140,16 +140,39 @@ StorageObserver::Notify(const char* aTop
                         const nsACString& aOriginScope)
 {
   for (uint32_t i = 0; i < mSinks.Length(); ++i) {
     StorageObserverSink* sink = mSinks[i];
     sink->Observe(aTopic, aOriginAttributesPattern, aOriginScope);
   }
 }
 
+nsresult
+GetOriginScopeForDomain(const nsACString& aDomain, nsACString& aOriginScope)
+{
+  // Convert the domain name to the ACE format
+  nsresult rv;
+  nsAutoCString aceDomain;
+  nsCOMPtr<nsIIDNService> converter = do_GetService(NS_IDNSERVICE_CONTRACTID);
+  if (converter) {
+    rv = converter->ConvertUTF8toACE(aDomain, aceDomain);
+    NS_ENSURE_SUCCESS(rv, rv);
+  } else {
+    // In case the IDN service is not available, this is the best we can come
+    // up with!
+    rv = NS_EscapeURL(aDomain,
+                      esc_OnlyNonASCII | esc_AlwaysCopy,
+                      aceDomain,
+                      fallible);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  return CreateReversedDomain(aceDomain, aOriginScope);
+}
+
 NS_IMETHODIMP
 StorageObserver::Observe(nsISupports* aSubject,
                          const char* aTopic,
                          const char16_t* aData)
 {
   nsresult rv;
 
   // Start the thread that opens the database.
@@ -249,47 +272,54 @@ StorageObserver::Observe(nsISupports* aS
 
     Notify("session-only-cleared", NS_ConvertUTF8toUTF16(originSuffix),
            originScope);
 
     return NS_OK;
   }
 
   if (!strcmp(aTopic, "extension:purge-localStorage")) {
-    StorageDBBridge* db = LocalStorageCache::StartDatabase();
-    NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
+    nsCString data = NS_ConvertUTF16toUTF8(aData);
+    if (data.IsEmpty()) {
+      StorageDBBridge* db = LocalStorageCache::StartDatabase();
+      NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
+
+      db->AsyncClearAll();
+    } else {
+      nsTArray<nsCString> originScopes;
+      nsAutoCString originScope;
+      for (const nsACString& domain : data.Split('^')) {
+        rv = GetOriginScopeForDomain(domain, originScope);
+        NS_ENSURE_SUCCESS(rv, rv);
+        originScopes.AppendElement(originScope);
+      }
 
-    db->AsyncClearAll();
+      StorageDBBridge* db = LocalStorageCache::StartDatabase();
+      NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
 
-    Notify("extension:purge-localStorage-caches");
+      data = EmptyCString();
+      for (uint32_t i=0; i<originScopes.Length(); ++i) {
+        if (i > 0) {
+          data += "^";
+        }
+        db->AsyncClearMatchingOrigin(originScopes[i]);
+        data += originScopes[i];
+      }
+    }
+
+    Notify("extension:purge-localStorage-caches", EmptyString(), data);
 
     return NS_OK;
   }
 
   // Clear everything (including so and pb data) from caches and database
   // for the gived domain and subdomains.
   if (!strcmp(aTopic, "browser:purge-domain-data")) {
-    // Convert the domain name to the ACE format
-    nsAutoCString aceDomain;
-    nsCOMPtr<nsIIDNService> converter = do_GetService(NS_IDNSERVICE_CONTRACTID);
-    if (converter) {
-      rv = converter->ConvertUTF8toACE(NS_ConvertUTF16toUTF8(aData), aceDomain);
-      NS_ENSURE_SUCCESS(rv, rv);
-    } else {
-      // In case the IDN service is not available, this is the best we can come
-      // up with!
-      rv = NS_EscapeURL(NS_ConvertUTF16toUTF8(aData),
-                        esc_OnlyNonASCII | esc_AlwaysCopy,
-                        aceDomain,
-                        fallible);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
-
     nsAutoCString originScope;
-    rv = CreateReversedDomain(aceDomain, originScope);
+    rv = GetOriginScopeForDomain(NS_ConvertUTF16toUTF8(aData), originScope);
     NS_ENSURE_SUCCESS(rv, rv);
 
     StorageDBBridge* db = LocalStorageCache::StartDatabase();
     NS_ENSURE_TRUE(db, NS_ERROR_FAILURE);
 
     db->AsyncClearMatchingOrigin(originScope);
 
     Notify("domain-data-cleared", EmptyString(), originScope);