Bug 1421737 - Part 1 - Include cookies in SiteDataManager.jsm. r=Gijs
authorJohann Hofmann <jhofmann@mozilla.com>
Fri, 09 Feb 2018 20:47:07 +0100
changeset 403928 0a00a0ef8bfcb309fc1f67e94b33d248f82f061b
parent 403927 515a82d2ec7bb7ab02e72970f0b2e3b2835e0c6d
child 403929 4a9b1b488c9426d5a9f07ca143e22486f67e2c77
push id99888
push userjhofmann@mozilla.com
push dateThu, 15 Feb 2018 12:34:34 +0000
treeherdermozilla-inbound@7fe3e62d6413 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1421737
milestone60.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 1421737 - Part 1 - Include cookies in SiteDataManager.jsm. r=Gijs MozReview-Commit-ID: HovCREaRbgL
browser/components/preferences/SiteDataManager.jsm
browser/components/preferences/cookies.js
--- a/browser/components/preferences/SiteDataManager.jsm
+++ b/browser/components/preferences/SiteDataManager.jsm
@@ -1,17 +1,15 @@
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "OfflineAppCacheHelper",
                                "resource:///modules/offlineAppCache.jsm");
-ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
-                               "resource://gre/modules/ContextualIdentityService.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
                                    "@mozilla.org/serviceworkers/manager;1",
                                    "nsIServiceWorkerManager");
 
 this.EXPORTED_SYMBOLS = [
   "SiteDataManager"
 ];
 
@@ -43,21 +41,58 @@ this.SiteDataManager = {
   _getCacheSizePromise: null,
 
   _getQuotaUsagePromise: null,
 
   _quotaUsageRequest: null,
 
   async updateSites() {
     Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
+    // Clear old data and requests first
+    this._sites.clear();
+    this._getAllCookies();
     await this._getQuotaUsage();
     this._updateAppCache();
     Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
   },
 
+  _getBaseDomainFromHost(host) {
+    let result = host;
+    try {
+      result = Services.eTLD.getBaseDomainFromHost(host);
+    } catch (e) {
+      if (e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+          e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+        // For these 2 expected errors, just take the host as the result.
+        // - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6.
+        // - NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: not enough domain parts to extract.
+        result = host;
+      } else {
+        throw e;
+      }
+    }
+    return result;
+  },
+
+  _getOrInsertSite(host) {
+    let site = this._sites.get(host);
+    if (!site) {
+      site = {
+        baseDomain: this._getBaseDomainFromHost(host),
+        cookies: [],
+        persisted: false,
+        quotaUsage: 0,
+        principals: [],
+        appCacheList: [],
+      };
+      this._sites.set(host, site);
+    }
+    return site;
+  },
+
   /**
    * Retrieves the amount of space currently used by disk cache.
    *
    * You can use DownloadUtils.convertByteUnits to convert this to
    * a user-understandable size/unit combination.
    *
    * @returns a Promise that resolves with the cache size on disk in bytes.
    */
@@ -89,66 +124,64 @@ this.SiteDataManager = {
         this._getCacheSizeObserver = null;
       }
     });
 
     return this._getCacheSizePromise;
   },
 
   _getQuotaUsage() {
-    // Clear old data and requests first
-    this._sites.clear();
     this._cancelGetQuotaUsage();
     this._getQuotaUsagePromise = new Promise(resolve => {
       let onUsageResult = request => {
         if (request.resultCode == Cr.NS_OK) {
           let items = request.result;
           for (let item of items) {
             if (!item.persisted && item.usage <= 0) {
               // An non-persistent-storage site with 0 byte quota usage is redundant for us so skip it.
               continue;
             }
             let principal =
               Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
             let uri = principal.URI;
             if (uri.scheme == "http" || uri.scheme == "https") {
-              let site = this._sites.get(uri.host);
-              if (!site) {
-                site = {
-                  persisted: false,
-                  quotaUsage: 0,
-                  principals: [],
-                  appCacheList: [],
-                };
-              }
+              let site = this._getOrInsertSite(uri.host);
               // Assume 3 sites:
               //   - Site A (not persisted): https://www.foo.com
               //   - Site B (not persisted): https://www.foo.com^userContextId=2
               //   - Site C (persisted):     https://www.foo.com:1234
               // Although only C is persisted, grouping by host, as a result,
               // we still mark as persisted here under this host group.
               if (item.persisted) {
                 site.persisted = true;
               }
               site.principals.push(principal);
               site.quotaUsage += item.usage;
-              this._sites.set(uri.host, site);
             }
           }
         }
         resolve();
       };
       // XXX: The work of integrating localStorage into Quota Manager is in progress.
       //      After the bug 742822 and 1286798 landed, localStorage usage will be included.
       //      So currently only get indexedDB usage.
       this._quotaUsageRequest = this._qms.getUsage(onUsageResult);
     });
     return this._getQuotaUsagePromise;
   },
 
