Bug 1355576 - Add ability to clear all localStorage with the browsingData API; r?bsilverberg, janv draft
authorThomas Wisniewski <wisniewskit@gmail.com>
Tue, 04 Jul 2017 20:59:26 -0400
changeset 641400 5e711791e719be18880c592898e50cbdddd34f88
parent 603793 acbd9a64c0fa4e1980c7732735d4f4c2761ca4c9
child 724774 20f1f214338a67409122e2fbb0225a8aae93ba61
push id72506
push userwisniewskit@gmail.com
push dateSun, 06 Aug 2017 22:51:49 +0000
reviewersbsilverberg, janv
bugs1355576
milestone56.0a1
Bug 1355576 - Add ability to clear all localStorage with the browsingData API; r?bsilverberg, janv MozReview-Commit-ID: 4UUqg62yIo9
browser/components/extensions/ext-browsingData.js
browser/components/extensions/schemas/browsing_data.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
browser/components/extensions/test/xpcshell/test_ext_browsingData.js
dom/storage/LocalStorageManager.cpp
dom/storage/StorageObserver.cpp
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -77,16 +77,20 @@ const clearDownloads = options => {
 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");
+};
+
 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.
     let logins = loginManager.getAllLogins();
     for (let login of logins) {
@@ -146,16 +150,19 @@ const doRemoval = (options, dataToRemove
           removalPromises.push(clearDownloads(options));
           break;
         case "formData":
           removalPromises.push(clearFormData(options));
           break;
         case "history":
           removalPromises.push(clearHistory(options));
           break;
+        case "localStorage":
+          removalPromises.push(clearLocalStorage(options));
+          break;
         case "passwords":
           removalPromises.push(clearPasswords(options));
           break;
         case "pluginData":
           removalPromises.push(clearPluginData(options));
           break;
         case "serviceWorkers":
           removalPromises.push(clearServiceWorkers());
@@ -219,16 +226,19 @@ this.browsingData = class extends Extens
           return doRemoval(options, {downloads: true});
         },
         removeFormData(options) {
           return doRemoval(options, {formData: true});
         },
         removeHistory(options) {
           return doRemoval(options, {history: true});
         },
+        removeLocalStorage(options) {
+          return doRemoval(options, {localStorage: true});
+        },
         removePasswords(options) {
           return doRemoval(options, {passwords: true});
         },
         removePluginData(options) {
           return doRemoval(options, {pluginData: true});
         },
       },
     };
