Bug 1667155 [wpt PR 25777] - [AspectRatio] Fix bug in flex-aspect-ratio-024 test...
[gecko.git] / browser / modules / SiteDataManager.jsm
blobdc0db46e3abd87225f06e90c474ffb8a5ee44595
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/. */
5 "use strict";
7 const { XPCOMUtils } = ChromeUtils.import(
8   "resource://gre/modules/XPCOMUtils.jsm"
9 );
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"
17   );
18 });
20 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
21   return Services.strings.createBundle(
22     "chrome://branding/locale/brand.properties"
23   );
24 });
26 var SiteDataManager = {
27   _appCache: Cc["@mozilla.org/network/application-cache-service;1"].getService(
28     Ci.nsIApplicationCacheService
29   ),
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
39   _sites: new Map(),
41   _getCacheSizeObserver: null,
43   _getCacheSizePromise: null,
45   _getQuotaUsagePromise: null,
47   _quotaUsageRequest: null,
49   async updateSites() {
50     Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
51     // Clear old data and requests first
52     this._sites.clear();
53     this._getAllCookies();
54     await this._getQuotaUsage();
55     this._updateAppCache();
56     Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
57   },
59   getBaseDomainFromHost(host) {
60     let result = host;
61     try {
62       result = Services.eTLD.getBaseDomainFromHost(host);
63     } catch (e) {
64       if (
65         e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
66         e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
67       ) {
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.
71         result = host;
72       } else {
73         throw e;
74       }
75     }
76     return result;
77   },
79   _getOrInsertSite(host) {
80     let site = this._sites.get(host);
81     if (!site) {
82       site = {
83         baseDomain: this.getBaseDomainFromHost(host),
84         cookies: [],
85         persisted: false,
86         quotaUsage: 0,
87         lastAccessed: 0,
88         principals: [],
89         appCacheList: [],
90       };
91       this._sites.set(host, site);
92     }
93     return site;
94   },
96   /**
97    * Retrieves the amount of space currently used by disk cache.
98    *
99    * You can use DownloadUtils.convertByteUnits to convert this to
100    * a user-understandable size/unit combination.
101    *
102    * @returns a Promise that resolves with the cache size on disk in bytes.
103    */
104   getCacheSize() {
105     if (this._getCacheSizePromise) {
106       return this._getCacheSizePromise;
107     }
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;
116         },
118         QueryInterface: ChromeUtils.generateQI([
119           "nsICacheStorageConsumptionObserver",
120           "nsISupportsWeakReference",
121         ]),
122       };
124       try {
125         Services.cache2.asyncGetDiskConsumption(this._getCacheSizeObserver);
126       } catch (e) {
127         reject(e);
128         this._getCacheSizePromise = null;
129         this._getCacheSizeObserver = null;
130       }
131     });
133     return this._getCacheSizePromise;
134   },
136   _getQuotaUsage() {
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.
145               continue;
146             }
147             let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
148               item.origin
149             );
150             if (principal.schemeIs("http") || principal.schemeIs("https")) {
151               let site = this._getOrInsertSite(principal.host);
152               // Assume 3 sites:
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;
160               }
161               if (site.lastAccessed < item.lastAccessed) {
162                 site.lastAccessed = item.lastAccessed;
163               }
164               site.principals.push(principal);
165               site.quotaUsage += item.usage;
166             }
167           }
168         }
169         resolve();
170       };
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);
175     });
176     return this._getQuotaUsagePromise;
177   },
179   _getAllCookies() {
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;
185       }
186     }
187   },
189   _cancelGetQuotaUsage() {
190     if (this._quotaUsageRequest) {
191       this._quotaUsageRequest.cancel();
192       this._quotaUsageRequest = null;
193     }
194   },
196   _updateAppCache() {
197     let groups;
198     try {
199       groups = this._appCache.getGroups();
200     } catch (e) {
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) {
205         Cu.reportError(e);
206       }
207       return;
208     }
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.
214         continue;
215       }
216       let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
217         group
218       );
219       let site = this._getOrInsertSite(principal.host);
220       if (!site.principals.some(p => p.origin == principal.origin)) {
221         site.principals.push(principal);
222       }
223       site.appCacheList.push(cache);
224     }
225   },
227   /**
228    * Gets the current AppCache usage by host. This is using asciiHost to compare
229    * against the provided host.
230    *
231    * @param {String} the ascii host to check usage for
232    * @returns the usage in bytes
233    */
234   getAppCacheUsageByHost(host) {
235     let usage = 0;
237     let groups;
238     try {
239       groups = this._appCache.getGroups();
240     } catch (e) {
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) {
245         Cu.reportError(e);
246       }
247       return usage;
248     }
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;
255       }
256     }
258     return usage;
259   },
261   /**
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)
265    *   - AppCache
266    *   - Quota Usage
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.
269    *
270    * @param {String} the ASCII host to check
271    * @returns {Boolean} whether the site has any data associated with it
272    */
273   async hasSiteData(asciiHost) {
274     if (Services.cookies.countCookiesFromHost(asciiHost)) {
275       return true;
276     }
278     let appCacheUsage = this.getAppCacheUsageByHost(asciiHost);
279     if (appCacheUsage > 0) {
280       return true;
281     }
283     let hasQuota = await new Promise(resolve => {
284       Services.qms.getUsage(request => {
285         if (request.resultCode != Cr.NS_OK) {
286           resolve(false);
287           return;
288         }
290         for (let item of request.result) {
291           if (!item.persisted && item.usage <= 0) {
292             continue;
293           }
295           let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
296             item.origin
297           );
298           if (principal.asciiHost == asciiHost) {
299             resolve(true);
300             return;
301           }
302         }
304         resolve(false);
305       });
306     });
308     if (hasQuota) {
309       return true;
310     }
312     return false;
313   },
315   getTotalUsage() {
316     return this._getQuotaUsagePromise.then(() => {
317       let usage = 0;
318       for (let site of this._sites.values()) {
319         for (let cache of site.appCacheList) {
320           usage += cache.usage;
321         }
322         usage += site.quotaUsage;
323       }
324       return usage;
325     });
326   },
328   /**
329    * Gets all sites that are currently storing site data.
330    *
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.)
335    *
336    * @param {String} [optional] baseDomain - if specified, it will
337    *                            only return data for sites with
338    *                            the specified base domain.
339    *
340    * @returns a Promise that resolves with the list of all sites.
341    */
342   getSites(baseDomain) {
343     return this._getQuotaUsagePromise.then(() => {
344       let list = [];
345       for (let [host, site] of this._sites) {
346         if (baseDomain && site.baseDomain != baseDomain) {
347           continue;
348         }
350         let usage = site.quotaUsage;
351         for (let cache of site.appCacheList) {
352           usage += cache.usage;
353         }
354         list.push({
355           baseDomain: site.baseDomain,
356           cookies: site.cookies,
357           host,
358           usage,
359           persisted: site.persisted,
360           lastAccessed: new Date(site.lastAccessed / 1000),
361         });
362       }
363       return list;
364     });
365   },
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
376         continue;
377       }
378       removals.add(originNoSuffix);
379       Services.perms.removeFromPrincipal(principal, "persistent-storage");
380     }
381   },
383   _removeQuotaUsage(site) {
384     let promises = [];
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
393         continue;
394       }
395       removals.add(originNoSuffix);
396       promises.push(
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(
401             originNoSuffix
402           );
403           let request = this._qms.clearStoragesForPrincipal(
404             principal,
405             null,
406             null,
407             true
408           );
409           request.callback = resolve;
410         })
411       );
412     }
413     return Promise.all(promises);
414   },
416   _removeAppCache(site) {
417     for (let cache of site.appCacheList) {
418       cache.discard();
419     }
420   },
422   _removeCookies(site) {
423     for (let cookie of site.cookies) {
424       Services.cookies.remove(
425         cookie.host,
426         cookie.name,
427         cookie.path,
428         cookie.originAttributes
429       );
430     }
431     site.cookies = [];
432   },
434   // Returns a list of permissions from the permission manager that
435   // we consider part of "site data and cookies".
436   _getDeletablePermissions() {
437     let perms = [];
439     for (let permission of Services.perms.all) {
440       if (
441         permission.type == "persistent-storage" ||
442         permission.type == "storage-access"
443       ) {
444         perms.push(permission);
445       }
446     }
448     return perms;
449   },
451   /**
452    * Removes all site data for the specified list of hosts.
453    *
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.
457    */
458   async remove(hosts) {
459     let perms = this._getDeletablePermissions();
460     let promises = [];
461     for (let host of hosts) {
462       const kFlags =
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;
469       promises.push(
470         new Promise(function(resolve) {
471           const { clearData } = Services;
472           if (host) {
473             clearData.deleteDataFromHost(host, true, kFlags, resolve);
474           } else {
475             clearData.deleteDataFromLocalFiles(true, kFlags, resolve);
476           }
477         })
478       );
480       for (let perm of perms) {
481         // Specialcase local file permissions.
482         if (!host) {
483           if (perm.principal.schemeIs("file")) {
484             Services.perms.removePermission(perm);
485           }
486         } else if (Services.eTLD.hasRootDomain(perm.principal.host, host)) {
487           Services.perms.removePermission(perm);
488         }
489       }
490     }
492     await Promise.all(promises);
494     return this.updateSites();
495   },
497   /**
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.
501    *
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.
505    */
506   promptSiteDataRemoval(win, removals) {
507     if (removals) {
508       let args = {
509         hosts: removals,
510         allowed: false,
511       };
512       let features = "centerscreen,chrome,modal,resizable=no";
513       win.browsingContext.topChromeWindow.openDialog(
514         "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml",
515         "",
516         features,
517         args
518       );
519       return args.allowed;
520     }
522     let brandName = gBrandBundle.GetStringFromName("brandShortName");
523     let flags =
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", [
529       brandName,
530     ]);
531     let btn0Label = gStringBundle.GetStringFromName("clearSiteDataNow");
533     let result = Services.prompt.confirmEx(
534       win,
535       title,
536       text,
537       flags,
538       btn0Label,
539       null,
540       null,
541       null,
542       {}
543     );
544     return result == 0;
545   },
547   /**
548    * Clears all site data and cache
549    *
550    * @returns a Promise that resolves when the data is cleared.
551    */
552   async removeAll() {
553     await this.removeCache();
554     return this.removeSiteData();
555   },
557   /**
558    * Clears all caches.
559    *
560    * @returns a Promise that resolves when the data is cleared.
561    */
562   removeCache() {
563     return new Promise(function(resolve) {
564       Services.clearData.deleteData(
565         Ci.nsIClearDataService.CLEAR_ALL_CACHES,
566         resolve
567       );
568     });
569   },
571   /**
572    * Clears all site data, but not cache, because the UI offers
573    * that functionality separately.
574    *
575    * @returns a Promise that resolves when the data is cleared.
576    */
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,
585         resolve
586       );
587     });
589     for (let permission of this._getDeletablePermissions()) {
590       Services.perms.removePermission(permission);
591     }
593     return this.updateSites();
594   },