forked from mirrors/gecko-dev
		
	 80327d3561
			
		
	
	
		80327d3561
		
	
	
	
	
		
			
			Differential Revision: https://phabricator.services.mozilla.com/D3729 --HG-- extra : rebase_source : e187b8e9a6b6db7ebc762adda5e489b25c7a7e43 extra : histedit_source : 868cb99d09954a51d6be321fcb516475ef70eb33
		
			
				
	
	
		
			457 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			457 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "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");
 | |
| 
 | |
| 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).
 | |
|   // 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
 | |
|   _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) {
 | |
|     let site = this._sites.get(host);
 | |
|     if (!site) {
 | |
|       site = {
 | |
|         baseDomain: this.getBaseDomainFromHost(host),
 | |
|         cookies: [],
 | |
|         persisted: false,
 | |
|         quotaUsage: 0,
 | |
|         lastAccessed: 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.
 | |
|    */
 | |
|   getCacheSize() {
 | |
|     if (this._getCacheSizePromise) {
 | |
|       return this._getCacheSizePromise;
 | |
|     }
 | |
| 
 | |
|     this._getCacheSizePromise = new Promise((resolve, reject) => {
 | |
|       // Needs to root the observer since cache service keeps only a weak reference.
 | |
|       this._getCacheSizeObserver = {
 | |
|         onNetworkCacheDiskConsumption: consumption => {
 | |
|           resolve(consumption);
 | |
|           this._getCacheSizePromise = null;
 | |
|           this._getCacheSizeObserver = null;
 | |
|         },
 | |
| 
 | |
|         QueryInterface: ChromeUtils.generateQI([
 | |
|           Ci.nsICacheStorageConsumptionObserver,
 | |
|           Ci.nsISupportsWeakReference
 | |
|         ])
 | |
|       };
 | |
| 
 | |
|       try {
 | |
|         Services.cache2.asyncGetDiskConsumption(this._getCacheSizeObserver);
 | |
|       } catch (e) {
 | |
|         reject(e);
 | |
|         this._getCacheSizePromise = null;
 | |
|         this._getCacheSizeObserver = null;
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     return this._getCacheSizePromise;
 | |
|   },
 | |
| 
 | |
|   _getQuotaUsage() {
 | |
|     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._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;
 | |
|               }
 | |
|               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);
 | |
|     });
 | |
|     return this._getQuotaUsagePromise;
 | |
|   },
 | |
| 
 | |
|   _getAllCookies() {
 | |
|     for (let cookie of Services.cookies.enumerator) {
 | |
|       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() {
 | |
|     let groups;
 | |
|     try {
 | |
|       groups = this._appCache.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);
 | |
|       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._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(() => {
 | |
|       let usage = 0;
 | |
|       for (let site of this._sites.values()) {
 | |
|         for (let cache of site.appCacheList) {
 | |
|           usage += cache.usage;
 | |
|         }
 | |
|         usage += site.quotaUsage;
 | |
|       }
 | |
|       return usage;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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
 | |
|         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);
 | |
|   },
 | |
| 
 | |
|   _removeAppCache(site) {
 | |
|     for (let cache of site.appCacheList) {
 | |
|       cache.discard();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _removeCookies(site) {
 | |
|     for (let cookie of site.cookies) {
 | |
|       Services.cookies.remove(
 | |
|         cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
 | |
|     }
 | |
|     site.cookies = [];
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     await Promise.all(promises);
 | |
| 
 | |
|     if (unknownHost) {
 | |
|       throw `SiteDataManager: removing unknown site of ${unknownHost}`;
 | |
|     }
 | |
| 
 | |
|     return this.updateSites();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|    * @returns a boolean whether the user confirmed the prompt.
 | |
|    */
 | |
|   promptSiteDataRemoval(win, removals) {
 | |
|     if (removals) {
 | |
|       let args = {
 | |
|         hosts: removals,
 | |
|         allowed: false
 | |
|       };
 | |
|       let features = "centerscreen,chrome,modal,resizable=no";
 | |
|       win.openDialog("chrome://browser/content/preferences/siteDataRemoveSelected.xul", "", features, args);
 | |
|       return args.allowed;
 | |
|     }
 | |
| 
 | |
|     let brandName = gBrandBundle.GetStringFromName("brandShortName");
 | |
|     let flags =
 | |
|       Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
 | |
|       Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1 +
 | |
|       Services.prompt.BUTTON_POS_0_DEFAULT;
 | |
|     let title = gStringBundle.GetStringFromName("clearSiteDataPromptTitle");
 | |
|     let text = gStringBundle.formatStringFromName("clearSiteDataPromptText", [brandName], 1);
 | |
|     let btn0Label = gStringBundle.GetStringFromName("clearSiteDataNow");
 | |
| 
 | |
|     let result = Services.prompt.confirmEx(
 | |
|       win, title, text, flags, btn0Label, null, null, null, {});
 | |
|     return result == 0;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Clears all site data and cache
 | |
|    *
 | |
|    * @returns a Promise that resolves when the data is cleared.
 | |
|    */
 | |
|   async removeAll() {
 | |
|     this.removeCache();
 | |
|     return this.removeSiteData();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Clears the entire network cache.
 | |
|    */
 | |
|   removeCache() {
 | |
|     Services.cache2.clear();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Clears all site data, which currently means
 | |
|    *   - Cookies
 | |
|    *   - AppCache
 | |
|    *   - LocalStorage
 | |
|    *   - ServiceWorkers
 | |
|    *   - Quota Managed Storage
 | |
|    *   - persistent-storage permissions
 | |
|    *
 | |
|    * @returns a Promise that resolves with the cache size on disk in bytes
 | |
|    */
 | |
|   async removeSiteData() {
 | |
|     // LocalStorage
 | |
|     Services.obs.notifyObservers(null, "extension:purge-localStorage");
 | |
| 
 | |
|     Services.cookies.removeAll();
 | |
|     OfflineAppCacheHelper.clear();
 | |
| 
 | |
|     await ServiceWorkerCleanUp.removeAll();
 | |
| 
 | |
|     // 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));
 | |
|     }
 | |
|     return Promise.all(promises).then(() => this.updateSites());
 | |
|   },
 | |
| };
 |