Bug 1333050 - add support for browsingData.removeIndexedDB and remove(indexedDB); r?bsilverberg, baku draft
authorThomas Wisniewski <wisniewskit@gmail.com>
Tue, 11 Apr 2017 15:52:58 -0400
changeset 560679 30f93687468ef4569a47825ca2c2ff662395f827
parent 560546 abf145ebd05fe105efbc78b761858c34f7690154
child 623783 0cba277d2b2dfa04025a48e02aefe9d6168340a0
push id53511
push userwisniewskit@gmail.com
push dateTue, 11 Apr 2017 19:55:50 +0000
reviewersbsilverberg, baku
bugs1333050
milestone55.0a1
Bug 1333050 - add support for browsingData.removeIndexedDB and remove(indexedDB); r?bsilverberg, baku MozReview-Commit-ID: AnWIeVwildA
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_indexedDB.js
--- a/browser/components/extensions/ext-browsingData.js
+++ b/browser/components/extensions/ext-browsingData.js
@@ -1,14 +1,16 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.import("resource://gre/modules/Task.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
                                   "resource:///modules/Sanitizer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
@@ -78,16 +80,86 @@ function clearDownloads(options) {
 function clearFormData(options) {
   return sanitizer.items.formdata.clear(makeRange(options));
 }
 
 function clearHistory(options) {
   return sanitizer.items.history.clear(makeRange(options));
 }
 
+let directoryIterator = Task.async(function* (path, callback) {
+  let iter = new OS.File.DirectoryIterator(path);
+  try {
+    while (true) {
+      let entry = yield iter.next();
+      yield callback(entry);
+    }
+  } catch (e) {
+    iter.close();
+    if (e !== StopIteration) {
+      throw e;
+    }
+  }
+});
+
+let AllStorageHostsIterator = Task.async(function* (callback) {
+  // expect web-content storage paths to look something like this:
+  // - PathToProfileDir/storage/default/http+++www.example.com/
+  // - PathToProfileDir/storage/permanent/http+++www.example.com/
+  // - PathToProfileDir/storage/temporary/http+++www.example.com/
+  let profileDir = OS.Constants.Path.profileDir;
+  let storagePath = OS.Path.join(profileDir, "storage");
+  yield directoryIterator(storagePath, function* (storageTypePath) {
+    yield directoryIterator(storageTypePath.path, function* (hostPath) {
+      if (hostPath.isDir && hostPath.name.startsWith("http")) {
+        yield callback(hostPath);
+      }
+    });
+  });
+});
+
+let clearIndexedDBs = Task.async(function* (options) {
+  let yieldCounter = 0;
+  let ssm = Services.scriptSecurityManager;
+  let qms = Services.qms;
+
+  yield AllStorageHostsIterator(function* (hostPath) {
+    let idbPath = OS.Path.join(hostPath.path, "idb");
+    let idbStat = yield OS.File.stat(idbPath);
+    if (idbStat.isDir) {
+      let shouldClear = false;
+      if (options.since) {
+        // Clear if the idb directory itself was accessed since.
+        if (idbStat.lastAccessDate.getTime() >= options.since) {
+          shouldClear = true;
+        } else {
+          // Also clear if any of its files/subdirectories where accessed since.
+          yield directoryIterator(idbPath, function* (file) {
+            let time = (yield OS.File.stat(file.path)).lastAccessDate.getTime();
+            if (time >= options.since) {
+              shouldClear = true;
+              throw StopIteration;
+            }
+          });
+        }
+      } else {
+        shouldClear = true;
+      }
+      if (shouldClear) {
+        let actualHost = hostPath.name.replace("+++", "://");
+        let principal = ssm.createCodebasePrincipalFromOrigin(actualHost);
+        qms.clearStoragesForPrincipal(principal);
+      }
+    }
+    if (++yieldCounter % YIELD_PERIOD == 0) {
+      yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+    }
+  });
+});
+
 let clearPasswords = Task.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) {
@@ -147,16 +219,19 @@ function doRemoval(options, dataToRemove
           removalPromises.push(clearDownloads(options));
           break;
         case "formData":
           removalPromises.push(clearFormData(options));
           break;
         case "history":
           removalPromises.push(clearHistory(options));
           break;
+        case "indexedDB":
+          removalPromises.push(clearIndexedDBs(options));
+          break;
         case "passwords":
           removalPromises.push(clearPasswords(options));
           break;
         case "pluginData":
           removalPromises.push(clearPluginData(options));
           break;
         case "serviceWorkers":
           removalPromises.push(clearServiceWorkers());
@@ -220,16 +295,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});
         },
