Bug 1333050 - add support for browsingData.removeIndexedDB and remove(indexedDB); r?bsilverberg, baku
MozReview-Commit-ID: AnWIeVwildA
--- 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();
+});
+