Bug 1422365 - Introduce nsIClearDataService - part 4 - download, r=johannh
authorAndrea Marchesini <amarchesini@mozilla.com>
Fri, 01 Jun 2018 14:30:00 +0200
changeset 420873 bb116c8d9d2cb9cdfdf8bf46b7b9cbcc931951bc
parent 420872 ef06c41bff1df36828cb9d31f5d0cd6e24a10ba2
child 420874 7f2aed939175cff35ca48725282f23a365d24593
push id34083
push userapavel@mozilla.com
push dateSat, 02 Jun 2018 23:03:25 +0000
treeherdermozilla-central@1f62ecdf59b6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjohannh
bugs1422365
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1422365 - Introduce nsIClearDataService - part 4 - download, r=johannh
browser/modules/Sanitizer.jsm
toolkit/components/cleardata/ClearDataService.js
toolkit/components/cleardata/nsIClearDataService.idl
toolkit/components/cleardata/tests/unit/head.js
toolkit/components/cleardata/tests/unit/test_downloads.js
toolkit/components/cleardata/tests/unit/xpcshell.ini
toolkit/forgetaboutsite/ForgetAboutSite.jsm
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -7,17 +7,16 @@ var EXPORTED_SYMBOLS = ["Sanitizer"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   FormHistory: "resource://gre/modules/FormHistory.jsm",
-  Downloads: "resource://gre/modules/Downloads.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
   OfflineAppCacheHelper: "resource://gre/modules/offlineAppCache.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "sas",
                                    "@mozilla.org/storage/activity-service;1",
@@ -516,32 +515,18 @@ var Sanitizer = {
         }
       }
     },
 
     downloads: {
       async clear(range) {
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
-        try {
-          let filterByTime = null;
-          if (range) {
-            // Convert microseconds back to milliseconds for date comparisons.
-            let rangeBeginMs = range[0] / 1000;
-            let rangeEndMs = range[1] / 1000;
-            filterByTime = download => download.startTime >= rangeBeginMs &&
-                                       download.startTime <= rangeEndMs;
-          }
-
-          // Clear all completed/cancelled downloads
-          let list = await Downloads.getList(Downloads.ALL);
-          list.removeFinished(filterByTime);
-        } finally {
-          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
-        }
+        await clearData(range, Ci.nsIClearDataService.CLEAR_DOWNLOADS);
+        TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
       }
     },
 
     sessions: {
       async clear(range) {
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj);
 
--- a/toolkit/components/cleardata/ClearDataService.js
+++ b/toolkit/components/cleardata/ClearDataService.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
+ChromeUtils.import("resource://gre/modules/Downloads.jsm");
 
 // A Cleaner is an object with 3 methods. These methods must return a Promise
 // object. Here a description of these methods:
 // * deleteAll() - this method _must_ exist. When called, it deletes all the
 //                 data owned by the cleaner.
 // * deleteByHost() - this method is implemented only if the cleaner knows
 //                    how to delete data by host + originAttributes pattern. If
 //                    not implemented, deleteAll() will be used as fallback.
@@ -166,29 +167,58 @@ const PluginDataCleaner = {
     // elapsed, we proceed with the shutdown of Firefox.
     return Promise.race([
       Promise.all(promises),
       new Promise(aResolve => setTimeout(aResolve, 10000 /* 10 seconds */))
     ]);
   },
 };
 
