Bug 1422365 - Introduce nsIClearDataService - part 8 - DOM Quota, r=johannh
authorAndrea Marchesini <amarchesini@mozilla.com>
Fri, 01 Jun 2018 14:30:00 +0200
changeset 420842 3b9b4cca1b33e7034ca4eb699c856d313b3d1c3d
parent 420841 6652b69cb5d06143464b29d4a9b5309f9aedc72c
child 420843 470c748aeffea5535d2148c1cad67a5ca448db1b
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 8 - DOM Quota, r=johannh
browser/components/sessionstore/test/browser_464199.js
browser/modules/Sanitizer.jsm
toolkit/components/cleardata/ClearDataService.js
toolkit/components/cleardata/nsIClearDataService.idl
toolkit/forgetaboutsite/ForgetAboutSite.jsm
--- a/browser/components/sessionstore/test/browser_464199.js
+++ b/browser/components/sessionstore/test/browser_464199.js
@@ -63,18 +63,19 @@ add_task(async function() {
   is(closedTabs.length, test_state.windows[0]._closedTabs.length,
      "Closed tab list has the expected length");
   is(countByTitle(closedTabs, FORGET),
      test_state.windows[0]._closedTabs.length - remember_count,
      "The correct amout of tabs are to be forgotten");
   is(countByTitle(closedTabs, REMEMBER), remember_count,
      "Everything is set up.");
 
+  let promise = promiseClearHistory();
   await ForgetAboutSite.removeDataFromDomain("example.net");
-  await promiseClearHistory();
+  await promise;
   closedTabs = JSON.parse(ss.getClosedTabData(newWin));
   is(closedTabs.length, remember_count,
      "The correct amout of tabs was removed");
   is(countByTitle(closedTabs, FORGET), 0,
      "All tabs to be forgotten were indeed removed");
   is(countByTitle(closedTabs, REMEMBER), remember_count,
      "... and tabs to be remembered weren't.");
   // clean up
--- a/browser/modules/Sanitizer.jsm
+++ b/browser/modules/Sanitizer.jsm
@@ -8,23 +8,19 @@ 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",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
-  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
 });
 
-XPCOMUtils.defineLazyServiceGetter(this, "sas",
-                                   "@mozilla.org/storage/activity-service;1",
-                                   "nsIStorageActivityService");
 XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService",
                                    "@mozilla.org/dom/quota-manager-service;1",
                                    "nsIQuotaManagerService");
 XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
                                    "@mozilla.org/serviceworkers/manager;1",
                                    "nsIServiceWorkerManager");
 
 