+  _getAllCookies() {
+    let cookiesEnum = Services.cookies.enumerator;
+    while (cookiesEnum.hasMoreElements()) {
+      let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
+      let site = this._getOrInsertSite(cookie.rawHost);
+      site.cookies.push(cookie);
+    }
+  },
+
   _cancelGetQuotaUsage() {
     if (this._quotaUsageRequest) {
       this._quotaUsageRequest.cancel();
       this._quotaUsageRequest = null;
     }
   },
 
   _updateAppCache() {
@@ -156,26 +189,18 @@ this.SiteDataManager = {
     for (let group of groups) {
       let cache = this._appCache.getActiveCache(group);
       if (cache.usage <= 0) {
         // A site with 0 byte appcache usage is redundant for us so skip it.
         continue;
       }
       let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(group);
       let uri = principal.URI;
-      let site = this._sites.get(uri.host);
-      if (!site) {
-        site = {
-          persisted: false,
-          quotaUsage: 0,
-          principals: [ principal ],
-          appCacheList: [],
-        };
-        this._sites.set(uri.host, site);
-      } else if (!site.principals.some(p => p.origin == principal.origin)) {
+      let site = this._getOrInsertSite(uri.host);
+      if (!site.principals.some(p => p.origin == principal.origin)) {
         site.principals.push(principal);
       }
       site.appCacheList.push(cache);
     }
   },
 
   getTotalUsage() {
     return this._getQuotaUsagePromise.then(() => {
@@ -194,16 +219,18 @@ this.SiteDataManager = {
     return this._getQuotaUsagePromise.then(() => {
       let list = [];
       for (let [host, site] of this._sites) {
         let usage = site.quotaUsage;
         for (let cache of site.appCacheList) {
           usage += cache.usage;
         }
         list.push({
+          baseDomain: site.baseDomain,
+          cookies: site.cookies,
           host,
           usage,
           persisted: site.persisted
         });
       }
       return list;
     });
   },
@@ -249,81 +276,72 @@ this.SiteDataManager = {
   },
 
   _removeAppCache(site) {
     for (let cache of site.appCacheList) {
       cache.discard();
     }
   },
 
-  _removeCookie(site) {
-    for (let principal of site.principals) {
-      // Although `getCookiesFromHost` can get cookies across hosts under the same base domain, OAs matter.
-      // We still need OAs here.
-      let e = Services.cookies.getCookiesFromHost(principal.URI.host, principal.originAttributes);
-      while (e.hasMoreElements()) {
-        let cookie = e.getNext();
-        if (cookie instanceof Components.interfaces.nsICookie) {
-          if (this.isPrivateCookie(cookie)) {
-            continue;
-          }
-          Services.cookies.remove(
-            cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
-        }
-      }
-
-      Services.obs.notifyObservers(null, "browser:purge-domain-data", principal.URI.host);
+  _removeCookies(site) {
+    for (let cookie of site.cookies) {
+      Services.cookies.remove(
+        cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
     }
+    site.cookies = [];
   },
 
   _unregisterServiceWorker(serviceWorker) {
     return new Promise(resolve => {
       let unregisterCallback = {
         unregisterSucceeded: resolve,
         unregisterFailed: resolve, // We don't care about failures.
         QueryInterface: XPCOMUtils.generateQI([Ci.nsIServiceWorkerUnregisterCallback])
       };
       serviceWorkerManager.propagateUnregister(serviceWorker.principal, unregisterCallback, serviceWorker.scope);
     });
   },
 
   _removeServiceWorkersForSites(sites) {
     let promises = [];
-    let targetHosts = sites.map(s => s.principals[0].URI.host);
     let serviceWorkers = serviceWorkerManager.getAllRegistrations();
     for (let i = 0; i < serviceWorkers.length; i++) {
       let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
       // Sites are grouped and removed by host so we unregister service workers by the same host as well
-      if (targetHosts.includes(sw.principal.URI.host)) {
+      if (sites.has(sw.principal.URI.host)) {
         promises.push(this._unregisterServiceWorker(sw));
       }
     }
     return Promise.all(promises);
   },
 
   remove(hosts) {
     let unknownHost = "";
-    let targetSites = [];
+    let targetSites = new Map();
     for (let host of hosts) {
       let site = this._sites.get(host);
       if (site) {
         this._removePermission(site);
         this._removeAppCache(site);
-        this._removeCookie(site);
-        targetSites.push(site);
+        this._removeCookies(site);
+        Services.obs.notifyObservers(null, "browser:purge-domain-data", host);
+        targetSites.set(host, site);
       } else {
         unknownHost = host;
         break;
       }
     }
 
-    if (targetSites.length > 0) {
+    if (targetSites.size > 0) {
       this._removeServiceWorkersForSites(targetSites)
           .then(() => {
-            let promises = targetSites.map(s => this._removeQuotaUsage(s));
+            let promises = [];
+            for (let [, site] of targetSites) {
+              promises.push(this._removeQuotaUsage(site));
+            }
             return Promise.all(promises);
           })
           .then(() => this.updateSites());
     }
     if (unknownHost) {
       throw `SiteDataManager: removing unknown site of ${unknownHost}`;
     }
   },
@@ -396,23 +414,18 @@ this.SiteDataManager = {
     //   1. User goes to the about:preferences Site Data section.
     //   2. With the about:preferences opened, user visits another website.
     //   3. The website saves to quota usage, like indexedDB.
     //   4. User goes back to the Site Data section and commands to clear all site data.
     // For this case, we should refresh the site list so not to miss the website in the step 3.
     // We don't do "Clear All" on the quota manager like the cookie, appcache, http cache above
     // because that would clear browser data as well too,
     // see https://bugzilla.mozilla.org/show_bug.cgi?id=1312361#c9
+    this._sites.clear();
     await this._getQuotaUsage();
     promises = [];
     for (let site of this._sites.values()) {
       this._removePermission(site);
       promises.push(this._removeQuotaUsage(site));
     }
     return Promise.all(promises).then(() => this.updateSites());
   },
-
-  isPrivateCookie(cookie) {
-    let { userContextId } = cookie.originAttributes;
-    // A private cookie is when its userContextId points to a private identity.
-    return userContextId && !ContextualIdentityService.getPublicIdentityFromId(userContextId);
-  }
 };
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -5,18 +5,16 @@
 
 const nsICookie = Components.interfaces.nsICookie;
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "SiteDataManager",
-                               "resource:///modules/SiteDataManager.jsm");
 ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
                                "resource://gre/modules/ContextualIdentityService.jsm");
 
 var gCookiesWindow = {
   _hosts: {},
   _hostOrder: [],
   _tree: null,
   _bundle: null,
@@ -72,22 +70,28 @@ var gCookiesWindow = {
   _cookieEquals(aCookieA, aCookieB, aStrippedHost) {
     return aCookieA.rawHost == aStrippedHost &&
            aCookieA.name == aCookieB.name &&
            aCookieA.path == aCookieB.path &&
            ChromeUtils.isOriginAttributesEqual(aCookieA.originAttributes,
                                                aCookieB.originAttributes);
   },
 
+  _isPrivateCookie(cookie) {
+    let { userContextId } = cookie.originAttributes;
+    // A private cookie is when its userContextId points to a private identity.
+    return userContextId && !ContextualIdentityService.getPublicIdentityFromId(userContextId);
+  },
+
   observe(aCookie, aTopic, aData) {
     if (aTopic != "cookie-changed")
       return;
 
     if (aCookie instanceof Components.interfaces.nsICookie) {
-      if (SiteDataManager.isPrivateCookie(aCookie)) {
+      if (this._isPrivateCookie(aCookie)) {
         return;
       }
 
       var strippedHost = this._makeStrippedHost(aCookie.host);
       if (aData == "changed")
         this._handleCookieChanged(aCookie, strippedHost);
       else if (aData == "added")
         this._handleCookieAdded(aCookie, strippedHost);
@@ -470,17 +474,17 @@ var gCookiesWindow = {
   _loadCookies() {
     var e = Services.cookies.enumerator;
     var hostCount = { value: 0 };
     this._hosts = {};
     this._hostOrder = [];
     while (e.hasMoreElements()) {
       var cookie = e.getNext();
       if (cookie && cookie instanceof Components.interfaces.nsICookie) {
-        if (SiteDataManager.isPrivateCookie(cookie)) {
+        if (this._isPrivateCookie(cookie)) {
           continue;
         }
 
         var strippedHost = this._makeStrippedHost(cookie.host);
         this._addCookie(strippedHost, cookie, hostCount);
       } else
         break;
     }