--- a/browser/components/extensions/schemas/browsing_data.json
+++ b/browser/components/extensions/schemas/browsing_data.json
@@ -330,17 +330,16 @@
           }
         ]
       },
       {
         "name": "removeLocalStorage",
         "description": "Clears websites' local storage data.",
         "type": "function",
         "async": "callback",
-        "unsupported": true,
         "parameters": [
           {
             "$ref": "RemovalOptions",
             "name": "options"
           },
           {
             "name": "callback",
             "type": "function",
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -39,16 +39,17 @@ support-files =
 [browser_ext_browserAction_popup_preload.js]
 skip-if = (os == 'win' && !debug) # bug 1352668
 [browser_ext_browserAction_popup_resize.js]
 [browser_ext_browserAction_simple.js]
 [browser_ext_browserAction_telemetry.js]
 [browser_ext_browserAction_theme_icons.js]
 [browser_ext_browsingData_formData.js]
 [browser_ext_browsingData_history.js]
+[browser_ext_browsingData_localStorage.js]
 [browser_ext_browsingData_pluginData.js]
 [browser_ext_browsingData_serviceWorkers.js]
 [browser_ext_commands_execute_browser_action.js]
 [browser_ext_commands_execute_page_action.js]
 [browser_ext_commands_execute_sidebar_action.js]
 [browser_ext_commands_getAll.js]
 [browser_ext_commands_onCommand.js]
 [browser_ext_contentscript_connect.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_browsingData_localStorage.js
@@ -0,0 +1,93 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testLocalStorage() {
+  async function background() {
+    function openTabs() {
+      let promise = new Promise(resolve => {
+        let tabURLs = [
+          "http://example.com/",
+          "http://example.net/",
+        ];
+
+        let tabs;
+        let waitingCount = tabURLs.length;
+
+        let listener = async msg => {
+          if (msg !== "content-script-ready" || --waitingCount) {
+            return;
+          }
+          browser.runtime.onMessage.removeListener(listener);
+          resolve(Promise.all(tabs));
+        };
+
+        browser.runtime.onMessage.addListener(listener);
+
+        tabs = tabURLs.map(url => {
+          return browser.tabs.create({url: url});
+        });
+      });
+
+      return promise;
+    }
+
+    function sendMessageToTabs(tabs, message) {
+      return Promise.all(
+        tabs.map(tab => { return browser.tabs.sendMessage(tab.id, message); }));
+    }
+
+    let tabs = await openTabs();
+
+    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});
+    await sendMessageToTabs(tabs, "checkLocalStorageCleared");
+
+    browser.tabs.remove(tabs.map(tab => tab.id));
+
+    browser.test.notifyPass("done");
+  }
+
+  function contentScript() {
+    browser.runtime.onMessage.addListener(msg => {
+      if (msg === "resetLocalStorage") {
+        localStorage.clear();
+        localStorage.setItem("test", "test");
+      } else if (msg === "checkLocalStorageSet") {
+        browser.test.assertEq("test", localStorage.getItem("test"));
+      } else if (msg === "checkLocalStorageCleared") {
+        browser.test.assertEq(null, localStorage.getItem("test"));
+      }
+    });
+    browser.runtime.sendMessage("content-script-ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      "permissions": ["browsingData"],
+      "content_scripts": [{
+        "matches": [
+          "http://example.com/",
+          "http://example.net/",
+        ],
+        "js": ["content-script.js"],
+        "run_at": "document_start",
+      }],
+    },
+    files: {
+      "content-script.js": contentScript,
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("done");
+  await extension.unload();
+});
+
--- a/browser/components/extensions/test/xpcshell/test_ext_browsingData.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData.js
@@ -39,28 +39,28 @@ add_task(async function testInvalidArgum
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   await extension.startup();
   await extension.awaitFinish("invalidArguments");
   await extension.unload();
 });
 
 add_task(async function testUnimplementedDataType() {
   function background() {
-    browser.browsingData.remove({}, {localStorage: true});
+    browser.browsingData.remove({}, {indexedDB: true});
     browser.test.sendMessage("finished");
   }
 
   let {messages} = await promiseConsoleOutput(async function() {
     let extension = ExtensionTestUtils.loadExtension({
       background: background,
       manifest: {
         permissions: ["browsingData"],
       },
     });
 
     await extension.startup();
     await extension.awaitMessage("finished");
     await extension.unload();
   });
 
-  let warningObserved = messages.find(line => /Firefox does not support dataTypes: localStorage/.test(line));
+  let warningObserved = messages.find(line => /Firefox does not support dataTypes: indexedDB/.test(line));
   ok(warningObserved, "Warning issued when calling remove with an unimplemented dataType.");
 });
--- a/dom/storage/LocalStorageManager.cpp
+++ b/dom/storage/LocalStorageManager.cpp
@@ -381,17 +381,18 @@ LocalStorageManager::Observe(const char*
 {
   OriginAttributesPattern pattern;
   if (!pattern.Init(aOriginAttributesPattern)) {
     NS_ERROR("Cannot parse origin attributes pattern");
     return NS_ERROR_FAILURE;
   }
 
   // Clear everything, caches + database
-  if (!strcmp(aTopic, "cookie-cleared")) {
+  if (!strcmp(aTopic, "cookie-cleared") ||
+      !strcmp(aTopic, "extension:purge-localStorage-caches")) {
     ClearCaches(LocalStorageCache::kUnloadComplete, pattern, EmptyCString());
     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);
--- a/dom/storage/StorageObserver.cpp
+++ b/dom/storage/StorageObserver.cpp
@@ -61,16 +61,17 @@ StorageObserver::Init()
 
   // Chrome clear operations.
   obs->AddObserver(sSelf, kStartupTopic, true);
   obs->AddObserver(sSelf, "cookie-changed", true);
   obs->AddObserver(sSelf, "perm-changed", true);
   obs->AddObserver(sSelf, "browser:purge-domain-data", true);
   obs->AddObserver(sSelf, "last-pb-context-exited", true);
   obs->AddObserver(sSelf, "clear-origin-attributes-data", true);
+  obs->AddObserver(sSelf, "extension:purge-localStorage", true);
 
   // Shutdown
   obs->AddObserver(sSelf, "profile-after-change", true);
   obs->AddObserver(sSelf, "profile-before-change", true);
   obs->AddObserver(sSelf, "xpcom-shutdown", true);
 
   // Observe low device storage notifications.
   obs->AddObserver(sSelf, "disk-space-watcher", true);
@@ -247,16 +248,27 @@ StorageObserver::Observe(nsISupports* aS
     NS_ENSURE_SUCCESS(rv, rv);
 
     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);
+
+    db->AsyncClearAll();
+
+    Notify("extension:purge-localStorage-caches");
+
+    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);