@@ -329,80 +325,16 @@ var Sanitizer = {
                                Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES);
         TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
       },
     },
 
     offlineApps: {
       async clear(range) {
         await clearData(range, Ci.nsIClearDataService.CLEAR_DOM_STORAGES);
-
-        if (range) {
-          let principals = sas.getActiveOrigins(range[0], range[1])
-                              .QueryInterface(Ci.nsIArray);
-
-          let promises = [];
-
-          for (let i = 0; i < principals.length; ++i) {
-            let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
-
-            if (principal.URI.scheme != "http" &&
-                principal.URI.scheme != "https" &&
-                principal.URI.scheme != "file") {
-              continue;
-            }
-
-            // LocalStorage
-            Services.obs.notifyObservers(null, "browser:purge-domain-data", principal.URI.host);
-
-            // ServiceWorkers
-            await ServiceWorkerCleanUp.removeFromPrincipal(principal);
-
-            // QuotaManager
-            promises.push(new Promise(r => {
-              let req = quotaManagerService.clearStoragesForPrincipal(principal, null, false);
-              req.callback = () => { r(); };
-            }));
-          }
-
-          return Promise.all(promises);
-        }
-
-        // LocalStorage
-        Services.obs.notifyObservers(null, "extension:purge-localStorage");
-
-        // ServiceWorkers
-        await ServiceWorkerCleanUp.removeAll();
-
-        // QuotaManager
-        let promises = [];
-        await new Promise(resolve => {
-          quotaManagerService.getUsage(request => {
-            if (request.resultCode != Cr.NS_OK) {
-              // We are probably shutting down. We don't want to propagate the
-              // error, rejecting the promise.
-              resolve();
-              return;
-            }
-
-            for (let item of request.result) {
-              let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
-              let uri = principal.URI;
-              if (uri.scheme == "http" || uri.scheme == "https" || uri.scheme == "file") {
-                promises.push(new Promise(r => {
-                  let req = quotaManagerService.clearStoragesForPrincipal(principal, null, false);
-                  req.callback = () => { r(); };
-                }));
-              }
-            }
-            resolve();
-          });
-        });
-
-        return Promise.all(promises);
       }
     },
 
     history: {
       async clear(range) {
         let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
@@ -925,23 +857,23 @@ async function maybeSanitizeSessionPrinc
       promises.push(sanitizeSessionPrincipal(principals[i]));
     }
   }
 
   return Promise.all(promises);
 }
 
 async function sanitizeSessionPrincipal(principal) {
-  return Promise.all([
-    new Promise(r => {
-      let req = quotaManagerService.clearStoragesForPrincipal(principal, null, false);
-      req.callback = () => { r(); };
-    }).catch(() => {}),
-    ServiceWorkerCleanUp.removeFromPrincipal(principal).catch(() => {}),
-  ]);
+  await new Promise(resolve => {
+    let service = Cc["@mozilla.org/clear-data-service;1"]
+                    .getService(Ci.nsIClearDataService);
+    service.deleteDataFromPrincipal(principal, true /* user request */,
+                                    Ci.nsIClearDataService.CLEAR_DOM_STORAGES,
+                                    resolve);
+  });
 }
 
 function sanitizeNewTabSegregation() {
   let identity = ContextualIdentityService.getPrivateIdentity("userContextIdInternal.thumbnail");
   if (identity) {
     Services.obs.notifyObservers(null, "clear-origin-attributes-data",
                                  JSON.stringify({ userContextId: identity.userContextId }));
   }
--- a/toolkit/components/cleardata/ClearDataService.js
+++ b/toolkit/components/cleardata/ClearDataService.js
@@ -6,22 +6,30 @@
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   setTimeout: "resource://gre/modules/Timer.jsm",
   Downloads: "resource://gre/modules/Downloads.jsm",
   OfflineAppCacheHelper: "resource://gre/modules/offlineAppCache.jsm",
+  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
 });
 
+XPCOMUtils.defineLazyServiceGetter(this, "sas",
+                                   "@mozilla.org/storage/activity-service;1",
+                                   "nsIStorageActivityService");
+
 // 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.
+// * deleteByPrincipal() - this method is implemented only if the cleaner knows
+//                         how to delete data by nsIPrincipal. If not
+//                         implemented, deleteByHost will be used instead.
 // * 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.
 
