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 ChromeUtils.defineLazyGetter(lazy, "gStringBundle", function () {
8 return Services.strings.createBundle(
9 "chrome://browser/locale/siteData.properties"
13 ChromeUtils.defineLazyGetter(lazy, "gBrandBundle", function () {
14 return Services.strings.createBundle(
15 "chrome://branding/locale/brand.properties"
19 ChromeUtils.defineESModuleGetters(lazy, {
20 Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
23 export var SiteDataManager = {
24 // A Map of sites and their disk usage according to Quota Manager.
25 // Key is base domain (group sites based on base domain across scheme, port,
26 // origin attributes) or host if the entry does not have a base domain.
27 // Value is one object holding:
28 // - baseDomainOrHost: Same as key.
29 // - principals: instances of nsIPrincipal (only when the site has
31 // - persisted: the persistent-storage status.
32 // - quotaUsage: the usage of indexedDB and localStorage.
33 // - containersData: a map containing cookiesBlocked,lastAccessed and quotaUsage by userContextID.
36 _getCacheSizeObserver: null,
38 _getCacheSizePromise: null,
40 _getQuotaUsagePromise: null,
42 _quotaUsageRequest: null,
45 * Retrieve the latest site data and store it in SiteDataManager.
47 * Updating site data is a *very* expensive operation. This method exists so that
48 * consumers can manually decide when to update, most methods on SiteDataManager
49 * will not trigger updates automatically.
51 * It is *highly discouraged* to await on this function to finish before showing UI.
52 * Either trigger the update some time before the data is needed or use the
53 * entryUpdatedCallback parameter to update the UI async.
55 * @param {entryUpdatedCallback} a function to be called whenever a site is added or
56 * updated. This can be used to e.g. fill a UI that lists sites without
57 * blocking on the entire update to finish.
58 * @returns a Promise that resolves when updating is done.
60 async updateSites(entryUpdatedCallback) {
61 Services.obs.notifyObservers(null, "sitedatamanager:updating-sites");
62 // Clear old data and requests first
64 this._getAllCookies(entryUpdatedCallback);
65 await this._getQuotaUsage(entryUpdatedCallback);
66 Services.obs.notifyObservers(null, "sitedatamanager:sites-updated");
70 * Get the base domain of a host on a best-effort basis.
71 * @param {string} host - Host to convert.
72 * @returns {string} Computed base domain. If the base domain cannot be
73 * determined, because the host is an IP address or does not have enough
74 * domain levels we will return the original host. This includes the empty
76 * @throws {Error} Throws for unexpected conversion errors from eTLD service.
78 getBaseDomainFromHost(host) {
81 result = Services.eTLD.getBaseDomainFromHost(host);
84 e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
85 e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
87 // For these 2 expected errors, just take the host as the result.
88 // - NS_ERROR_HOST_IS_IP_ADDRESS: the host is in ipv4/ipv6.
89 // - NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS: not enough domain parts to extract.
98 _getOrInsertSite(baseDomainOrHost) {
99 let site = this._sites.get(baseDomainOrHost);
109 this._sites.set(baseDomainOrHost, site);
115 * Insert site with specific params into the SiteDataManager
116 * Currently used for testing purposes
118 * @param {String} baseDomainOrHost
119 * @param {Object} Site info params
120 * @returns {Object} site object
140 this._sites.set(baseDomainOrHost, site);
145 _getOrInsertContainersData(site, userContextId) {
146 if (!site.containersData) {
147 site.containersData = new Map();
150 let containerData = site.containersData.get(userContextId);
151 if (!containerData) {
154 lastAccessed: new Date(0),
157 site.containersData.set(userContextId, containerData);
159 return containerData;
163 * Retrieves the amount of space currently used by disk cache.
165 * You can use DownloadUtils.convertByteUnits to convert this to
166 * a user-understandable size/unit combination.
168 * @returns a Promise that resolves with the cache size on disk in bytes.
171 if (this._getCacheSizePromise) {
172 return this._getCacheSizePromise;
175 this._getCacheSizePromise = new Promise((resolve, reject) => {
176 // Needs to root the observer since cache service keeps only a weak reference.
177 this._getCacheSizeObserver = {
178 onNetworkCacheDiskConsumption: consumption => {
179 resolve(consumption);
180 this._getCacheSizePromise = null;
181 this._getCacheSizeObserver = null;
184 QueryInterface: ChromeUtils.generateQI([
185 "nsICacheStorageConsumptionObserver",
186 "nsISupportsWeakReference",
191 Services.cache2.asyncGetDiskConsumption(this._getCacheSizeObserver);
194 this._getCacheSizePromise = null;
195 this._getCacheSizeObserver = null;
199 return this._getCacheSizePromise;
202 _getQuotaUsage(entryUpdatedCallback) {
203 this._cancelGetQuotaUsage();
204 this._getQuotaUsagePromise = new Promise(resolve => {
205 let onUsageResult = request => {
206 if (request.resultCode == Cr.NS_OK) {
207 let items = request.result;
208 for (let item of items) {
209 if (!item.persisted && item.usage <= 0) {
210 // An non-persistent-storage site with 0 byte quota usage is redundant for us so skip it.
214 Services.scriptSecurityManager.createContentPrincipalFromOrigin(
217 if (principal.schemeIs("http") || principal.schemeIs("https")) {
218 // Group dom storage by first party. If an entry is partitioned
219 // the first party site will be in the partitionKey, instead of
220 // the principal baseDomain.
223 pkBaseDomain = ChromeUtils.getBaseDomainFromPartitionKey(
224 principal.originAttributes.partitionKey
229 let site = this._getOrInsertSite(
230 pkBaseDomain || principal.baseDomain
233 // - Site A (not persisted): https://www.foo.com
234 // - Site B (not persisted): https://www.foo.com^userContextId=2
235 // - Site C (persisted): https://www.foo.com:1234
236 // Although only C is persisted, grouping by base domain, as a
237 // result, we still mark as persisted here under this base
239 if (item.persisted) {
240 site.persisted = true;
242 if (site.lastAccessed < item.lastAccessed) {
243 site.lastAccessed = item.lastAccessed;
245 if (Number.isInteger(principal.userContextId)) {
246 let containerData = this._getOrInsertContainersData(
248 principal.userContextId
250 containerData.quotaUsage = item.usage;
251 let itemTime = item.lastAccessed / 1000;
252 if (containerData.lastAccessed.getTime() < itemTime) {
253 containerData.lastAccessed.setTime(itemTime);
256 site.principals.push(principal);
257 site.quotaUsage += item.usage;
258 if (entryUpdatedCallback) {
259 entryUpdatedCallback(principal.baseDomain, site);
266 // XXX: The work of integrating localStorage into Quota Manager is in progress.
267 // After the bug 742822 and 1286798 landed, localStorage usage will be included.
268 // So currently only get indexedDB usage.
269 this._quotaUsageRequest = Services.qms.getUsage(onUsageResult);
271 return this._getQuotaUsagePromise;
274 _getAllCookies(entryUpdatedCallback) {
275 for (let cookie of Services.cookies.cookies) {
276 // Group cookies by first party. If a cookie is partitioned the
277 // partitionKey will contain the first party site, instead of the host
281 pkBaseDomain = ChromeUtils.getBaseDomainFromPartitionKey(
282 cookie.originAttributes.partitionKey
287 let baseDomainOrHost =
288 pkBaseDomain || this.getBaseDomainFromHost(cookie.rawHost);
289 let site = this._getOrInsertSite(baseDomainOrHost);
290 if (entryUpdatedCallback) {
291 entryUpdatedCallback(baseDomainOrHost, site);
293 site.cookies.push(cookie);
294 if (Number.isInteger(cookie.originAttributes.userContextId)) {
295 let containerData = this._getOrInsertContainersData(
297 cookie.originAttributes.userContextId
299 containerData.cookiesBlocked += 1;
300 let cookieTime = cookie.lastAccessed / 1000;
301 if (containerData.lastAccessed.getTime() < cookieTime) {
302 containerData.lastAccessed.setTime(cookieTime);
305 if (site.lastAccessed < cookie.lastAccessed) {
306 site.lastAccessed = cookie.lastAccessed;
311 _cancelGetQuotaUsage() {
312 if (this._quotaUsageRequest) {
313 this._quotaUsageRequest.cancel();
314 this._quotaUsageRequest = null;
319 * Checks if the site with the provided ASCII host is using any site data at all.
320 * This will check for:
321 * - Cookies (incl. subdomains)
323 * in that order. This function is meant to be fast, and thus will
324 * end searching and return true once the first trace of site data is found.
326 * @param {String} the ASCII host to check
327 * @returns {Boolean} whether the site has any data associated with it
329 async hasSiteData(asciiHost) {
330 if (Services.cookies.countCookiesFromHost(asciiHost)) {
334 let hasQuota = await new Promise(resolve => {
335 Services.qms.getUsage(request => {
336 if (request.resultCode != Cr.NS_OK) {
341 for (let item of request.result) {
342 if (!item.persisted && item.usage <= 0) {
347 Services.scriptSecurityManager.createContentPrincipalFromOrigin(
350 if (principal.asciiHost == asciiHost) {
368 * Fetches total quota usage
369 * This method assumes that siteDataManager.updateSites has been called externally
371 * @returns total quota usage
374 return this._getQuotaUsagePromise.then(() => {
376 for (let site of this._sites.values()) {
377 usage += site.quotaUsage;
385 * Fetch quota usage for all time ranges to display in the clear data dialog.
386 * This method assumes that SiteDataManager.updateSites has been called externally
388 * @param {string[]} timeSpanArr - Array of timespan options to get quota usage
389 * from Sanitizer, e.g. ["TIMESPAN_HOUR", "TIMESPAN_2HOURS"]
390 * @returns {Object} bytes used for each timespan
392 async getQuotaUsageForTimeRanges(timeSpanArr) {
394 await this._getQuotaUsagePromise;
396 for (let timespan of timeSpanArr) {
400 let timeNow = Date.now();
401 for (let site of this._sites.values()) {
402 let lastAccessed = new Date(site.lastAccessed / 1000);
403 for (let timeSpan of timeSpanArr) {
404 let compareTime = new Date(
405 timeNow - lazy.Sanitizer.timeSpanMsMap[timeSpan]
408 if (timeSpan === "TIMESPAN_EVERYTHING") {
409 usage[timeSpan] += site.quotaUsage;
410 } else if (lastAccessed >= compareTime) {
411 usage[timeSpan] += site.quotaUsage;
419 * Gets all sites that are currently storing site data. Entries are grouped by
420 * parent base domain if applicable. For example "foo.example.com",
421 * "example.com" and "bar.example.com" will have one entry with the baseDomain
423 * A base domain entry will represent all data of its storage jar. The storage
424 * jar holds all first party data of the domain as well as any third party
425 * data partitioned under the domain. Additionally we will add data which
426 * belongs to the domain but is part of other domains storage jars . That is
427 * data third-party partitioned under other domains.
428 * Sites which cannot be associated with a base domain, for example IP hosts,
431 * The list is not automatically up-to-date. You need to call
432 * {@link updateSites} before you can use this method for the first time (and
433 * whenever you want to get an updated set of list.)
435 * @returns {Promise} Promise that resolves with the list of all sites.
438 await this._getQuotaUsagePromise;
440 return Array.from(this._sites.values()).map(site => ({
441 baseDomain: site.baseDomainOrHost,
442 cookies: site.cookies,
443 usage: site.quotaUsage,
444 containersData: site.containersData,
445 persisted: site.persisted,
446 lastAccessed: new Date(site.lastAccessed / 1000),
451 * Get site, which stores data, by base domain or host.
453 * The list is not automatically up-to-date. You need to call
454 * {@link updateSites} before you can use this method for the first time (and
455 * whenever you want to get an updated set of list.)
457 * @param {String} baseDomainOrHost - Base domain or host of the site to get.
459 * @returns {Promise<Object|null>} Promise that resolves with the site object
460 * or null if no site with given base domain or host stores data.
462 async getSite(baseDomainOrHost) {
463 let baseDomain = this.getBaseDomainFromHost(baseDomainOrHost);
465 let site = this._sites.get(baseDomain);
470 baseDomain: site.baseDomainOrHost,
471 cookies: site.cookies,
472 usage: site.quotaUsage,
473 containersData: site.containersData,
474 persisted: site.persisted,
475 lastAccessed: new Date(site.lastAccessed / 1000),
479 _removePermission(site) {
480 let removals = new Set();
481 for (let principal of site.principals) {
482 let { originNoSuffix } = principal;
483 if (removals.has(originNoSuffix)) {
484 // In case of encountering
485 // - https://www.foo.com
486 // - https://www.foo.com^userContextId=2
487 // because setting/removing permission is across OAs already so skip the same origin without suffix
490 removals.add(originNoSuffix);
491 Services.perms.removeFromPrincipal(principal, "persistent-storage");
495 _removeCookies(site) {
496 for (let cookie of site.cookies) {
497 Services.cookies.remove(
501 cookie.originAttributes
508 * Removes all site data for the specified list of domains and hosts.
509 * This includes site data of subdomains belonging to the domains or hosts and
510 * partitioned storage. Data is cleared per storage jar, which means if we
511 * clear "example.com", we will also clear third parties embedded on
512 * "example.com". Additionally we will clear all data of "example.com" (as a
513 * third party) from other jars.
515 * @param {string|string[]} domainsOrHosts - List of domains and hosts or
516 * single domain or host to remove.
517 * @returns {Promise} Promise that resolves when data is removed and the site
518 * data manager has been updated.
520 async remove(domainsOrHosts) {
521 if (domainsOrHosts == null) {
522 throw new Error("domainsOrHosts is required.");
524 // Allow the caller to pass a single base domain or host.
525 if (!Array.isArray(domainsOrHosts)) {
526 domainsOrHosts = [domainsOrHosts];
530 for (let domainOrHost of domainsOrHosts) {
532 Ci.nsIClearDataService.CLEAR_COOKIES |
533 Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
534 Ci.nsIClearDataService.CLEAR_EME |
535 Ci.nsIClearDataService.CLEAR_ALL_CACHES |
536 Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD |
537 Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE |
538 Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE |
539 Ci.nsIClearDataService.CLEAR_STORAGE_PERMISSIONS;
541 new Promise(function (resolve) {
542 const { clearData } = Services;
544 // First try to clear by base domain for aDomainOrHost. If we can't
545 // get a base domain, fall back to clearing by just host.
547 clearData.deleteDataFromBaseDomain(
555 e.result != Cr.NS_ERROR_HOST_IS_IP_ADDRESS &&
556 e.result != Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
560 clearData.deleteDataFromHost(domainOrHost, true, kFlags, resolve);
563 clearData.deleteDataFromLocalFiles(true, kFlags, resolve);
569 await Promise.all(promises);
571 return this.updateSites();
575 * In the specified window, shows a prompt for removing all site data or the
576 * specified list of base domains or hosts, warning the user that this may log
577 * them out of websites.
579 * @param {mozIDOMWindowProxy} win - a parent DOM window to host the dialog.
580 * @param {string[]} [removals] - an array of base domain or host strings that
582 * @returns {boolean} whether the user confirmed the prompt.
584 promptSiteDataRemoval(win, removals) {
590 let features = "centerscreen,chrome,modal,resizable=no";
591 win.browsingContext.topChromeWindow.openDialog(
592 "chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml",
600 let brandName = lazy.gBrandBundle.GetStringFromName("brandShortName");
602 Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
603 Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
604 Services.prompt.BUTTON_POS_0_DEFAULT;
605 let title = lazy.gStringBundle.GetStringFromName(
606 "clearSiteDataPromptTitle"
608 let text = lazy.gStringBundle.formatStringFromName(
609 "clearSiteDataPromptText",
612 let btn0Label = lazy.gStringBundle.GetStringFromName("clearSiteDataNow");
614 let result = Services.prompt.confirmEx(
629 * Clears all site data and cache
631 * @returns a Promise that resolves when the data is cleared.
634 await this.removeCache();
635 return this.removeSiteData();
641 * @returns a Promise that resolves when the data is cleared.
644 return new Promise(function (resolve) {
645 Services.clearData.deleteData(
646 Ci.nsIClearDataService.CLEAR_ALL_CACHES,
653 * Clears all site data, but not cache, because the UI offers
654 * that functionality separately.
656 * @returns a Promise that resolves when the data is cleared.
658 async removeSiteData() {
659 await new Promise(function (resolve) {
660 Services.clearData.deleteData(
661 Ci.nsIClearDataService.CLEAR_COOKIES_AND_SITE_DATA,
666 return this.updateSites();