Bug 1460768 - Part 2 - Make SiteDataManager automatically update itself when on storage changes. draft
authorJohann Hofmann <jhofmann@mozilla.com>
Thu, 05 Jul 2018 23:21:51 +0200
changeset 826310 e940841e9f6320955bb1a1f7230ab2dc8e685d6f
parent 826309 eae522ddc81bc4925c0c92c99145bc943bd41291
child 826311 0ba868988657f35a4c67d5ff9e7bcf84bb1be0a1
push id118287
push userjhofmann@mozilla.com
push dateFri, 03 Aug 2018 14:35:58 +0000
bugs1460768
milestone63.0a1
Bug 1460768 - Part 2 - Make SiteDataManager automatically update itself when on storage changes. MozReview-Commit-ID: CI6axhFWdSL
browser/components/nsBrowserGlue.js
browser/modules/SiteDataManager.jsm
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -129,16 +129,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   ReaderParent: "resource:///modules/ReaderParent.jsm",
   RemotePrompt: "resource:///modules/RemotePrompt.jsm",
   RemoteSettings: "resource://services-settings/remote-settings.js",
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SavantShieldStudy: "resource:///modules/SavantShieldStudy.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   ShellService: "resource:///modules/ShellService.jsm",
+  SiteDataManager: "resource:///modules/SiteDataManager.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   UIState: "resource://services-sync/UIState.jsm",
   UITour: "resource:///modules/UITour.jsm",
   WebChannel: "resource://gre/modules/WebChannel.jsm",
   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
 });
 
 /* global ContentPrefServiceParent:false, ContentSearch:false,
@@ -1031,16 +1032,18 @@ BrowserGlue.prototype = {
     }
 
     PageThumbs.init();
 
     NewTabUtils.init();
 
     AboutPrivateBrowsingHandler.init();
 
+    SiteDataManager.init();
+
     PageActions.init();
 
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
 
     // Set the default favicon size for UI views that use the page-icon protocol.
     PlacesUtils.favicons.setDefaultIconURIPreferredSize(16 * aWindow.devicePixelRatio);
   },
@@ -1082,16 +1085,17 @@ BrowserGlue.prototype = {
     // Only uninit PingCentre if the getter has initialized it
     if (Object.prototype.hasOwnProperty.call(this, "pingCentre")) {
       this.pingCentre.uninit();
     }
 
     PageThumbs.uninit();
     NewTabUtils.uninit();
     AboutPrivateBrowsingHandler.uninit();
+    SiteDataManager.uninit();
     AutoCompletePopup.uninit();
     DateTimePickerParent.uninit();
 
     // Browser errors are only collected on Nightly, but telemetry for
     // them is collected on all channels.
     if (AppConstants.MOZ_DATA_REPORTING) {
       this.browserErrorReporter.uninit();
     }
--- a/browser/modules/SiteDataManager.jsm
+++ b/browser/modules/SiteDataManager.jsm
@@ -1,100 +1,257 @@
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-ChromeUtils.defineModuleGetter(this, "OfflineAppCacheHelper",
-                               "resource://gre/modules/offlineAppCache.jsm");
-ChromeUtils.defineModuleGetter(this, "ServiceWorkerCleanUp",
-                               "resource://gre/modules/ServiceWorkerCleanUp.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "appCacheService",
+                                   "@mozilla.org/network/application-cache-service;1",
+                                   "nsIApplicationCacheService");
 
 var EXPORTED_SYMBOLS = [
   "SiteDataManager"
 ];
 
 XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
   return Services.strings.createBundle("chrome://browser/locale/siteData.properties");
 });
 
 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
   return Services.strings.createBundle("chrome://branding/locale/brand.properties");
 });
 
 var SiteDataManager = {
-
-  _qms: Services.qms,
-
-  _appCache: Cc["@mozilla.org/network/application-cache-service;1"].getService(Ci.nsIApplicationCacheService),
-
-  // A Map of sites and their disk usage according to Quota Manager and appcache
-  // Key is host (group sites based on host across scheme, port, origin atttributes).
+  // A Map of sites that groups storage and cookie information keyed by host.
+  //
   // Value is one object holding:
-  //   - principals: instances of nsIPrincipal (only when the site has
-  //     quota storage or AppCache).
-  //   - persisted: the persistent-storage status.
-  //   - quotaUsage: the usage of indexedDB and localStorage.
-  //   - appCacheList: an array of app cache; instances of nsIApplicationCache
+  //   - baseDomain: the base domain of the host (without subdomains)
+  //   - cookies: all currently set cookies for the host
+  //   - storage: a dictionary of quota storage information keyed by origin
+  //     - persisted: the persistent-storage status.
+  //     - quotaUsage: indexedDB usage. After bug 742822 and 1286798 are done,
+  //                   localStorage usage will be included.
+  //     - appCacheUsage: the usage of AppCache
+  //     - lastAccessed: when the data was last accessed by web content
   _sites: new Map(),
-
-  _getCacheSizeObserver: null,
-
-  _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) {
+  _getOrInsertSite(host, origin) {
     let site = this._sites.get(host);
     if (!site) {
       site = {
         baseDomain: this.getBaseDomainFromHost(host),
         cookies: [],
+        storage: {},
+      };
+      this._sites.set(host, site);
+    }
+    if (origin && !site.storage[origin]) {
+      site.storage[origin] = {
+        principal: null,
         persisted: false,
         quotaUsage: 0,
+        appCacheUsage: 0,
         lastAccessed: 0,
-        principals: [],
-        appCacheList: [],
       };
-      this._sites.set(host, site);
     }
     return site;
   },
 
   /**
+   * Initializes the SiteDataManager by fetching the initial cookie and
+   * site data information and registering observers to watch changes to
+   * cookies and site data. Hence, it should only be called once.
+   */
+  async init() {
+    await this.refresh();
+    Services.obs.addObserver(this, "cookie-changed");
+    Services.obs.addObserver(this, "storage-activity");
+  },
+
+  /**
+   * Unregisters observers for site data changes.
+   */
+  uninit() {
+    Services.obs.removeObserver(this, "cookie-changed");
+    Services.obs.removeObserver(this, "storage-activity");
+  },
+
+  /* OBSERVER MECHANISM */
+
+  // Holds the idleCallback that was requested to run for updating site data.
+  _pendingUpdate: null,
+  // An object that holds all update information to be accessed in the task
+  // that runs the next update. Has the following fields:
+  //   - refresh: Whether to refresh the entire set of data.
+  //   - cookies: A Set of hosts that had their cookies changed.
+  //   - quota: A Set of origins that had their storage changed.
+  _toUpdate: {
+    refresh: false,
+    cookies: new Set(),
+    quota: new Set(),
+  },
+
+  // Schedules a new update to site data info, if one isn't already pending.
+  _scheduleUpdate() {
+    if (this._pendingUpdate) {
+      return;
+    }
+
+    let hiddenDOMWindow = Services.appShell.hiddenDOMWindow;
+    this._pendingUpdate = hiddenDOMWindow
+      .requestIdleCallback(() => this._update(), {timeout: 15000});
+  },
+
+  // Given a list of hosts, updates the SiteDataManager cookie information for these hosts.
+  _updateCookies(hosts) {
+    for (let host of hosts) {
+      let site = this._getOrInsertSite(host);
+      site.cookies = [];
+
+      let cookies = Services.cookies.getCookiesFromHost(host);
+      while (cookies.hasMoreElements()) {
+        let cookie = cookies.getNext().QueryInterface(Ci.nsICookie2);
+        site.cookies.push(cookie);
+        if (site.lastAccessed < cookie.lastAccessed) {
+          site.lastAccessed = cookie.lastAccessed;
+        }
+      }
+    }
+  },
+
+  // Given a list of origins, updates the SiteDataManager storage information for these origins.
+  async _updateStorage(origins) {
+    for (let origin of origins) {
+      let principal =
+        Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
+      let site = this._getOrInsertSite(principal.URI.host, origin);
+
+      let storage = site.storage[origin];
+
+      storage.quotaUsage = 0;
+      storage.appCacheUsage = 0;
+      storage.principal = principal;
+      storage.lastAccessed = Date.now() * 1000;
+
+      let cache = appCacheService.getActiveCache(origin);
+      if (cache) {
+        storage.appCacheUsage = cache.usage;
+      }
+
+      storage.persisted = await new Promise(resolve => {
+        let persistedQuery = Services.qms.persisted(principal);
+        persistedQuery.callback = ({result}) => resolve(result);
+      });
+
+      await new Promise(resolve => {
+        let onUsageResult = request => {
+          if (request.resultCode == Cr.NS_OK) {
+            let item = request.result;
+            storage.quotaUsage = item.usage;
+          }
+
+          if (storage.quotaUsage <= 0 && storage.appCacheUsage <= 0 && !storage.persisted) {
+            delete site.storage[origin];
+          }
+
+          resolve();
+        };
+        Services.qms.getUsageForPrincipal(principal, onUsageResult);
+      });
+    }
+  },
+
+  // Parses the _toUpdate information and refreshes data for all hosts or
+  // origins that it contained at the time this function was called. It then
+  // empties the _toUpdate object.
+  async _update() {
+    // Copy over all sites that were updated since the last update.
+    let cookieHosts = Array.from(this._toUpdate.cookies);
+    let quotaOrigins = Array.from(this._toUpdate.quota);
+    let refresh = this._toUpdate.refresh;
+
+    // Now that we've read the sites to be updated we need to
+    // reset them so that the list can be filled for the next
+    // scheduled update.
+    this._toUpdate.refresh = false;
+    this._toUpdate.cookies.clear();
+    this._toUpdate.quota.clear();
+
+    if (refresh) {
+      await this.refresh();
+    } else {
+      this._updateCookies(cookieHosts);
+      await this._updateStorage(quotaOrigins);
+      Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+    }
+
+    // We're done now, unlock the guard that prevents running multiple
+    // updates at the same time (allow for another update to be scheduled).
+    this._pendingUpdate = null;
+
+    // If we have more sites in our list, schedule another update.
+    if (this._toUpdate.refresh ||
+        this._toUpdate.cookies.size > 0 ||
+        this._toUpdate.quota.size > 0) {
+      this._scheduleUpdate();
+    }
+  },
+
+  observe(subject, topic, data) {
+    switch (topic) {
+      case "cookie-changed": {
+        if (data == "batch-deleted" || data == "cleared") {
+          // A lot changed. Whatever, just refresh the whole thing.
+          this._toUpdate.refresh = true;
+        } else {
+          // One host changed, we can update the information from that host.
+          let host = subject.QueryInterface(Ci.nsICookie2).host;
+          this._toUpdate.cookies.add(host);
+        }
+        this._scheduleUpdate();
+        break;
+      }
+      case "storage-activity": {
+        let origin = data;
+        if (!origin.startsWith("http")) {
+          return;
+        }
+        this._toUpdate.quota.add(origin);
+        this._scheduleUpdate();
+        break;
+      }
+    }
+  },
+
+  /* FULL REFRESH */
+
+  /**
+   * Does a full refresh of all site data information in the site data manager.
+   * This function should generally not be called (it's mainly used internally
+   * and in tests), the SiteDataManager updates itself asynchronously when site
+   * data changes.
+   */
+  async refresh() {
+    // Since we're going to do a full refresh anyway let's cancel the pending update.
+    if (this._pendingUpdate) {
+      let hiddenDOMWindow = Services.appShell.hiddenDOMWindow;
+      hiddenDOMWindow.cancelIdleCallback(this._pendingUpdate);
+    }
+
+    // Clear old data and requests first
+    this._sites.clear();
+    this._refreshCookies();
+    await this._refreshQuotaUsage();
+    this._refreshAppCache();
+    Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
+    this._pendingUpdate = null;
+  },
+
+  _getCacheSizeObserver: null,
+  _getCacheSizePromise: null,
+  /**
    * 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.
    */
   getCacheSize() {
@@ -124,252 +281,165 @@ var SiteDataManager = {
         this._getCacheSizePromise = null;
         this._getCacheSizeObserver = null;
       }
     });
 
     return this._getCacheSizePromise;
   },
 