+const DownloadsCleaner = {
+  deleteByHost(aHost, aOriginAttributes) {
+    return Downloads.getList(Downloads.ALL).then(aList => {
+      aList.removeFinished(aDownload => hasRootDomain(
+        Services.io.newURI(aDownload.source.url).host, aHost));
+    });
+  },
+
+  deleteByRange(aFrom, aTo) {
+    // Convert microseconds back to milliseconds for date comparisons.
+    let rangeBeginMs = aFrom / 1000;
+    let rangeEndMs = aTo / 1000;
+
+    return Downloads.getList(Downloads.ALL).then(aList => {
+      aList.removeFinished(aDownload => aDownload.startTime >= rangeBeginMs &&
+                                        aDownload.startTime <= rangeEndMs);
+    });
+  },
+
+  deleteAll() {
+    return Downloads.getList(Downloads.ALL).then(aList => {
+      aList.removeFinished(null);
+    });
+  },
+};
+
 // Here the map of Flags-Cleaner.
 const FLAGS_MAP = [
  { flag: Ci.nsIClearDataService.CLEAR_COOKIES,
    cleaner: CookieCleaner },
 
  { flag: Ci.nsIClearDataService.CLEAR_NETWORK_CACHE,
    cleaner: NetworkCacheCleaner },
 
  { flag: Ci.nsIClearDataService.CLEAR_IMAGE_CACHE,
    cleaner: ImageCacheCleaner, },
 
  { flag: Ci.nsIClearDataService.CLEAR_PLUGIN_DATA,
    cleaner: PluginDataCleaner, },
+
+ { flag: Ci.nsIClearDataService.CLEAR_DOWNLOADS,
+   cleaner: DownloadsCleaner, },
 ];
 
 this.ClearDataService = function() {};
 
 ClearDataService.prototype = Object.freeze({
   classID: Components.ID("{0c06583d-7dd8-4293-b1a5-912205f779aa}"),
   QueryInterface: ChromeUtils.generateQI([Ci.nsIClearDataService]),
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(ClearDataService),
@@ -276,8 +306,32 @@ ClearDataService.prototype = Object.free
       return aHelper(c.cleaner).catch(() => { resultFlags |= c.flag; });
     });
     Promise.all(promises).then(() => { aCallback.onDataDeleted(resultFlags); });
     return Cr.NS_OK;
   },
 });
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ClearDataService]);
+
+/**
+ * Returns true if the string passed in is part of the root domain of the
+ * current string.  For example, if this is "www.mozilla.org", and we pass in
+ * "mozilla.org", this will return true.  It would return false the other way
+ * around.
+ */
+function hasRootDomain(str, aDomain) {
+  let index = str.indexOf(aDomain);
+  // If aDomain is not found, we know we do not have it as a root domain.
+  if (index == -1)
+    return false;
+
+  // If the strings are the same, we obviously have a match.
+  if (str == aDomain)
+    return true;
+
+  // Otherwise, we have aDomain as our root domain iff the index of aDomain is
+  // aDomain.length subtracted from our length and (since we do not have an
+  // exact match) the character before the index is a dot or slash.
+  let prevChar = str[index - 1];
+  return (index == (str.length - aDomain.length)) &&
+         (prevChar == "." || prevChar == "/");
+}
--- a/toolkit/components/cleardata/nsIClearDataService.idl
+++ b/toolkit/components/cleardata/nsIClearDataService.idl
@@ -98,19 +98,23 @@ interface nsIClearDataService : nsISuppo
    */
   const uint32_t CLEAR_IMAGE_CACHE = 1 << 2;
 
   /**
    * Data stored by external plugins.
    */
   const uint32_t CLEAR_PLUGIN_DATA = 1 << 3;
 
