1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const { XPCOMUtils } = ChromeUtils.import(
8 "resource://gre/modules/XPCOMUtils.jsm"
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
12 var EXPORTED_SYMBOLS = ["SiteDataManager"];
14 XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
15 return Services.strings.createBundle(
16 "chrome://browser/locale/siteData.properties"
20 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
21 return Services.strings.createBundle(
22 "chrome://branding/locale/brand.properties"
26 var SiteDataManager = {
27 _appCache: Cc["@mozilla.org/network/application-cache-service;1"].getService(
28 Ci.nsIApplicationCacheService
31 // A Map of sites and their disk usage according to Quota Manager and appcache
32 // Key is host (group sites based on host across scheme, port, origin atttributes).
33 // Value is one object holding:
34 // - principals: instances of nsIPrincipal (only when the site has
35 // quota storage or AppCache).
36 // - persisted: the persistent-storage status.
37 // - quotaUsage: the usage of indexedDB and localStorage.
38 // - appCacheList: an array of app cache; instances of nsIApplicationCache
41 _getCacheSizeObserver: null,
43 _getCacheSizePromise: null,
45 _getQuotaUsagePromise: null,
47 _quotaUsageRequest: null,
50 Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
51 // Clear old data and requests first
53 this._getAllCookies();
54 await this._getQuotaUsage();
55 this._updateAppCache();
56 Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
59 getBaseDomainFromHost(host) {
62 result = Services.eTLD.getBaseDomainFromHost(host);
65 e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
66 e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
68 // For these 2 expected errors, just take the host as the result.
69 // - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6.
70 // - NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: not enough domain parts to extract.
79 _getOrInsertSite(host) {
80 let site = this._sites.get(host);
83 baseDomain: this.getBaseDomainFromHost(host),
91 this._sites.set(host, site);
97 * Retrieves the amount of space currently used by disk cache.
99 * You can use DownloadUtils.convertByteUnits to convert this to
100 * a user-understandable size/unit combination.
102 * @returns a Promise that resolves with the cache size on disk in bytes.
105 if (this._getCacheSizePromise) {
106 return this._getCacheSizePromise;
109 this._getCacheSizePromise = new Promise((resolve, reject) => {
110 // Needs to root the observer since cache service keeps only a weak reference.
111 this._getCacheSizeObserver = {
112 onNetworkCacheDiskConsumption: consumption => {
113 resolve(consumption);
114 this._getCacheSizePromise = null;
115 this._getCacheSizeObserver = null;
118 QueryInterface: ChromeUtils.generateQI([
119 "nsICacheStorageConsumptionObserver",
120 "nsISupportsWeakReference",
125 Services.cache2.asyncGetDiskConsumption(this._getCacheSizeObserver);
128 this._getCacheSizePromise = null;
129 this._getCacheSizeObserver = null;
133 return this._getCacheSizePromise;
137 this._cancelGetQuotaUsage();
138 this._getQuotaUsagePromise = new Promise(resolve => {
139 let onUsageResult = request => {
140 if (request.resultCode == Cr.NS_OK) {
141 let items = request.result;
142 for (let item of items) {
143 if (!item.persisted && item.usage <= 0) {
144 // An non-persistent-storage site with 0 byte quota usage is redundant for us so skip it.
147 let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
150 if (principal.schemeIs("http") || principal.schemeIs("https")) {
151 let site = this._getOrInsertSite(principal.host);
153 // - Site A (not persisted): https://www.foo.com
154 // - Site B (not persisted): https://www.foo.com^userContextId=2
155 // - Site C (persisted): https://www.foo.com:1234
156 // Although only C is persisted, grouping by host, as a result,
157 // we still mark as persisted here under this host group.
158 if (item.persisted) {
159 site.persisted = true;
161 if (site.lastAccessed < item.lastAccessed) {
162 site.lastAccessed = item.lastAccessed;
164 site.principals.push(principal);
165 site.quotaUsage += item.usage;
171 // XXX: The work of integrating localStorage into Quota Manager is in progress.
172 // After the bug 742822 and 1286798 landed, localStorage usage will be included.
173 // So currently only get indexedDB usage.
174 this._quotaUsageRequest = Services.qms.getUsage(onUsageResult);
176 return this._getQuotaUsagePromise;
180 for (let cookie of Services.cookies.cookies) {
181 let site = this._getOrInsertSite(cookie.rawHost);
182 site.cookies.push(cookie);
183 if (site.lastAccessed < cookie.lastAccessed) {
184 site.lastAccessed = cookie.lastAccessed;
189 _cancelGetQuotaUsage() {
190 if (this._quotaUsageRequest) {
191 this._quotaUsageRequest.cancel();
192 this._quotaUsageRequest = null;
199 groups = this._appCache.getGroups();
201 // NS_ERROR_NOT_AVAILABLE means that appCache is not initialized,
202 // which probably means the user has disabled it. Otherwise, log an
203 // error. Either way, there's nothing we can do here.
204 if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
210 for (let group of groups) {
211 let cache = this._appCache.getActiveCache(group);
212 if (cache.usage <= 0) {
213 // A site with 0 byte appcache usage is redundant for us so skip it.
216 let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
219 let site = this._getOrInsertSite(principal.host);
220 if (!site.principals.some(p => p.origin == principal.origin)) {
221 site.principals.push(principal);
223 site.appCacheList.push(cache);
228 * Gets the current AppCache usage by host. This is using asciiHost to compare
229 * against the provided host.
231 * @param {String} the ascii host to check usage for
232 * @returns the usage in bytes
234 getAppCacheUsageByHost(host) {
239 groups = this._appCache.getGroups();
241 // NS_ERROR_NOT_AVAILABLE means that appCache is not initialized,
242 // which probably means the user has disabled it. Otherwise, log an
243 // error. Either way, there's nothing we can do here.
244 if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
250 for (let group of groups) {
251 let uri = Services.io.newURI(group);
252 if (uri.asciiHost == host) {
253 let cache = this._appCache.getActiveCache(group);
254 usage += cache.usage;
262 * Checks if the site with the provided ASCII host is using any site data at all.
263 * This will check for:
264 * - Cookies (incl. subdomains)
267 * in that order. This function is meant to be fast, and thus will
268 * end searching and return true once the first trace of site data is found.
270 * @param {String} the ASCII host to check
271 * @returns {Boolean} whether the site has any data associated with it
273 async hasSiteData(asciiHost) {
274 if (Services.cookies.countCookiesFromHost(asciiHost)) {
278 let appCacheUsage = this.getAppCacheUsageByHost(asciiHost);
279 if (appCacheUsage > 0) {
283 let hasQuota = await new Promise(resolve => {
284 Services.qms.getUsage(request => {
285 if (request.resultCode != Cr.NS_OK) {
290 for (let item of request.result) {
291 if (!item.persisted && item.usage <= 0) {
295 let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
298 if (principal.asciiHost == asciiHost) {
316 return this._getQuotaUsagePromise.then(() => {
318 for (let site of this._sites.values()) {
319 for (let cache of site.appCacheList) {
320 usage += cache.usage;
322 usage += site.quotaUsage;
329 * Gets all sites that are currently storing site data.
331 * The list is not automatically up-to-date.
332 * You need to call SiteDataManager.updateSites() before you
333 * can use this method for the first time (and whenever you want
334 * to get an updated set of list.)
336 * @param {String} [optional] baseDomain - if specified, it will
337 * only return data for sites with
338 * the specified base domain.
340 * @returns a Promise that resolves with the list of all sites.
342 getSites(baseDomain) {
343 return this._getQuotaUsagePromise.then(() => {
345 for (let [host, site] of this._sites) {
346 if (baseDomain && site.baseDomain != baseDomain) {
350 let usage = site.quotaUsage;
351 for (let cache of site.appCacheList) {
352 usage += cache.usage;
355 baseDomain: site.baseDomain,
356 cookies: site.cookies,
359 persisted: site.persisted,
360 lastAccessed: new Date(site.lastAccessed / 1000),
367 _removePermission(site) {
368 let removals = new Set();
369 for (let principal of site.principals) {
370 let { originNoSuffix } = principal;
371 if (removals.has(originNoSuffix)) {
372 // In case of encountering
373 // - https://www.foo.com
374 // - https://www.foo.com^userContextId=2
375 // because setting/removing permission is across OAs already so skip the same origin without suffix
378 removals.add(originNoSuffix);
379 Services.perms.removeFromPrincipal(principal, "persistent-storage");
383 _removeQuotaUsage(site) {
385 let removals = new Set();
386 for (let principal of site.principals) {
387 let { originNoSuffix } = principal;
388 if (removals.has(originNoSuffix)) {
389 // In case of encountering
390 // - https://www.foo.com
391 // - https://www.foo.com^userContextId=2
392 // below we have already removed across OAs so skip the same origin without suffix
395 removals.add(originNoSuffix);
397 new Promise(resolve => {
398 // We are clearing *All* across OAs so need to ensure a principal without suffix here,
399 // or the call of `clearStoragesForPrincipal` would fail.
400 principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
403 let request = this._qms.clearStoragesForPrincipal(
409 request.callback = resolve;
413 return Promise.all(promises);
416 _removeAppCache(site) {
417 for (let cache of site.appCacheList) {
422 _removeCookies(site) {
423 for (let cookie of site.cookies) {
424 Services.cookies.remove(
428 cookie.originAttributes
434 // Returns a list of permissions from the permission manager that
435 // we consider part of "site data and cookies".
436 _getDeletablePermissions() {
439 for (let permission of Services.perms.all) {
441 permission.type == "persistent-storage" ||
442 permission.type == "storage-access"
444 perms.push(permission);
452 * Removes all site data for the specified list of hosts.
454 * @param {Array} a list of hosts to match for removal.
455 * @returns a Promise that resolves when data is removed and the site data
456 * manager has been updated.
458 async remove(hosts) {
459 let perms = this._getDeletablePermissions();
461 for (let host of hosts) {
463 Ci.nsIClearDataService.CLEAR_COOKIES |
464 Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
465 Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS |
466 Ci.nsIClearDataService.CLEAR_PLUGIN_DATA |
467 Ci.nsIClearDataService.CLEAR_EME |
468 Ci.nsIClearDataService.CLEAR_ALL_CACHES;
470 new Promise(function(resolve) {
471 const { clearData } = Services;
473 clearData.deleteDataFromHost(host, true, kFlags, resolve);
475 clearData.deleteDataFromLocalFiles(true, kFlags, resolve);
480 for (let perm of perms) {
481 // Specialcase local file permissions.
483 if (perm.principal.schemeIs("file")) {
484 Services.perms.removePermission(perm);
486 } else if (Services.eTLD.hasRootDomain(perm.principal.host, host)) {
487 Services.perms.removePermission(perm);
492 await Promise.all(promises);
494 return this.updateSites();
498 * In the specified window, shows a prompt for removing
499 * all site data or the specified list of hosts, warning the
500 * user that this may log them out of websites.
502 * @param {mozIDOMWindowProxy} a parent DOM window to host the dialog.
503 * @param {Array} [optional] an array of host name strings that will be removed.
504 * @returns a boolean whether the user confirmed the prompt.
506 promptSiteDataRemoval(win, removals) {
512 let features = "centerscreen,chrome,modal,resizable=no";
513 win.browsingContext.topChromeWindow.openDialog(
514 "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml",
522 let brandName = gBrandBundle.GetStringFromName("brandShortName");
524 Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
525 Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
526 Services.prompt.BUTTON_POS_0_DEFAULT;
527 let title = gStringBundle.GetStringFromName("clearSiteDataPromptTitle");
528 let text = gStringBundle.formatStringFromName("clearSiteDataPromptText", [
531 let btn0Label = gStringBundle.GetStringFromName("clearSiteDataNow");
533 let result = Services.prompt.confirmEx(
548 * Clears all site data and cache
550 * @returns a Promise that resolves when the data is cleared.
553 await this.removeCache();
554 return this.removeSiteData();
560 * @returns a Promise that resolves when the data is cleared.
563 return new Promise(function(resolve) {
564 Services.clearData.deleteData(
565 Ci.nsIClearDataService.CLEAR_ALL_CACHES,
572 * Clears all site data, but not cache, because the UI offers
573 * that functionality separately.
575 * @returns a Promise that resolves when the data is cleared.
577 async removeSiteData() {
578 await new Promise(function(resolve) {
579 Services.clearData.deleteData(
580 Ci.nsIClearDataService.CLEAR_COOKIES |
581 Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
582 Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS |
583 Ci.nsIClearDataService.CLEAR_EME |
584 Ci.nsIClearDataService.CLEAR_PLUGIN_DATA,
589 for (let permission of this._getDeletablePermissions()) {
590 Services.perms.removePermission(permission);
593 return this.updateSites();