-  _getQuotaUsage() {
+  _getQuotaUsagePromise: null,
+  _quotaUsageRequest: null,
+  _refreshQuotaUsage() {
     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 origin = item.origin;
             let principal =
-              Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin);
+              Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
             let uri = principal.URI;
             if (uri.scheme == "http" || uri.scheme == "https") {
-              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.
+              let site = this._getOrInsertSite(uri.host, origin);
+              let storage = site.storage[origin];
+              storage.principal = principal;
+              storage.quotaUsage = item.usage;
+              if (storage.lastAccessed < item.lastAccessed) {
+                storage.lastAccessed = item.lastAccessed;
+              }
               if (item.persisted) {
-                site.persisted = true;
+                storage.persisted = true;
               }
-              if (site.lastAccessed < item.lastAccessed) {
-                site.lastAccessed = item.lastAccessed;
-              }
-              site.principals.push(principal);
-              site.quotaUsage += item.usage;
             }
           }
         }
         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);
+      this._quotaUsageRequest = Services.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);
-      if (site.lastAccessed < cookie.lastAccessed) {
-        site.lastAccessed = cookie.lastAccessed;
-      }
-    }
-  },
-
   _cancelGetQuotaUsage() {
     if (this._quotaUsageRequest) {
       this._quotaUsageRequest.cancel();
       this._quotaUsageRequest = null;
     }
   },
 
