Bug 1312361 - Clear all sites data from the Site Data section draft
authorFischer.json <fischer.json@gmail.com>
Tue, 15 Nov 2016 14:49:18 +0800
changeset 442749 a213b2bf244889d2c388f8db0b3d1ab9918e6a35
parent 442748 90b691bf09f5cc4fe7d0c6445fcf5afa2c34eeee
child 537873 0b4aa9a43ee69fbbdbe2e54b003c3b1b8d8d03a9
push id36795
push userbmo:fliu@mozilla.com
push dateWed, 23 Nov 2016 04:01:39 +0000
bugs1312361
milestone53.0a1
Bug 1312361 - Clear all sites data from the Site Data section MozReview-Commit-ID: 7JbzO7TQaeX
browser/components/preferences/CookiesUtil.jsm
browser/components/preferences/SiteDataManager.jsm
browser/components/preferences/cookies.js
browser/components/preferences/in-content/advanced.js
browser/components/preferences/in-content/advanced.xul
browser/components/preferences/moz.build
browser/locales/en-US/chrome/browser/preferences/advanced.dtd
browser/locales/en-US/chrome/browser/preferences/preferences.properties
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/CookiesUtil.jsm
@@ -0,0 +1,27 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
+                                  "resource://gre/modules/ContextualIdentityService.jsm");
+
+this.EXPORTED_SYMBOLS = [
+  "CookiesUtil"
+];
+
+this.CookiesUtil = {
+
+  isPrivateCookie(cookie) {
+      var { userContextId } = cookie.originAttributes;
+      if (!userContextId) {
+        // Default identity is public.
+        return false;
+      }
+      return !ContextualIdentityService.getIdentityFromId(userContextId).public;
+  },
+
+  makeStrippedHost(host) {
+    var formattedHost = host.charAt(0) == "." ? host.substring(1, host.length) : host;
+    return formattedHost.substring(0, 4) == "www." ? formattedHost.substring(4, formattedHost.length) : formattedHost;
+  }
+};
--- a/browser/components/preferences/SiteDataManager.jsm
+++ b/browser/components/preferences/SiteDataManager.jsm
@@ -1,15 +1,18 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "CookiesUtil",
+                                  "resource:///modules/CookiesUtil.jsm");
+
 this.EXPORTED_SYMBOLS = [
   "SiteDataManager"
 ];
 
 this.SiteDataManager = {
 
   _qms: Services.qms,
 
@@ -20,16 +23,17 @@ this.SiteDataManager = {
   // A Map of sites using the persistent-storage API (have requested persistent-storage permission)
   // Key is site's origin.
   // Value is one object holding:
   //   - perm: persistent-storage permision; instance of nsIPermission
   //   - status: the permission granted/rejected status
   //   - quotaUsage: the usage of indexedDB and localStorage.
   //   - appCacheList: an array of app cache; instances of nsIApplicationCache
   //   - diskCacheList: an array. Each element is object holding metadata of http cache:
+  //       - uri: the uri of that http cache
   //       - dataSize: that http cache size
   //       - idEnhance: the id extension of that http cache
   _sites: new Map(),
 
   _updateQuotaPromise: null,
 
   _updateDiskCachePromise: null,
 
@@ -57,16 +61,21 @@ this.SiteDataManager = {
           diskCacheList: []
         });
       }
     }
 
     this._updateQuota();
     this._updateAppCache();
     this._updateDiskCache();