@@ -256,16 +264,135 @@ const MediaDevicesCleaner = {
 const AppCacheCleaner = {
   deleteAll() {
     // AppCache: this doesn't wait for the cleanup to be complete.
     OfflineAppCacheHelper.clear();
     return Promise.resolve();
   },
 };
 
+const QuotaCleaner = {
+  deleteByPrincipal(aPrincipal) {
+    // localStorage
+    Services.obs.notifyObservers(null, "browser:purge-domain-data",
+                                 aPrincipal.URI.host);
+
+    // ServiceWorkers: they must be removed before cleaning QuotaManager.
+    return ServiceWorkerCleanUp.removeFromPrincipal(aPrincipal)
+      .then(_ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true)
+      .then(exceptionThrown => {
+        // QuotaManager
+        return new Promise((aResolve, aReject) => {
+          let req = Services.qms.clearStoragesForPrincipal(aPrincipal, null, false);
+          req.callback = () => {
+            if (exceptionThrown) {
+              aReject();
+            } else {
+              aResolve();
+            }
+          };
+        });
+      });
+  },
+
+  deleteByHost(aHost, aOriginAttributes) {
+    // localStorage
+    Services.obs.notifyObservers(null, "browser:purge-domain-data", aHost);
+
+    let exceptionThrown = false;
+
+    // ServiceWorkers: they must be removed before cleaning QuotaManager.
+    return Promise.all([
+      ServiceWorkerCleanUp.removeFromHost("http://" + aHost).catch(_ => { exceptionThrown = true; }),
+      ServiceWorkerCleanUp.removeFromHost("https://" + aHost).catch(_ => { exceptionThrown = true; }),
+    ]).then(() => {
+        // QuotaManager
+        // delete data from both HTTP and HTTPS sites
+        let httpURI = Services.io.newURI("http://" + aHost);
+        let httpsURI = Services.io.newURI("https://" + aHost);
+        let httpPrincipal = Services.scriptSecurityManager
+                                     .createCodebasePrincipal(httpURI, aOriginAttributes);
+        let httpsPrincipal = Services.scriptSecurityManager
+                                     .createCodebasePrincipal(httpsURI, aOriginAttributes);
+        return Promise.all([
+          new Promise(aResolve => {
+            let req = Services.qms.clearStoragesForPrincipal(httpPrincipal, null, true);
+            req.callback = () => { aResolve(); };
+          }),
+          new Promise(aResolve => {
+            let req = Services.qms.clearStoragesForPrincipal(httpsPrincipal, null, true);
+            req.callback = () => { aResolve(); };
+          }),
+        ]).then(() => {
+          return exceptionThrown ? Promise.reject() : Promise.resolve();
+        });
+      });
+  },
+
+  deleteByRange(aFrom, aTo) {
+    let principals = sas.getActiveOrigins(aFrom, aTo)
+                        .QueryInterface(Ci.nsIArray);
+
+    let promises = [];
+    for (let i = 0; i < principals.length; ++i) {
+      let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
+
+      if (principal.URI.scheme != "http" &&
+          principal.URI.scheme != "https" &&
+          principal.URI.scheme != "file") {
+        continue;
+      }
+
+      promises.push(this.deleteByPrincipal(principal));
+    }
+
+    return Promise.all(promises);
+  },
+
+  deleteAll() {
+    // localStorage
+    Services.obs.notifyObservers(null, "extension:purge-localStorage");
+
+    // ServiceWorkers
+    return ServiceWorkerCleanUp.removeAll()
+      .then(_ => /* exceptionThrown = */ false, _ => /* exceptionThrown = */ true)
+      .then(exceptionThrown => {
+        // QuotaManager
+        return new Promise((aResolve, aReject) => {
+          Services.qms.getUsage(aRequest => {
+            if (aRequest.resultCode != Cr.NS_OK) {
+              // We are probably shutting down.
+              if (exceptionThrown) {
+                aReject();
+              } else {
+                aResolve();
+              }
+              return;
+            }
+
+            let promises = [];
+            for (let item of aRequest.result) {
+              let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
+              if (principal.URI.scheme == "http" ||
+                  principal.URI.scheme == "https" ||
+                  principal.URI.scheme == "file") {
+                promises.push(new Promise(aResolve => {
+                  let req = Services.qms.clearStoragesForPrincipal(principal, null, false);
+                  req.callback = () => { aResolve(); };
+                }));
+              }
+            }
+
+            Promise.all(promises).then(exceptionThrown ? aReject : 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 },
 
@@ -281,16 +408,19 @@ const FLAGS_MAP = [
  { flag: Ci.nsIClearDataService.CLEAR_PASSWORDS,
    cleaner: PasswordsCleaner, },
 
  { flag: Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES,
    cleaner: MediaDevicesCleaner, },
 
  { flag: Ci.nsIClearDataService.CLEAR_APPCACHE,
    cleaner: AppCacheCleaner, },
+
+ { flag: Ci.nsIClearDataService.CLEAR_DOM_QUOTA,
+   cleaner: QuotaCleaner, },
 ];
 
 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),
@@ -317,23 +447,26 @@ ClearDataService.prototype = Object.free
   },
 
   deleteDataFromPrincipal(aPrincipal, aIsUserRequest, aFlags, aCallback) {
     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 ("deleteByPrincipal" in aCleaner && aCleaner.deleteByPrincipal) {
+        return aCleaner.deleteByPrincipal(aPrincipal);
+      }
+      // Some of the 'Cleaners' do not support to delete by principal. Fallback
+      // is to delete by host.
       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.
+      // Next fallback is to use deleteAll(), but only if this was a user request.
       if (aIsUserRequest) {
         return aCleaner.deleteAll();
       }
       // We don't want to delete more than what is strictly required.
       return Promise.resolve();
     });
   },
 
--- a/toolkit/components/cleardata/nsIClearDataService.idl
+++ b/toolkit/components/cleardata/nsIClearDataService.idl
@@ -118,20 +118,24 @@ interface nsIClearDataService : nsISuppo
    */
   const uint32_t CLEAR_MEDIA_DEVICES = 1 << 6;
 
   /**
    * AppCache.
    */
   const uint32_t CLEAR_APPCACHE = 1 << 7;
 
+  /**
+   * LocalStorage, IndexedDB, ServiceWorkers, DOM Cache and so on.
+   */
+  const uint32_t CLEAR_DOM_QUOTA = 1 << 8;
+
   /* TODO
   const uint32_t CLEAR_EME = 1 << 4;
   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;
   const uint32_t CLEAR_HISTORY = 1 << 14;
   const uint32_t CLEAR_SESSION_HISTORY = 1 << 15;
   const uint32_t CLEAR_FORMDATA = 1 << 16;
@@ -152,19 +156,19 @@ interface nsIClearDataService : nsISuppo
   /**
    * Delete all the possible caches.
    * TODO: add CLEAR_PREDICTOR_CACHE ?
    */
   const uint32_t CLEAR_ALL_CACHES = CLEAR_NETWORK_CACHE | CLEAR_IMAGE_CACHE;
 
   /**
    * Delete all DOM storages
-   * TODO: add CLEAR_DOM_QUOTA | CLEAR_DOM_PUSH_NOTIFICATIONS | CLEAR_FORMDATA | CLEAR_SESSION_HISTORY;
+   * TODO: add CLEAR_DOM_PUSH_NOTIFICATIONS | CLEAR_FORMDATA | CLEAR_SESSION_HISTORY;
    */
-  const uint32_t CLEAR_DOM_STORAGES = CLEAR_APPCACHE;
+  const uint32_t CLEAR_DOM_STORAGES = CLEAR_APPCACHE | CLEAR_DOM_QUOTA;
 
   /*
   const uint32_t CLEAR_BROWSER_DATA = CLEAR_DOWNLOADS | CLEAR_PASSWORDS | CLEAR_PERMISSIONS | CLEAR_CONTENT_PREFERENCES | CLEAR_HISTORY | CLEAR_LOGINS;
   */
 };
 
 /**
  * This is a companion interface for
--- a/toolkit/forgetaboutsite/ForgetAboutSite.jsm
+++ b/toolkit/forgetaboutsite/ForgetAboutSite.jsm
@@ -4,20 +4,16 @@
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
                                "resource://gre/modules/PlacesUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "Downloads",
-                               "resource://gre/modules/Downloads.jsm");
-ChromeUtils.defineModuleGetter(this, "ServiceWorkerCleanUp",
-                               "resource://gre/modules/ServiceWorkerCleanUp.jsm");
 
 var EXPORTED_SYMBOLS = ["ForgetAboutSite"];
 
 /**
  * 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.
@@ -76,38 +72,16 @@ var ForgetAboutSite = {
         } catch (ex) {
           // Ignore entry
         } finally {
           resolve();
         }
       }));
     }
 
-    // ServiceWorkers
-    await ServiceWorkerCleanUp.removeFromHost("http://" + aDomain);
-    await ServiceWorkerCleanUp.removeFromHost("https://" + aDomain);
-
-    // Offline Storages. This must run after the ServiceWorkers promises.
-    promises.push((async function() {
-      // delete data from both HTTP and HTTPS sites
-      let httpURI = NetUtil.newURI("http://" + aDomain);
-      let httpsURI = NetUtil.newURI("https://" + aDomain);
-      // Following code section has been reverted to the state before Bug 1238183,
-      // but added a new argument to clearStoragesForPrincipal() for indicating
-      // clear all storages under a given origin.
-      let httpPrincipal = Services.scriptSecurityManager
-                                   .createCodebasePrincipal(httpURI, {});
-      let httpsPrincipal = Services.scriptSecurityManager
-                                   .createCodebasePrincipal(httpsURI, {});
-      Services.qms.clearStoragesForPrincipal(httpPrincipal, null, true);
-      Services.qms.clearStoragesForPrincipal(httpsPrincipal, null, true);
-    })().catch(ex => {
-      throw new Error("Exception occured while clearing offline storages: " + ex);
-    }));
-
     // Content Preferences
     promises.push((async function() {
       let cps2 = Cc["@mozilla.org/content-pref/service;1"].
                  getService(Ci.nsIContentPrefService2);
       cps2.removeBySubdomain(aDomain, null, {
         handleCompletion: (reason) => {
           // Notify other consumers, including extensions
           Services.obs.notifyObservers(null, "browser:purge-domain-data", aDomain);