-  _updateAppCache() {
+  _refreshAppCache() {
     let groups;
     try {
-      groups = this._appCache.getGroups();
+      groups = appCacheService.getGroups();
     } catch (e) {
       // NS_ERROR_NOT_AVAILABLE means that appCache is not initialized,
       // which probably means the user has disabled it. Otherwise, log an
       // error. Either way, there's nothing we can do here.
       if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
         Cu.reportError(e);
       }
       return;
     }
 
-    for (let group of groups) {
-      let cache = this._appCache.getActiveCache(group);
+    for (let origin of groups) {
+      let cache = appCacheService.getActiveCache(origin);
       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 principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin);
       let uri = principal.URI;
-      let site = this._getOrInsertSite(uri.host);
-      if (!site.principals.some(p => p.origin == principal.origin)) {
-        site.principals.push(principal);
-      }
-      site.appCacheList.push(cache);
+      let site = this._getOrInsertSite(uri.host, origin);
+      let storage = site.storage[origin];
+      storage.principal = principal;
+      storage.appCacheUsage = cache.usage;
     }
   },
 
-  getTotalUsage() {
-    return this._getQuotaUsagePromise.then(() => {
-      let usage = 0;
-      for (let site of this._sites.values()) {
-        for (let cache of site.appCacheList) {
-          usage += cache.usage;
-        }
-        usage += site.quotaUsage;
-      }
-      return usage;
-    });
+  _refreshCookies() {
+    let cookiesEnum;
+    try {
+     cookiesEnum = Services.cookies.enumerator;
+    } catch (e) {
+      // This happens if the cookies DB is not in readable state, presumably
+      // because this code was scheduled to run after the profile was closed.
+      return;
+    }
+    while (cookiesEnum.hasMoreElements()) {
+      let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
+      let site = this._getOrInsertSite(cookie.rawHost);
+      site.cookies.push(cookie);
+    }
   },
 
+  /* USAGE INFORMATION */
+
   /**
    * Gets all sites that are currently storing site data.
    *
-   * The list is not automatically up-to-date.
-   * You need to call SiteDataManager.updateSites() before you
-   * can use this method for the first time (and whenever you want
-   * to get an updated set of list.)
-   *
    * @param {String} [optional] baseDomain - if specified, it will
    *                            only return data for sites with
    *                            the specified base domain.
    *
    * @returns a Promise that resolves with the list of all sites.
    */
   getSites(baseDomain) {
-    return this._getQuotaUsagePromise.then(() => {
-      let list = [];
-      for (let [host, site] of this._sites) {
-        if (baseDomain && site.baseDomain != baseDomain) {
-          continue;
-        }
-
-        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,
-          lastAccessed: new Date(site.lastAccessed / 1000),
-        });
-      }
-      return list;
-    });
-  },
-
-  _removePermission(site) {
-    let removals = new Set();
-    for (let principal of site.principals) {
-      let { originNoSuffix } = principal;
-      if (removals.has(originNoSuffix)) {
-        // In case of encountering
-        //   - https://www.foo.com
-        //   - https://www.foo.com^userContextId=2
-        // because setting/removing permission is across OAs already so skip the same origin without suffix
-        continue;
-      }
-      removals.add(originNoSuffix);
-      Services.perms.removeFromPrincipal(principal, "persistent-storage");
-    }
-  },
-
-  _removeQuotaUsage(site) {
-    let promises = [];
-    let removals = new Set();
-    for (let principal of site.principals) {
-      let { originNoSuffix } = principal;
-      if (removals.has(originNoSuffix)) {
-        // In case of encountering
-        //   - https://www.foo.com
-        //   - https://www.foo.com^userContextId=2
-        // below we have already removed across OAs so skip the same origin without suffix
+    let list = [];
+    for (let [host, site] of this._sites) {
+      if (baseDomain && site.baseDomain != baseDomain) {
         continue;
       }
-      removals.add(originNoSuffix);
-      promises.push(new Promise(resolve => {
-        // We are clearing *All* across OAs so need to ensure a principal without suffix here,
-        // or the call of `clearStoragesForPrincipal` would fail.
-        principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(originNoSuffix);
-        let request = this._qms.clearStoragesForPrincipal(principal, null, true);
-        request.callback = resolve;
-      }));
-    }
-    return Promise.all(promises);
-  },
+
+      let lastAccessed = 0;
+
+      for (let cookie of site.cookies) {
+        if (lastAccessed < cookie.lastAccessed) {
+          lastAccessed = cookie.lastAccessed;
+        }
+      }
 
-  _removeAppCache(site) {
-    for (let cache of site.appCacheList) {
-      cache.discard();
-    }
-  },
+      let usage = 0;
+      let persisted = false;
+      for (let origin in site.storage) {
+        let storage = site.storage[origin];
+        usage += storage.quotaUsage + storage.appCacheUsage;
+        persisted = persisted || storage.persisted;
+        if (lastAccessed < storage.lastAccessed) {
+          lastAccessed = storage.lastAccessed;
+        }
+      }
 
-  _removeCookies(site) {
-    for (let cookie of site.cookies) {
-      Services.cookies.remove(
-        cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
+      list.push({
+        baseDomain: site.baseDomain,
+        cookies: site.cookies,
+        lastAccessed: new Date(lastAccessed / 1000),
+        host, usage, persisted,
+      });
     }
-    site.cookies = [];
+    return list;
   },
 
-  /**
-   * Removes all site data for the specified list of hosts.
-   *
-   * @param {Array} a list of hosts to match for removal.
-   * @returns a Promise that resolves when data is removed and the site data
-   *          manager has been updated.
-   */
-  async remove(hosts) {
-    // Make sure we have up-to-date information.
-    await this._getQuotaUsage();
-    this._updateAppCache();
-
-    let unknownHost = "";
-    let promises = [];
-    for (let host of hosts) {
-      let site = this._sites.get(host);
-      if (site) {
-        // Clear localstorage.
-        Services.obs.notifyObservers(null, "browser:purge-domain-data", host);
-        this._removePermission(site);
-        this._removeAppCache(site);
-        this._removeCookies(site);
-        promises.push(ServiceWorkerCleanUp.removeFromHost(host));
-        promises.push(this._removeQuotaUsage(site));
-      } else {
-        unknownHost = host;
-        break;
+  getTotalUsage() {
+    let usage = 0;
+    for (let site of this._sites.values()) {
+      for (let origin in site.storage) {
+        let storage = site.storage[origin];
+        usage += storage.quotaUsage + storage.appCacheUsage;
       }
     }
-
-    await Promise.all(promises);
+    return usage;
+  },
 
-    if (unknownHost) {
-      throw `SiteDataManager: removing unknown site of ${unknownHost}`;
-    }
-
-    return this.updateSites();
-  },
+  /* DATA REMOVAL */
 
   /**
    * In the specified window, shows a prompt for removing
    * all site data or the specified list of hosts, warning the
    * user that this may log them out of websites.
    *
    * @param {mozIDOMWindowProxy} a parent DOM window to host the dialog.
    * @param {Array} [optional] an array of host name strings that will be removed.
@@ -396,64 +466,114 @@ var SiteDataManager = {
     let btn0Label = gStringBundle.GetStringFromName("clearSiteDataNow");
 
     let result = Services.prompt.confirmEx(
       win, title, text, flags, btn0Label, null, null, null, {});
     return result == 0;
   },
 
   /**
+   * Removes all site data for the specified list of hosts.
+   *
+   * @param {Array} a list of hosts to match for removal.
+   * @returns a Promise that resolves when data is removed and the site data
+   *          manager has been updated.
+   */
+  async remove(hosts) {
+    let promises = [];
+    for (let host of hosts) {
+      promises.push(new Promise(function(resolve) {
+        Services.clearData.deleteDataFromHost(host, true,
+          Ci.nsIClearDataService.CLEAR_COOKIES |
+          Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
+          Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS |
+          Ci.nsIClearDataService.CLEAR_PLUGIN_DATA |
+          Ci.nsIClearDataService.CLEAR_PERMISSIONS |
+          Ci.nsIClearDataService.CLEAR_ALL_CACHES, resolve);
+      }));
+    }
+
+    await Promise.all(promises);
+    return this.refresh();
+  },
+
+  /**
    * Clears all site data and cache
    *
    * @returns a Promise that resolves when the data is cleared.
    */
   async removeAll() {
-    this.removeCache();
+    await this.removeCache();
     return this.removeSiteData();
   },
 
   /**
    * Clears the entire network cache.
+   *
+   * @returns a Promise that resolves when the data is cleared.
    */
   removeCache() {
-    Services.cache2.clear();
+    return new Promise(function(resolve) {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL_CACHES, resolve);
+    });
   },
 
   /**
-   * Clears all site data, which currently means
-   *   - Cookies
-   *   - AppCache
-   *   - LocalStorage
-   *   - ServiceWorkers
-   *   - Quota Managed Storage
-   *   - persistent-storage permissions
+   * Clears all site data
    *
-   * @returns a Promise that resolves with the cache size on disk in bytes
+   * @returns a Promise that resolves when the data is cleared.
    */
   async removeSiteData() {
-    // LocalStorage
-    Services.obs.notifyObservers(null, "extension:purge-localStorage");
+    await new Promise(function(resolve) {
+      Services.clearData.deleteData(
+        Ci.nsIClearDataService.CLEAR_COOKIES |
+        Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
+        Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS |
+        Ci.nsIClearDataService.CLEAR_PLUGIN_DATA, resolve);
+    });
 
-    Services.cookies.removeAll();
-    OfflineAppCacheHelper.clear();
+    // Remove all "persistent-storage" permissions.
+    let enumerator = Services.perms.enumerator;
+    while (enumerator.hasMoreElements()) {
+      let permission = enumerator.getNext().QueryInterface(Ci.nsIPermission);
+      if (permission.type == "persistent-storage") {
+        Services.perms.removePermission(permission);
+      }
+    }
 
-    await ServiceWorkerCleanUp.removeAll();
+    return this.refresh();
+  },
+
+  /* UTILITY FUNCTIONS */
 
-    // Refresh sites using quota usage again.
-    // This is for the case:
-    //   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();
-    let promises = [];
-    for (let site of this._sites.values()) {
-      this._removePermission(site);
-      promises.push(this._removeQuotaUsage(site));
+  /**
+   * Utility function to get the base domain from a given host.
+   * Strips leading dots if present.
+   *
+   * @param host (String) - host to extract the base domain from.
+   * @throws if the host is malformed
+   * @returns (String) the base domain.
+   */
+  getBaseDomainFromHost(host) {
+    let result;
+
+    // Strip leading dot if present.
+    if (host.charAt(0) == ".") {
+      host = host.slice(1);
     }
-    return Promise.all(promises).then(() => this.updateSites());
+
+    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;
   },
+
 };