+  /**
+   * Completed downloads.
+   */
+  const uint32_t CLEAR_DOWNLOADS = 1 << 4;
+
   /* TODO
   const uint32_t CLEAR_EME = 1 << 4;
-  const uint32_t CLEAR_DOWNLOADS = 1 << 5;
   const uint32_t CLEAR_PASSWORDS = 1 << 6;
   const uint32_t CLEAR_PERMISSIONS = 1 << 7;
   const uint32_t CLEAR_DOM_QUOTA = 1 << 8;
   const uint32_t CLEAR_CONTENT_PREFERENCES = 1 << 9;
   const uint32_t CLEAR_PREDICTOR_CACHE = 1 << 10;
   const uint32_t CLEAR_DOM_PUSH_NOTIFICATIONS = 1 << 11;
   const uint32_t CLEAR_HSTS = 1 << 12;
   const uint32_t CLEAR_HPKP = 1 << 13;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cleardata/tests/unit/head.js
@@ -0,0 +1,6 @@
+"use strict";
+
+function run_test() {
+  do_get_profile();
+  run_next_test();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cleardata/tests/unit/test_downloads.js
@@ -0,0 +1,180 @@
+/**
+ * Tests for downloads.
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/Downloads.jsm");
+ChromeUtils.import("resource://testing-common/FileTestUtils.jsm");
+
+const TEST_TARGET_FILE_NAME = "test-download.txt";
+let fileURL;
+let downloadList;
+
+function createFileURL() {
+  if (!fileURL) {
+    const file = Services.dirsvc.get("TmpD", Ci.nsIFile);
+    file.append("foo.txt");
+    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+    fileURL = Services.io.newFileURI(file);
+  }
+
+  return fileURL;
+}
+
+async function createDownloadList() {
+  if (!downloadList) {
+    Downloads._promiseListsInitialized = null;
+    Downloads._lists = {};
+    Downloads._summaries = {};
+
+    downloadList = await Downloads.getList(Downloads.ALL);
+  }
+
+  return downloadList;
+}
+
+add_task(async function test_all_downloads() {
+  const url = createFileURL();
+
+  const service = Cc["@mozilla.org/clear-data-service;1"]
+                    .getService(Ci.nsIClearDataService);
+  Assert.ok(!!service);
+
+  const list = await createDownloadList();
+
+  // First download.
+  let download = await Downloads.createDownload({
+    source: { url: url.spec, isPrivate: false },
+    target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path },
+  });
+  Assert.ok(!!download);
+  list.add(download);
+
+  // Second download.
+  download = await Downloads.createDownload({
+    source: { url: url.spec, isPrivate: false },
+    target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path },
+  });
+  await download.start();
+  Assert.ok(!!download);
+  list.add(download);
+
+  let items = await list.getAll();
+  Assert.equal(items.length, 2);
+
+  await new Promise(resolve => {
+    service.deleteData(Ci.nsIClearDataService.CLEAR_DOWNLOADS, value => {
+      Assert.equal(value, 0);
+      resolve();
+    });
+  });
+
+  items = await list.getAll();
+
+  // We don't remove the active downloads.
+  Assert.equal(items.length, 1);
+
+  await download.cancel();
+
+  await new Promise(resolve => {
+    service.deleteData(Ci.nsIClearDataService.CLEAR_DOWNLOADS, value => {
+      Assert.equal(value, 0);
+      resolve();
+    });
+  });
+
+  items = await list.getAll();
+  Assert.equal(items.length, 0);
+});
+
+add_task(async function test_range_downloads() {
+  const url = createFileURL();
+
+  const service = Cc["@mozilla.org/clear-data-service;1"]
+                    .getService(Ci.nsIClearDataService);
+
+  Assert.ok(!!service);
+
+  const list = await createDownloadList();
+
+  let download = await Downloads.createDownload({
+    source: { url: url.spec, isPrivate: false },
+    target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path },
+  });
+  Assert.ok(!!download);
+  list.add(download);
+
+  // Start + cancel. I need to have a startTime value.
+  await download.start();
+  await download.cancel();
+
+  let items = await list.getAll();
+  Assert.equal(items.length, 1);
+
+  await new Promise(resolve => {
+    service.deleteDataInTimeRange(download.startTime.getTime() * 1000,
+                                  download.startTime.getTime() * 1000,
+                                  true /* user request */,
+                                  Ci.nsIClearDataService.CLEAR_DOWNLOADS, value => {
+      Assert.equal(value, 0);
+      resolve();
+    });
+  });
+
+  items = await list.getAll();
+  Assert.equal(items.length, 0);
+});
+
+add_task(async function test_principal_downloads() {
+  const service = Cc["@mozilla.org/clear-data-service;1"]
+                    .getService(Ci.nsIClearDataService);
+
+  Assert.ok(!!service);
+
+  const list = await createDownloadList();
+
+  let download = await Downloads.createDownload({
+    source: { url: "http://example.net", isPrivate: false },
+    target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path },
+  });
+  Assert.ok(!!download);
+  list.add(download);
+
+  download = await Downloads.createDownload({
+    source: { url: "http://example.com", isPrivate: false },
+    target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path },
+  });
+  Assert.ok(!!download);
+  list.add(download);
+
+  let items = await list.getAll();
+  Assert.equal(items.length, 2);
+
+  let uri = Services.io.newURI("http://example.com");
+  let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+
+  await new Promise(resolve => {
+    service.deleteDataFromPrincipal(principal,
+                                    true /* user request */,
+                                    Ci.nsIClearDataService.CLEAR_DOWNLOADS, value => {
+      Assert.equal(value, 0);
+      resolve();
+    });
+  });
+
+  items = await list.getAll();
+  Assert.equal(items.length, 1);
+
+  await new Promise(resolve => {
+    service.deleteData(Ci.nsIClearDataService.CLEAR_DOWNLOADS, value => {
+      Assert.equal(value, 0);
+      resolve();
+    });
+  });
+
+  items = await list.getAll();
+  Assert.equal(items.length, 0);
+});
--- a/toolkit/components/cleardata/tests/unit/xpcshell.ini
+++ b/toolkit/components/cleardata/tests/unit/xpcshell.ini
@@ -1,5 +1,7 @@
 [DEFAULT]
+head = head.js
 support-files =
 
 [test_basic.js]
 [test_cookies.js]
+[test_downloads.js]
--- a/toolkit/forgetaboutsite/ForgetAboutSite.jsm
+++ b/toolkit/forgetaboutsite/ForgetAboutSite.jsm
@@ -58,26 +58,16 @@ var ForgetAboutSite = {
     promises.push((async function() {
       let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].
                 getService(Ci.mozIGeckoMediaPluginChromeService);
       mps.forgetThisSite(aDomain, JSON.stringify({}));
     })().catch(ex => {
       throw new Error("Exception thrown while clearing Encrypted Media Extensions: " + ex);
     }));
 
-
-    // Downloads
-    promises.push((async function() {
-      let list = await Downloads.getList(Downloads.ALL);
-      list.removeFinished(download => hasRootDomain(
-        NetUtil.newURI(download.source.url).host, aDomain));
-    })().catch(ex => {
-      throw new Error("Exception in clearing Downloads: " + ex);
-    }));
-
     // Passwords
     promises.push((async function() {
       // Clear all passwords for domain
       let logins = Services.logins.getAllLogins();
       for (let i = 0; i < logins.length; i++)
         if (hasRootDomain(logins[i].hostname, aDomain))
           Services.logins.removeLogin(logins[i]);
     })().catch(ex => {