Bug 1422365 - Introduce nsIClearDataService - part 2 - cookies/network cache/image cache, r=johannh
authorAndrea Marchesini <amarchesini@mozilla.com>
Fri, 01 Jun 2018 14:29:59 +0200
changeset 420836 945c1f6e5c2957c88976bf3041c837efda497173
parent 420835 97b440fff0b5cc82dc5085533448e7c868c00a4a
child 420837 ef06c41bff1df36828cb9d31f5d0cd6e24a10ba2
push id103898
push useramarchesini@mozilla.com
push dateFri, 01 Jun 2018 12:31:55 +0000
treeherdermozilla-inbound@ee1e13b50338 [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 2 - cookies/network cache/image cache, r=johannh
browser/modules/Sanitizer.jsm
toolkit/components/cleardata/ClearDataService.js
toolkit/components/cleardata/tests/unit/.eslintrc.js
toolkit/components/cleardata/tests/unit/test_basic.js
toolkit/components/cleardata/tests/unit/test_cookies.js
toolkit/components/cleardata/tests/unit/xpcshell.ini
toolkit/forgetaboutsite/ForgetAboutSite.jsm
toolkit/modules/Services.jsm
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -30,22 +30,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/serviceworkers/manager;1",
                                    "nsIServiceWorkerManager");
 
 
 // Used as unique id for pending sanitizations.
 var gPendingSanitizationSerial = 0;
 
 /**
- * A number of iterations after which to yield time back
- * to the system.
- */
-const YIELD_PERIOD = 10;
-
-/**
  * Cookie lifetime policy is currently used to cleanup on shutdown other
  * components such as QuotaManager, localStorage, ServiceWorkers.
  */
 const PREF_COOKIE_LIFETIME = "network.cookie.lifetimePolicy";
 
 var Sanitizer = {
   /**
    * Whether we should sanitize on shutdown.
@@ -317,79 +311,32 @@ var Sanitizer = {
 
   // When making any changes to the sanitize implementations here,
   // please check whether the changes are applicable to Android
   // (mobile/android/modules/Sanitizer.jsm) as well.
 
   items: {
     cache: {
       async clear(range) {
-        let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
-
-        try {
-          // Cache doesn't consult timespan, nor does it have the
-          // facility for timespan-based eviction.  Wipe it.
-          Services.cache2.clear();
-        } catch (ex) {
-          seenException = ex;
-        }
-
-        try {
-          let imageCache = Cc["@mozilla.org/image/tools;1"]
-                             .getService(Ci.imgITools)
-                             .getImgCacheForDocument(null);
-          imageCache.clearCache(false); // true=chrome, false=content
-        } catch (ex) {
-          seenException = ex;
-        }
-
+        await clearData(range, Ci.nsIClearDataService.CLEAR_ALL_CACHES);
         TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj);
-        if (seenException) {
-          throw seenException;
-        }
       }
     },
 
     cookies: {
       async clear(range) {
         let seenException;
-        let yieldCounter = 0;
         let refObj = {};
 
         // Clear cookies.
         TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
-        try {
-          if (range) {
-            // Iterate through the cookies and delete any created after our cutoff.
-            let cookiesEnum = Services.cookies.enumerator;
-            while (cookiesEnum.hasMoreElements()) {
-              let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
-
-              if (cookie.creationTime > range[0]) {
-                // This cookie was created after our cutoff, clear it
-                Services.cookies.remove(cookie.host, cookie.name, cookie.path,
-                                        false, cookie.originAttributes);
-
-                if (++yieldCounter % YIELD_PERIOD == 0) {
-                  await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
-                }
-              }
-            }
-          } else {
-            // Remove everything
-            Services.cookies.removeAll();
-            await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
-          }
-        } catch (ex) {
-          seenException = ex;
-        } finally {
-          TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
-        }
+        await clearData(range, Ci.nsIClearDataService.CLEAR_COOKIES);
+        TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
 
         // Clear deviceIds. Done asynchronously (returns before complete).
         try {
           let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"]
                            .getService(Ci.nsIMediaManagerService);
           mediaMgr.sanitizeDeviceIds(range && range[0]);
         } catch (ex) {
           seenException = ex;
@@ -1150,8 +1097,21 @@ function safeGetPendingSanitizations() {
   try {
     return JSON.parse(
       Services.prefs.getStringPref(Sanitizer.PREF_PENDING_SANITIZATIONS, "[]"));
   } catch (ex) {
     Cu.reportError("Invalid JSON value for pending sanitizations: " + ex);
     return [];
   }
 }
+
+async function clearData(range, flags) {
+  if (range) {
+    await new Promise(resolve => {
+      Services.clearData.deleteDataInTimeRange(range[0], range[1], true /* user request */,
+                                               flags, resolve);
+    });
+  } else {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(flags, resolve);
+    });
+  }
+}
--- a/toolkit/components/cleardata/ClearDataService.js
+++ b/toolkit/components/cleardata/ClearDataService.js
@@ -1,33 +1,198 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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", this);
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/Timer.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.
+// *deleteByRange() - this method is implemented only if the cleaner knows how
+//                    to delete data by time range. It receives 2 time range
+//                    parameters: aFrom/aTo. If not implemented, deleteAll() is
+//                    used as fallback.
+
+const CookieCleaner = {
+  deleteByHost(aHost, aOriginAttributes) {
+    return new Promise(aResolve => {
+      Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify(aOriginAttributes),
+                                                         aHost);
+      aResolve();
+    });
+  },
+
+  deleteByRange(aFrom, aTo) {
+    let enumerator = Services.cookies.enumerator;
+    return this._deleteInternal(enumerator, aCookie => aCookie.creationTime > aFrom);
+  },
+
+  deleteAll() {
+    return new Promise(aResolve => {
+      Services.cookies.removeAll();
+      aResolve();
+    });
+  },
+
+  _deleteInternal(aEnumerator, aCb) {
+    // A number of iterations after which to yield time back to the system.
+    const YIELD_PERIOD = 10;
+
+    return new Promise((aResolve, aReject) => {
+      let count = 0;
+      while (aEnumerator.hasMoreElements()) {
+        let cookie = aEnumerator.getNext().QueryInterface(Ci.nsICookie2);
+        if (aCb(cookie)) {
+          Services.cookies.remove(cookie.host, cookie.name, cookie.path,
+                                  false, cookie.originAttributes);
+          // We don't want to block the main-thread.
+          if (++count % YIELD_PERIOD == 0) {
+            setTimeout(() => {
+              this._deleteInternal(aEnumerator, aCb).then(aResolve, aReject);
+            }, 0);
+            return;
+          }
+        }
+      }
+
+      aResolve();
+    });
+  },
+
+};
+
+const NetworkCacheCleaner = {
+  deleteAll() {
+    return new Promise(aResolve => {
+      Services.cache2.clear();
+      aResolve();
+    });
+  },
+};
+
+const ImageCacheCleaner = {
+  deleteAll() {
+    return new Promise(aResolve => {
+      let imageCache = Cc["@mozilla.org/image/tools;1"]
+                         .getService(Ci.imgITools)
+                         .getImgCacheForDocument(null);
+      imageCache.clearCache(false); // true=chrome, false=content
+      aResolve();
+    });
+  },
+};
+
+// 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, },
+];
 
 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),
 
   deleteDataFromHost(aHost, aIsUserRequest, aFlags, aCallback) {
-    // TODO
+    if (!aHost || !aCallback) {
+      return Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    return this._deleteInternal(aFlags, aCallback, aCleaner => {
+      // Some of the 'Cleaners' do not support to delete by principal. Let's
+      // use deleteAll() as fallback.
+      if (aCleaner.deleteByHost) {
+        // A generic originAttributes dictionary.
+        return aCleaner.deleteByHost(aHost, {});
+      }
+      // The user wants to delete data. Let's remove as much as we can.
+      if (aIsUserRequest) {
+        return aCleaner.deleteAll();
+      }
+      // We don't want to delete more than what is strictly required.
+      return Promise.resolve();
+    });
   },
 
   deleteDataFromPrincipal(aPrincipal, aIsUserRequest, aFlags, aCallback) {
-    // TODO
+    if (!aPrincipal || !aCallback) {
+      return Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    return this._deleteInternal(aFlags, aCallback, aCleaner => {
+      // Some of the 'Cleaners' do not support to delete by principal. Let's
+      // use deleteAll() as fallback.
+      if (aCleaner.deleteByHost) {
+        return aCleaner.deleteByHost(aPrincipal.URI.host,
+                                     aPrincipal.originAttributes);
+      }
+      // The user wants to delete data. Let's remove as much as we can.
+      if (aIsUserRequest) {
+        return aCleaner.deleteAll();
+      }
+      // We don't want to delete more than what is strictly required.
+      return Promise.resolve();
+    });
   },
 
   deleteDataInTimeRange(aFrom, aTo, aIsUserRequest, aFlags, aCallback) {
-    // TODO
+    if (aFrom > aTo || !aCallback) {
+      return Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    return this._deleteInternal(aFlags, aCallback, aCleaner => {
+      // Some of the 'Cleaners' do not support to delete by range. Let's use
+      // deleteAll() as fallback.
+      if (aCleaner.deleteByRange) {
+        return aCleaner.deleteByRange(aFrom, aTo);
+      }
+      // The user wants to delete data. Let's remove as much as we can.
+      if (aIsUserRequest) {
+        return aCleaner.deleteAll();
+      }
+      // We don't want to delete more than what is strictly required.
+      return Promise.resolve();
+    });
   },
 
   deleteData(aFlags, aCallback) {
-    // TODO
+    if (!aCallback) {
+      return Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    return this._deleteInternal(aFlags, aCallback, aCleaner => {
+      return aCleaner.deleteAll();
+    });
+  },
+
+  // This internal method uses aFlags against FLAGS_MAP in order to retrieve a
+  // list of 'Cleaners'. For each of them, the aHelper callback retrieves a
+  // promise object. All these promise objects are resolved before calling
+  // onDataDeleted.
+  _deleteInternal(aFlags, aCallback, aHelper) {
+    let resultFlags = 0;
+    let promises = FLAGS_MAP.filter(c => aFlags & c.flag).map(c => {
+      // Let's collect the failure in resultFlags.
+      return aHelper(c.cleaner).catch(() => { resultFlags |= c.flag; });
+    });
+    Promise.all(promises).then(() => { aCallback.onDataDeleted(resultFlags); });
+    return Cr.NS_OK;
   },
 });
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ClearDataService]);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cleardata/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/xpcshell-test"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cleardata/tests/unit/test_basic.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Basic test for nsIClearDataService module.
+ */
+
+"use strict";
+
+add_task(async function test_basic() {
+  const service = Cc["@mozilla.org/clear-data-service;1"]
+                  .getService(Ci.nsIClearDataService);
+  Assert.ok(!!service);
+
+  await new Promise(aResolve => {
+    service.deleteData(Ci.nsIClearDataService.CLEAR_IMAGE_CACHE |
+                       Ci.nsIClearDataService.CLEAR_COOKIES, value => {
+      Assert.equal(value, 0);
+      aResolve();
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cleardata/tests/unit/test_cookies.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests for cookies.
+ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+add_task(async function test_all_cookies() {
+  const service = Cc["@mozilla.org/clear-data-service;1"]
+                    .getService(Ci.nsIClearDataService);
+  Assert.ok(!!service);
+
+  const expiry = Date.now() + 24 * 60 * 60;
+  Services.cookies.add("example.net", "path", "name", "value", true /* secure */,
+                       true /* http only */, false /* session */,
+                       expiry, {});
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1);
+
+  await new Promise(aResolve => {
+    service.deleteData(Ci.nsIClearDataService.CLEAR_COOKIES, value => {
+      Assert.equal(value, 0);
+      aResolve();
+    });
+  });
+
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0);
+});
+
+add_task(async function test_range_cookies() {
+  const service = Cc["@mozilla.org/clear-data-service;1"]
+                    .getService(Ci.nsIClearDataService);
+  Assert.ok(!!service);
+
+  const expiry = Date.now() + 24 * 60 * 60;
+  Services.cookies.add("example.net", "path", "name", "value", true /* secure */,
+                       true /* http only */, false /* session */,
+                       expiry, {});
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1);
+
+  // The cookie is out of time range here.
+  let from = Date.now() + 60 * 60;
+  await new Promise(aResolve => {
+    service.deleteDataInTimeRange(from * 1000, expiry * 2000, true /* user request */,
+                                  Ci.nsIClearDataService.CLEAR_COOKIES, value => {
+      Assert.equal(value, 0);
+      aResolve();
+    });
+  });
+
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1);
+
+  // Now we delete all.
+  from = Date.now() - 60 * 60;
+  await new Promise(aResolve => {
+    service.deleteDataInTimeRange(from * 1000, expiry * 2000, true /* user request */,
+                                  Ci.nsIClearDataService.CLEAR_COOKIES, value => {
+      Assert.equal(value, 0);
+      aResolve();
+    });
+  });
+
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0);
+});
+
+add_task(async function test_principal_cookies() {
+  const service = Cc["@mozilla.org/clear-data-service;1"]
+                    .getService(Ci.nsIClearDataService);
+  Assert.ok(!!service);
+
+  const expiry = Date.now() + 24 * 60 * 60;
+  Services.cookies.add("example.net", "path", "name", "value", true /* secure */,
+                       true /* http only */, false /* session */,
+                       expiry, {});
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1);
+
+  let uri = Services.io.newURI("http://example.com");
+  let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+  await new Promise(aResolve => {
+    service.deleteDataFromPrincipal(principal, true /* user request */,
+                                    Ci.nsIClearDataService.CLEAR_COOKIES, value => {
+      Assert.equal(value, 0);
+      aResolve();
+    });
+  });
+
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1);
+
+  // Now we delete all.
+  uri = Services.io.newURI("http://example.net");
+  principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+  await new Promise(aResolve => {
+    service.deleteDataFromPrincipal(principal, true /* user request */,
+                                    Ci.nsIClearDataService.CLEAR_COOKIES, value => {
+      Assert.equal(value, 0);
+      aResolve();
+    });
+  });
+
+  Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cleardata/tests/unit/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+
+[test_basic.js]
+[test_cookies.js]
--- a/toolkit/forgetaboutsite/ForgetAboutSite.jsm
+++ b/toolkit/forgetaboutsite/ForgetAboutSite.jsm
@@ -40,45 +40,24 @@ function hasRootDomain(str, aDomain) {
          (prevChar == "." || prevChar == "/");
 }
 
 var ForgetAboutSite = {
   async removeDataFromDomain(aDomain) {
     await PlacesUtils.history.removeByFilter({ host: "." + aDomain });
 
     let promises = [];
-    // Cache
-    promises.push((async function() {
-      // NOTE: there is no way to clear just that domain, so we clear out
-      //       everything)
-      Services.cache2.clear();
-    })().catch(ex => {
-      throw new Error("Exception thrown while clearing the cache: " + ex);
-    }));
 
-    // Image Cache
-    promises.push((async function() {
-      let imageCache = Cc["@mozilla.org/image/tools;1"].
-                       getService(Ci.imgITools).getImgCacheForDocument(null);
-      imageCache.clearCache(false); // true=chrome, false=content
-    })().catch(ex => {
-      throw new Error("Exception thrown while clearing the image cache: " + ex);
-    }));
-
-    // Cookies
-    // Need to maximize the number of cookies cleaned here
-    promises.push((async function() {
-      let enumerator = Services.cookies.getCookiesWithOriginAttributes(JSON.stringify({}), aDomain);
-      while (enumerator.hasMoreElements()) {
-        let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie);
-        Services.cookies.remove(cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
-      }
-    })().catch(ex => {
-      throw new Error("Exception thrown while clearning cookies: " + ex);
-    }));
+    ["http://", "https://"].forEach(scheme => {
+      promises.push(new Promise(resolve => {
+        Services.clearData.deleteDataFromHost(aDomain, true /* user request */,
+                                              Ci.nsIClearDataService.CLEAR_ALL,
+                                              resolve);
+      }));
+    });
 
     // EME
     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);
--- a/toolkit/modules/Services.jsm
+++ b/toolkit/modules/Services.jsm
@@ -55,16 +55,17 @@ XPCOMUtils.defineLazyGetter(Services, "i
   return Cc["@mozilla.org/network/io-service;1"]
            .getService(Ci.nsIIOService)
            .QueryInterface(Ci.nsISpeculativeConnect);
 });
 
 var initTable = {
   appShell: ["@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
   cache2: ["@mozilla.org/netwerk/cache-storage-service;1", "nsICacheStorageService"],
+  clearData: ["@mozilla.org/clear-data-service;1", "nsIClearDataService"],
   cpmm: ["@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"],
   console: ["@mozilla.org/consoleservice;1", "nsIConsoleService"],
   cookies: ["@mozilla.org/cookiemanager;1", "nsICookieManager"],
   droppedLinkHandler: ["@mozilla.org/content/dropped-link-handler;1", "nsIDroppedLinkHandler"],
   els: ["@mozilla.org/eventlistenerservice;1", "nsIEventListenerService"],
   eTLD: ["@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"],
   intl: ["@mozilla.org/mozintl;1", "mozIMozIntl"],
   locale: ["@mozilla.org/intl/localeservice;1", "mozILocaleService"],