Bug 1363001 - Implement browsingData.removeDownloads WebExtension API method on android. r?grisha, bsilverberg draft
authorTushar Saini (:shatur) <tushar.saini1285@gmail.com>
Sun, 18 Jun 2017 01:29:48 +0530
changeset 609586 b53b372acd00a321709fbeed2402b2fa7aba2e03
parent 609584 01f2fca93f48cfcbae58b15e7ca75cf8fe005a26
child 637579 c9bcd3cdbfe12701667ed8dd26206518d179e622
push id68587
push userbmo:tushar.saini1285@gmail.com
push dateSun, 16 Jul 2017 20:53:38 +0000
reviewersgrisha, bsilverberg
bugs1363001
milestone56.0a1
Bug 1363001 - Implement browsingData.removeDownloads WebExtension API method on android. r?grisha, bsilverberg 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
@@ -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, "Sanitizer",
+                                  "resource://gre/modules/Sanitizer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences",
                                   "resource://gre/modules/SharedPreferences.jsm");
 
 this.browsingData = class extends ExtensionAPI {
   getAPI(context) {
     return {
@@ -49,12 +51,16 @@ this.browsingData = class extends Extens
             dataToRemove[name] = dataTrue.indexOf(`${PREF_KEY_PREFIX}${item}`) > -1;
             // Firefox doesn't have the same concept of dataRemovalPermitted
             // as Chrome, so it will always be true.
             dataRemovalPermitted[name] = true;
           }
 
           return Promise.resolve({options, dataToRemove, dataRemovalPermitted});
         },
+
+        async removeDownloads(options) {
+          return Sanitizer.clearItem("downloadHistory", options.since);
+        },
       },
     };
   }
 };
\ No newline at end of file
--- a/mobile/android/components/extensions/schemas/browsing_data.json
+++ b/mobile/android/components/extensions/schemas/browsing_data.json
@@ -242,17 +242,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
@@ -1,11 +1,12 @@
 [DEFAULT]
 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_downloads.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,122 @@
+<!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,27 +28,43 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 function dump(a) {
   Services.console.logStringMessage(a);
 }
 
 this.EXPORTED_SYMBOLS = ["Sanitizer"];
 
 function Sanitizer() {}
 Sanitizer.prototype = {
-  clearItem: function (aItemName)
+  _clear: function(aItemName, startTime)
   {
     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(startTime);
       });
     } else if (canClear) {
-      return item.clear();
+      return item.clear(startTime);
+    }
+  },
+
+  clearItem: function (aItemName, startTime)
+  {
+    // Only a subset of items support deletion with startTime.
+    if (typeof startTime != "undefined") {
+      switch (aItemName) {
+        case 'downloadHistory':
+          this._clear(aItemName, startTime);
+          break;
+        default:
+          this._clear(aItemName);
+      }
+    } else {
+      this._clear(aItemName);
     }
   },
 
   items: {
     cache: {
       clear: function ()
       {
         return new Promise(function(resolve, reject) {
@@ -245,16 +261,50 @@ Sanitizer.prototype = {
           handleResult: function(aResult) { count = aResult; },
           handleError: function(aError) { Cu.reportError(aError); },
           handleCompletion: function(aReason) { aCallback(aReason == 0 && count > 0); }
         };
         FormHistory.count({}, countDone);
       }
     },
 
+    downloadHistory: {
+      clear: Task.async(function* (startTime) {
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
+
+        // Initialize startTime to 0, if it is undefined
+        if (typeof startTime == "undefined")
+          startTime = 0;
+
+        let list = yield Downloads.getList(Downloads.ALL);
+        let downloads = yield list.getAll();
+        var finalizePromises = [];
+
+        for (let download of downloads) {
+          // Delete history items visisted after startTime
+          if (download.startTime.getTime() >= startTime) {
+            if (download.stopped && (!download.hasPartialData || download.error)) {
+              yield list.remove(download);
+              finalizePromises.push(download.finalize(true).then(() => null, Cu.reportError));
+            }
+          }
+        }
+
+        yield Promise.all(finalizePromises);
+        yield DownloadIntegration.forceSave();
+        TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
+      }),
+
+      get canClear()
+      {
+        return true;
+      }
+    },
+
     downloadFiles: {
       clear: Task.async(function* () {
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
 
         let list = yield Downloads.getList(Downloads.ALL);
         let downloads = yield list.getAll();
         var finalizePromises = [];