+
+    Promise.all([this._updateQuotaPromise, this._updateDiskCachePromise])
+           .then(() => {
+             Services.obs.notifyObservers(null, "sitedatamanager:sites-updated", null);
+           });
   },
 
   _updateQuota() {
     this._quotaUsageRequests = [];
     let promises = [];
     for (let [key, site] of this._sites) { // eslint-disable-line no-unused-vars
       promises.push(new Promise(resolve => {
         let callback = {
@@ -111,16 +120,17 @@ this.SiteDataManager = {
     this._updateDiskCachePromise = new Promise(resolve => {
       if (this._sites.size) {
         let sites = this._sites;
         let visitor = {
           onCacheEntryInfo: function(uri, idEnhance, dataSize) {
             for (let [key, site] of sites) { // eslint-disable-line no-unused-vars
               if (site.perm.matchesURI(uri, true)) {
                 site.diskCacheList.push({
+                  uri,
                   dataSize,
                   idEnhance
                 });
                 break;
               }
             }
           },
           onCacheEntryVisitCompleted: function() {
@@ -146,9 +156,65 @@ this.SiteDataManager = {
                       for (cache of site.diskCacheList) {
                         usage += cache.dataSize;
                       }
                       usage += site.quotaUsage;
                     }
                     return usage;
                   });
   },
+
+  _removePermission(site) {
+    Services.perms.removePermission(site.perm);
+  },
+
+  _removeDiskCache(site) {
+    // Need this callback or exception of not enough arguments would be thrown
+    let callback = {
+      onCacheEntryDoomed: function () {}
+    };
+    for (let cache of site.diskCacheList) {
+      this._diskCache.asyncDoomURI(cache.uri, cache.idEnhance, callback);
+    }
+  },
+
+  _removeQuotaUsage(site) {
+    this._qms.clearStoragesForPrincipal(site.perm.principal, null, true);
+  },
+
+  _removeAppCache(site) {
+    for (let cache of site.appCacheList) {
+      cache.discard();
+    }
+  },
+
+  _removeCookie(site) {
+    let target = Services.eTLD.getBaseDomainFromHost(site.perm.principal.URI.host);
+    let e = Services.cookies.enumerator;
+    while (e.hasMoreElements()) {
+
+      let cookie = e.getNext();
+      if (cookie instanceof Components.interfaces.nsICookie) {
+        if (CookiesUtil.isPrivateCookie(cookie)) {
+          continue;
+        }
+
+        let host = CookiesUtil.makeStrippedHost(cookie.host);
+        let baseDomain = Services.eTLD.getBaseDomainFromHost(host);
+        if (baseDomain === target) {
+          Services.cookies.remove(
+            cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+        }
+      }
+    }
+  },
+
+  removeAll() {
+    for (let [key, site] of this._sites) {
+      this._removePermission(site);
+      this._removeDiskCache(site);
+      this._removeQuotaUsage(site);
+      this._removeAppCache(site);
+      this._removeCookie(site);
+    }
+    this.updateSites();
+  }
 };
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -5,16 +5,19 @@
 
 const nsICookie = Components.interfaces.nsICookie;
 
 Components.utils.import("resource://gre/modules/AppConstants.jsm");
 Components.utils.import("resource://gre/modules/PluralForm.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm")
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "CookiesUtil",
+                                  "resource:///modules/CookiesUtil.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
                                   "resource://gre/modules/ContextualIdentityService.jsm");
 
 var gCookiesWindow = {
   _cm               : Components.classes["@mozilla.org/cookiemanager;1"]
                                 .getService(Components.interfaces.nsICookieManager),
   _hosts            : {},
   _hostOrder        : [],
@@ -72,35 +75,26 @@ var gCookiesWindow = {
   _cookieEquals: function(aCookieA, aCookieB, aStrippedHost) {
     return aCookieA.rawHost == aStrippedHost &&
            aCookieA.name == aCookieB.name &&
            aCookieA.path == aCookieB.path &&
            ChromeUtils.isOriginAttributesEqual(aCookieA.originAttributes,
                                                aCookieB.originAttributes);
   },
 
-  _isPrivateCookie: function(aCookie) {
-      let { userContextId } = aCookie.originAttributes;
-      if (!userContextId) {
-        // Default identity is public.
-        return false;
-      }
-      return !ContextualIdentityService.getIdentityFromId(userContextId).public;
-  },
-
   observe: function(aCookie, aTopic, aData) {
     if (aTopic != "cookie-changed")
       return;
 
     if (aCookie instanceof Components.interfaces.nsICookie) {
-      if (this._isPrivateCookie(aCookie)) {
+      if (CookiesUtil.isPrivateCookie(aCookie)) {
         return;
       }
 
-      var strippedHost = this._makeStrippedHost(aCookie.host);
+      var strippedHost = CookiesUtil.makeStrippedHost(aCookie.host);
       if (aData == "changed")
         this._handleCookieChanged(aCookie, strippedHost);
       else if (aData == "added")
         this._handleCookieAdded(aCookie, strippedHost);
     }
     else if (aData == "cleared") {
       this._hosts = {};
       this._hostOrder = [];
@@ -445,21 +439,16 @@ var gCookiesWindow = {
     },
     setCellValue: function(aIndex, aColumn, aValue) {},
     setCellText: function(aIndex, aColumn, aValue) {},
     performAction: function(aAction) {},
     performActionOnRow: function(aAction, aIndex) {},
     performActionOnCell: function(aAction, aindex, aColumn) {}
   },
 
-  _makeStrippedHost: function(aHost) {
-    var formattedHost = aHost.charAt(0) == "." ? aHost.substring(1, aHost.length) : aHost;
-    return formattedHost.substring(0, 4) == "www." ? formattedHost.substring(4, formattedHost.length) : formattedHost;
-  },
-
   _addCookie: function(aStrippedHost, aCookie, aHostCount) {
     if (!(aStrippedHost in this._hosts) || !this._hosts[aStrippedHost]) {
       this._hosts[aStrippedHost] = { cookies   : [],
                                      rawHost   : aStrippedHost,
                                      level     : 0,
                                      open      : false,
                                      container : true };
       this._hostOrder.push(aStrippedHost);
@@ -488,21 +477,21 @@ var gCookiesWindow = {
   _loadCookies: function() {
     var e = this._cm.enumerator;
     var hostCount = { value: 0 };
     this._hosts = {};
     this._hostOrder = [];
     while (e.hasMoreElements()) {
       var cookie = e.getNext();
       if (cookie && cookie instanceof Components.interfaces.nsICookie) {
-        if (this._isPrivateCookie(cookie)) {
+        if (CookiesUtil.isPrivateCookie(cookie)) {
           continue;
         }
 
-        var strippedHost = this._makeStrippedHost(cookie.host);
+        var strippedHost = CookiesUtil.makeStrippedHost(cookie.host);
         this._addCookie(strippedHost, cookie, hostCount);
       }
       else
         break;
     }
     this._view._rowCount = hostCount.value;
   },
 
--- a/browser/components/preferences/in-content/advanced.js
+++ b/browser/components/preferences/in-content/advanced.js
@@ -51,18 +51,21 @@ var gAdvancedPane = {
       this.initSubmitHealthReport();
     }
     this.updateOnScreenKeyboardVisibility();
     this.updateCacheSizeInputField();
     this.updateActualCacheSize();
     this.updateActualAppCacheSize();
 
     if (Services.prefs.getBoolPref("browser.storageManager.enabled")) {
+      Services.obs.addObserver(this, "sitedatamanager:sites-updated", false);
       SiteDataManager.updateSites();
-      this.updateTotalSiteDataSize();
+
+      setEventListener("clearSiteDataButton", "command",
+                       gAdvancedPane.clearSiteData);
     }
 
     setEventListener("layers.acceleration.disabled", "change",
                      gAdvancedPane.updateHardwareAcceleration);
     setEventListener("advancedPrefs", "select",
                      gAdvancedPane.tabSelectionChanged);
     if (AppConstants.MOZ_TELEMETRY_REPORTING) {
       setEventListener("submitHealthReportBox", "command",
@@ -474,16 +477,33 @@ var gAdvancedPane = {
   {
     Components.utils.import("resource:///modules/offlineAppCache.jsm");
     OfflineAppCacheHelper.clear();
 
     this.updateActualAppCacheSize();
     this.updateOfflineApps();
   },
 
+  clearSiteData: function () {
+    var flags =
+      Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+      Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
+      Services.prompt.BUTTON_POS_0_DEFAULT;
+    var prefStrBundle = document.getElementById("bundlePreferences");
+    var title = prefStrBundle.getString("clearSiteDataPromptTitle");
+    var text = prefStrBundle.getString("clearSiteDataPromptText");
+    var btn0Label = prefStrBundle.getString("clearSiteDataNow");
+
+    var result = Services.prompt.confirmEx(
+      window, title, text, flags, btn0Label, null, null, null, {});
+    if (result === 0) {
+      SiteDataManager.removeAll();
+    }
+  },
+
   readOfflineNotify: function()
   {
     var pref = document.getElementById("browser.offline-apps.notify");
     var button = document.getElementById("offlineNotifyExceptions");
     button.disabled = !pref.value;
     return pref.value;
   },
 
@@ -769,12 +789,16 @@ var gAdvancedPane = {
   },
 
   observe: function(aSubject, aTopic, aData) {
     if (AppConstants.MOZ_UPDATER) {
       switch (aTopic) {
         case "nsPref:changed":
           this.updateReadPrefs();
           break;
+
+        case "sitedatamanager:sites-updated":
+          this.updateTotalSiteDataSize();
+          break;
       }
     }
   },
 };
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -330,16 +330,18 @@
       </groupbox>
 
       <!-- Site Data -->
       <groupbox id="siteDataGroup" hidden="true">
         <caption><label>&siteData.label;</label></caption>
 
         <hbox align="center">
           <label id="totalSiteDataSize" flex="1"></label>
+          <button id="clearSiteDataButton" icon="clear"
+                  label="&clearSiteData.label;" accesskey="&clearSiteData.accesskey;"/>
         </hbox>
       </groupbox>
     </tabpanel>
 
     <!-- Update -->
     <tabpanel id="updatePanel" orient="vertical">
 #ifdef MOZ_UPDATER
       <groupbox id="updateApp" align="start">
--- a/browser/components/preferences/moz.build
+++ b/browser/components/preferences/moz.build
@@ -14,13 +14,14 @@ for var in ('MOZ_APP_NAME', 'MOZ_MACBUND
     DEFINES[var] = CONFIG[var]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3', 'cocoa'):
     DEFINES['HAVE_SHELL_SERVICE'] = 1
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_JS_MODULES += [
+    'CookiesUtil.jsm',
     'SiteDataManager.jsm',
 ]
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Preferences')
--- a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
@@ -53,17 +53,19 @@
 <!ENTITY connectionSettings.label        "Settings…">
 <!ENTITY connectionSettings.accesskey    "e">
 
 <!ENTITY httpCache.label                 "Cached Web Content">
 
 <!ENTITY offlineStorage2.label           "Offline Web Content and User Data">
 
 <!--  Site Data section manages sites using Storage API and is under Network -->
-<!ENTITY siteData.label           "Site Data">
+<!ENTITY siteData.label                  "Site Data">
+<!ENTITY clearSiteData.label             "Clear All Data">
+<!ENTITY clearSiteData.accesskey         "l">
 
 <!-- LOCALIZATION NOTE:
   The entities limitCacheSizeBefore.label and limitCacheSizeAfter.label appear on a single
   line in preferences as follows:
 
   &limitCacheSizeBefore.label [textbox for cache size in MB] &limitCacheSizeAfter.label;
 -->
 <!ENTITY limitCacheSizeBefore.label      "Limit cache to">
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -165,16 +165,19 @@ actualDiskCacheSizeCalculated=Calculating web content cache size…
 actualAppCacheSize=Your application cache is currently using %1$S %2$S of disk space
 
 ####Preferences::Advanced::Network
 #LOCALIZATION NOTE: The next string is for the total usage of site data.
 #   e.g., "The total usage is currently using 200 MB"
 #   %1$S = size
 #   %2$S = unit (MB, KB, etc.)
 totalSiteDataSize=Your stored site data is currently using %1$S %2$S of disk space
+clearSiteDataPromptTitle=Clear all cookies and site data
+clearSiteDataPromptText=Selecting Clear Now will clear all cookies and site data stored by Firefox. This may sign you out of websites and remove offline web content.
+clearSiteDataNow=Clear Now
 
 syncUnlink.title=Do you want to unlink your device?
 syncUnlink.label=This device will no longer be associated with your Sync account. All of your personal data, both on this device and in your Sync account, will remain intact.
 syncUnlinkConfirm.label=Unlink
 
 # LOCALIZATION NOTE (featureEnableRequiresRestart, featureDisableRequiresRestart, restartTitle): %S = brandShortName
 featureEnableRequiresRestart=%S must restart to enable this feature.
 featureDisableRequiresRestart=%S must restart to disable this feature.