Bug 1363001 - Implement browsingData.removeDownloads WebExtension API method on android. r=bsilverberg, JanH
MozReview-Commit-ID: FjjnfYjsJez
--- a/mobile/android/components/extensions/ext-browsingData.js
+++ b/mobile/android/components/extensions/ext-browsingData.js
@@ -89,12 +89,15 @@ this.browsingData = class extends Extens
return Promise.resolve({options: {since: 0}, dataToRemove, dataRemovalPermitted});
},
removeCookies(options) {
return clearCookies(options);
},
removeCache(options) {
return Sanitizer.clearItem("cache");
},
+ removeDownloads(options) {
+ return Sanitizer.clearItem("downloadHistory", options.since);
+ },
},
};
}
};
--- a/mobile/android/components/extensions/schemas/browsing_data.json
+++ b/mobile/android/components/extensions/schemas/browsing_data.json
@@ -240,17 +240,16 @@
}
]
},
{
"name": "removeDownloads",
"description": "Clears the browser's list of downloaded files (<em>not</em> the downloaded files themselves).",
"type": "function",
"async": "callback",
- "unsupported": true,
"parameters": [
{
"$ref": "RemovalOptions",
"name": "options"
},
{
"name": "callback",
"type": "function",
--- a/mobile/android/components/extensions/test/mochitest/chrome.ini
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -2,13 +2,14 @@
support-files =
head.js
../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js
tags = webextensions
[test_ext_browserAction_getTitle_setTitle.html]
[test_ext_browserAction_onClicked.html]
[test_ext_browsingData_cookies_cache.html]
+[test_ext_browsingData_downloads.html]
[test_ext_browsingData_settings.html]
[test_ext_options_ui.html]
[test_ext_pageAction_show_hide.html]
[test_ext_pageAction_getPopup_setPopup.html]
skip-if = os == 'android' # bug 1373170
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_browsingData_downloads.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>BrowsingData Settings test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+var {Downloads} = Cu.import("resource://gre/modules/Downloads.jsm", {});
+
+const OLD_NAMES = {[Downloads.PUBLIC]: "old-public", [Downloads.PRIVATE]: "old-private"};
+const RECENT_NAMES = {[Downloads.PUBLIC]: "recent-public", [Downloads.PRIVATE]: "recent-private"};
+const REFERENCE_DATE = new Date();
+const OLD_DATE = new Date(Number(REFERENCE_DATE) - 10000);
+
+async function downloadExists(list, path) {
+ let listArray = await list.getAll();
+ return listArray.some(i => i.target.path == path);
+}
+
+async function checkDownloads(expectOldExists = true, expectRecentExists = true) {
+ for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+ let downloadsList = await Downloads.getList(listType);
+ is(
+ (await downloadExists(downloadsList, OLD_NAMES[listType])),
+ expectOldExists,
+ `Fake old download ${(expectOldExists) ? "was found" : "was removed"}.`);
+ is(
+ (await downloadExists(downloadsList, RECENT_NAMES[listType])),
+ expectRecentExists,
+ `Fake recent download ${(expectRecentExists) ? "was found" : "was removed"}.`);
+ }
+}
+
+async function setupDownloads() {
+ let downloadsList = await Downloads.getList(Downloads.ALL);
+ await downloadsList.removeFinished();
+
+ for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+ downloadsList = await Downloads.getList(listType);
+ let download = await Downloads.createDownload({
+ source: {
+ url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1363001",
+ isPrivate: listType == Downloads.PRIVATE},
+ target: OLD_NAMES[listType],
+ });
+ download.startTime = OLD_DATE;
+ download.canceled = true;
+ await downloadsList.add(download);
+
+ download = await Downloads.createDownload({
+ source: {
+ url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1363001",
+ isPrivate: listType == Downloads.PRIVATE},
+ target: RECENT_NAMES[listType],
+ });
+ download.startTime = REFERENCE_DATE;
+ download.canceled = true;
+ await downloadsList.add(download);
+ }
+
+ // Confirm everything worked.
+ downloadsList = await Downloads.getList(Downloads.ALL);
+ is((await downloadsList.getAll()).length, 4, "4 fake downloads added.");
+ checkDownloads();
+}
+
+add_task(async function testDownloads() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ await browser.browsingData.removeDownloads(options);
+ browser.test.sendMessage("downloadsRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Clear downloads with no since value.
+ await setupDownloads();
+ extension.sendMessage(method, {});
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(false, false);
+
+ // Clear downloads with recent since value.
+ await setupDownloads();
+ extension.sendMessage(method, {since: REFERENCE_DATE});
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(true, false);
+
+ // Clear downloads with old since value.
+ await setupDownloads();
+ extension.sendMessage(method, {since: REFERENCE_DATE - 100000});
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(false, false);
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removeDownloads");
+
+ await extension.unload();
+});
+
+</script>
+</body>
+</html>
--- a/mobile/android/modules/Sanitizer.jsm
+++ b/mobile/android/modules/Sanitizer.jsm
@@ -28,26 +28,44 @@ XPCOMUtils.defineLazyModuleGetter(this,
function dump(a) {
Services.console.logStringMessage(a);
}
this.EXPORTED_SYMBOLS = ["Sanitizer"];
function Sanitizer() {}
Sanitizer.prototype = {
- clearItem: function(aItemName) {
+ clearItem: function(aItemName, startTime) {
+ // Only a subset of items support deletion with startTime.
+ // Those who do not will be rejected with error message.
+ if (typeof startTime != "undefined") {
+ switch (aItemName) {
+ // Normal call to DownloadFiles remove actual data from storage, but our web-extension consumer
+ // deletes only download history. So, for this reason we are passing a flag 'deleteFiles'.
+ case "downloadHistory":
+ this._clear("downloadFiles", { startTime, deleteFiles: false });
+ break;
+ default:
+ return Promise.reject({message: `Invalid argument: ${aItemName} does not support startTime argument.`});
+ }
+ } else {
+ this._clear(aItemName);
+ }
+ },
+
+ _clear: function(aItemName, options) {
let item = this.items[aItemName];
let canClear = item.canClear;
if (typeof canClear == "function") {
canClear(function clearCallback(aCanClear) {
if (aCanClear)
- return item.clear();
+ return item.clear(options);
});
} else if (canClear) {
- return item.clear();
+ return item.clear(options);
}
},
items: {
cache: {
clear: function() {
return new Promise(function(resolve, reject) {
let refObj = {};
@@ -228,47 +246,51 @@ Sanitizer.prototype = {
handleError: function(aError) { Cu.reportError(aError); },
handleCompletion: function(aReason) { aCallback(aReason == 0 && count > 0); }
};
FormHistory.count({}, countDone);
}
},
downloadFiles: {
- clear: Task.async(function* () {
+ clear: Task.async(function* ({ startTime = 0, deleteFiles = true} = {}) {
let refObj = {};
TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
let list = yield Downloads.getList(Downloads.ALL);
let downloads = yield list.getAll();
var finalizePromises = [];
// Logic copied from DownloadList.removeFinished. Ideally, we would
// just use that method directly, but we want to be able to remove the
// downloaded files as well.
for (let download of downloads) {
// Remove downloads that have been canceled, even if the cancellation
// operation hasn't completed yet so we don't check "stopped" here.
- // Failed downloads with partial data are also removed.
- if (download.stopped && (!download.hasPartialData || download.error)) {
+ // Failed downloads with partial data are also removed. The startTime
+ // check is provided for addons that may want to delete only recent downloads.
+ if (download.stopped && (!download.hasPartialData || download.error) &&
+ download.startTime.getTime() >= startTime) {
// Remove the download first, so that the views don't get the change
// notifications that may occur during finalization.
yield list.remove(download);
// Ensure that the download is stopped and no partial data is kept.
// This works even if the download state has changed meanwhile. We
// don't need to wait for the procedure to be complete before
// processing the other downloads in the list.
finalizePromises.push(download.finalize(true).then(() => null, Cu.reportError));
- // Delete the downloaded files themselves.
- OS.File.remove(download.target.path).then(() => null, ex => {
- if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
- Cu.reportError(ex);
- }
- });
+ if (deleteFiles) {
+ // Delete the downloaded files themselves.
+ OS.File.remove(download.target.path).then(() => null, ex => {
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ Cu.reportError(ex);
+ }
+ });
+ }
}
}
yield Promise.all(finalizePromises);
yield DownloadIntegration.forceSave();
TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
}),