+        removeIndexedDB(options) {
+          return doRemoval(options, {indexedDB: 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
@@ -310,17 +310,16 @@
           }
         ]
       },
       {
         "name": "removeIndexedDB",
         "description": "Clears websites' IndexedDB 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
@@ -34,16 +34,17 @@ support-files =
 [browser_ext_browserAction_pageAction_icon.js]
 [browser_ext_browserAction_pageAction_icon_permissions.js]
 [browser_ext_browserAction_popup.js]
 [browser_ext_browserAction_popup_preload.js]
 [browser_ext_browserAction_popup_resize.js]
 [browser_ext_browserAction_simple.js]
 [browser_ext_browsingData_formData.js]
 [browser_ext_browsingData_history.js]
+[browser_ext_browsingData_indexedDB.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_indexedDB.js
@@ -0,0 +1,122 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function setupIndexedDB(browser, name) {
+  return ContentTask.spawn(browser, {name: name}, function* (args) {
+    let request = content.indexedDB.open(args.name, 1);
+
+    request.onerror = function() {
+      throw new Error("Error opening DB connection.");
+    };
+
+    request.onupgradeneeded = event => {
+      let db = event.target.result;
+      db.createObjectStore("obj", {keyPath: "id"});
+    };
+
+    let db = yield new Promise(resolve => {
+      request.onsuccess = event => {
+        resolve(request.result);
+      };
+    });
+
+    db.close();
+  });
+}
+
+function checkDBExists(browser, name) {
+  return ContentTask.spawn(browser, {name: name}, function* (args) {
+    let request = content.indexedDB.open(args.name, 1);
+    return yield new Promise(done => {
+      request.onupgradeneeded = function() {
+        done(false);
+      };
+      request.onsuccess = event => {
+        event.target.result.close();
+        done(true);
+      };
+    });
+  });
+}
+
+async function setAccessTimeOfAllIDBFilesForHost(host, time) {
+  let profileDir = OS.Constants.Path.profileDir;
+  let storageDir = OS.Path.join(profileDir, "storage");
+  let storageTypeDir = OS.Path.join(storageDir, "default");
+  let hostDir = OS.Path.join(storageTypeDir, host.replace("://", "+++"));
+  let idbDir = OS.Path.join(hostDir, "idb");
+  await OS.File.setDates(idbDir, time, time);
+  let iter = new OS.File.DirectoryIterator(idbDir);
+  try {
+    while (true) {
+      let file = await iter.next();
+      await OS.File.setDates(file.path, time, time);
+    }
+  } catch (_) {
+    iter.close();
+  }
+}
+
+add_task(async function testIndexedDB() {
+  function background() {
+    browser.test.onMessage.addListener(async (msg, options) => {
+      if (msg == "removeDB") {
+        await browser.browsingData.removeIndexedDB(options);
+      } else {
+        await browser.browsingData.remove(options, {indexedDB: true});
+      }
+      browser.test.sendMessage("dbsRemoved");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["browsingData"],
+    },
+  });
+
+  async function testRemovalMethod(method) {
+    // Setup DBs on two hosts.
+    const host1 = "http://example.com";
+    let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, host1);
+    let browser1 = gBrowser.getBrowserForTab(tab1);
+
+    const host2 = "http://example.net";
+    let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, host2);
+    let browser2 = gBrowser.getBrowserForTab(tab2);
+
+    await setupIndexedDB(browser1, "db1");
+    await setupIndexedDB(browser2, "db2");
+
+    // Set the access time of the the first host's IDB files to an old value,
+    // since the resolution of OS.File.stat().lastAccessTime can be very coarse.
+    await setAccessTimeOfAllIDBFilesForHost(host1, 1000);
+
+    // Clear DBs with a since value.
+    extension.sendMessage(method, {since: 200000});
+    await extension.awaitMessage("dbsRemoved");
+
+    is(await checkDBExists(browser1, "db1"), true);
+    is(await checkDBExists(browser2, "db2"), false);
+
+    // Clear dbs without a since value.
+    extension.sendMessage(method, {});
+    await extension.awaitMessage("dbsRemoved");
+
+    is(await checkDBExists(browser1, "db1"), false);
+    is(await checkDBExists(browser2, "db2"), false);
+
+    await BrowserTestUtils.removeTab(tab1);
+    await BrowserTestUtils.removeTab(tab2);
+  }
+
+  await extension.startup();
+
+  await testRemovalMethod("removeIndexedDB");
+  await testRemovalMethod("remove");
+
+  await extension.unload();
+});
+