Bug 1363001 - Implement browsingData.removeDownloads WebExtension API method on android. r=bsilverberg, JanH draft
authorTushar Saini (:shatur) <tushar.saini1285@gmail.com>
Mon, 14 Aug 2017 00:25:42 +0530
changeset 654242 7d57d1a157d3493be2519fed05b5d1fb99191f10
parent 650074 a9d372645a32b8d23d44244f351639af9d73b96a
child 728515 9547b7f5eb35a4ec25a13467330ae7844c82de26
push id76518
push userbmo:tushar.saini1285@gmail.com
push dateMon, 28 Aug 2017 13:00:20 +0000
reviewersbsilverberg, JanH
bugs1363001
milestone57.0a1
Bug 1363001 - Implement browsingData.removeDownloads WebExtension API method on android. r=bsilverberg, JanH MozReview-Commit-ID: FjjnfYjsJez
mobile/android/components/extensions/ext-browsingData.js
mobile/android/components/extensions/schemas/browsing_data.json
mobile/android/components/extensions/test/mochitest/chrome.ini
mobile/android/components/extensions/test/mochitest/test_ext_browsingData_downloads.html
mobile/android/modules/Sanitizer.jsm
--- 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);
       }),