--- a/browser/components/preferences/SiteDataManager.jsm
+++ b/browser/components/preferences/SiteDataManager.jsm
@@ -1,17 +1,15 @@
"use strict";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "OfflineAppCacheHelper",
"resource:///modules/offlineAppCache.jsm");
-ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
- "resource://gre/modules/ContextualIdentityService.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
"@mozilla.org/serviceworkers/manager;1",
"nsIServiceWorkerManager");
this.EXPORTED_SYMBOLS = [
"SiteDataManager"
];
@@ -43,21 +41,58 @@ this.SiteDataManager = {
_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,
+ 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.
*/
@@ -89,66 +124,64 @@ this.SiteDataManager = {
this._getCacheSizeObserver = null;
}
});
return this._getCacheSizePromise;
},
_getQuotaUsage() {
- // Clear old data and requests first
- this._sites.clear();
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._sites.get(uri.host);
- if (!site) {
- site = {
- persisted: false,
- quotaUsage: 0,
- principals: [],
- appCacheList: [],
- };
- }
+ 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;
}
site.principals.push(principal);
site.quotaUsage += item.usage;
- this._sites.set(uri.host, site);
}
}
}
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() {
+ 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);
+ }
+ },
+
_cancelGetQuotaUsage() {
if (this._quotaUsageRequest) {
this._quotaUsageRequest.cancel();
this._quotaUsageRequest = null;
}
},
_updateAppCache() {
@@ -156,26 +189,18 @@ this.SiteDataManager = {
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._sites.get(uri.host);
- if (!site) {
- site = {
- persisted: false,
- quotaUsage: 0,
- principals: [ principal ],
- appCacheList: [],
- };
- this._sites.set(uri.host, site);
- } else if (!site.principals.some(p => p.origin == principal.origin)) {
+ 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(() => {
@@ -194,16 +219,18 @@ this.SiteDataManager = {
return this._getQuotaUsagePromise.then(() => {
let list = [];
for (let [host, site] of this._sites) {
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
});
}
return list;
});
},
@@ -249,81 +276,72 @@ this.SiteDataManager = {
},
_removeAppCache(site) {
for (let cache of site.appCacheList) {
cache.discard();
}
},
- _removeCookie(site) {
- for (let principal of site.principals) {
- // Although `getCookiesFromHost` can get cookies across hosts under the same base domain, OAs matter.
- // We still need OAs here.
- let e = Services.cookies.getCookiesFromHost(principal.URI.host, principal.originAttributes);
- while (e.hasMoreElements()) {
- let cookie = e.getNext();
- if (cookie instanceof Components.interfaces.nsICookie) {
- if (this.isPrivateCookie(cookie)) {
- continue;
- }
- Services.cookies.remove(
- cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
- }
- }
-
- Services.obs.notifyObservers(null, "browser:purge-domain-data", principal.URI.host);
+ _removeCookies(site) {
+ for (let cookie of site.cookies) {
+ Services.cookies.remove(
+ cookie.host, cookie.name, cookie.path, false, cookie.originAttributes);
}
+ site.cookies = [];
},
_unregisterServiceWorker(serviceWorker) {
return new Promise(resolve => {
let unregisterCallback = {
unregisterSucceeded: resolve,
unregisterFailed: resolve, // We don't care about failures.
QueryInterface: XPCOMUtils.generateQI([Ci.nsIServiceWorkerUnregisterCallback])
};
serviceWorkerManager.propagateUnregister(serviceWorker.principal, unregisterCallback, serviceWorker.scope);
});
},
_removeServiceWorkersForSites(sites) {
let promises = [];
- let targetHosts = sites.map(s => s.principals[0].URI.host);
let serviceWorkers = serviceWorkerManager.getAllRegistrations();
for (let i = 0; i < serviceWorkers.length; i++) {
let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
// Sites are grouped and removed by host so we unregister service workers by the same host as well
- if (targetHosts.includes(sw.principal.URI.host)) {
+ if (sites.has(sw.principal.URI.host)) {
promises.push(this._unregisterServiceWorker(sw));
}
}
return Promise.all(promises);
},
remove(hosts) {
let unknownHost = "";
- let targetSites = [];
+ let targetSites = new Map();
for (let host of hosts) {
let site = this._sites.get(host);
if (site) {
this._removePermission(site);
this._removeAppCache(site);
- this._removeCookie(site);
- targetSites.push(site);
+ this._removeCookies(site);
+ Services.obs.notifyObservers(null, "browser:purge-domain-data", host);
+ targetSites.set(host, site);
} else {
unknownHost = host;
break;
}
}
- if (targetSites.length > 0) {
+ if (targetSites.size > 0) {
this._removeServiceWorkersForSites(targetSites)
.then(() => {
- let promises = targetSites.map(s => this._removeQuotaUsage(s));
+ let promises = [];
+ for (let [, site] of targetSites) {
+ promises.push(this._removeQuotaUsage(site));
+ }
return Promise.all(promises);
})
.then(() => this.updateSites());
}
if (unknownHost) {
throw `SiteDataManager: removing unknown site of ${unknownHost}`;
}
},
@@ -396,23 +414,18 @@ this.SiteDataManager = {
// 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();
promises = [];
for (let site of this._sites.values()) {
this._removePermission(site);
promises.push(this._removeQuotaUsage(site));
}
return Promise.all(promises).then(() => this.updateSites());
},
-
- isPrivateCookie(cookie) {
- let { userContextId } = cookie.originAttributes;
- // A private cookie is when its userContextId points to a private identity.
- return userContextId && !ContextualIdentityService.getPublicIdentityFromId(userContextId);
- }
};
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -5,18 +5,16 @@
const nsICookie = Components.interfaces.nsICookie;
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "SiteDataManager",
- "resource:///modules/SiteDataManager.jsm");
ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
"resource://gre/modules/ContextualIdentityService.jsm");
var gCookiesWindow = {
_hosts: {},
_hostOrder: [],
_tree: null,
_bundle: null,
@@ -72,22 +70,28 @@ var gCookiesWindow = {
_cookieEquals(aCookieA, aCookieB, aStrippedHost) {
return aCookieA.rawHost == aStrippedHost &&
aCookieA.name == aCookieB.name &&
aCookieA.path == aCookieB.path &&
ChromeUtils.isOriginAttributesEqual(aCookieA.originAttributes,
aCookieB.originAttributes);
},
+ _isPrivateCookie(cookie) {
+ let { userContextId } = cookie.originAttributes;
+ // A private cookie is when its userContextId points to a private identity.
+ return userContextId && !ContextualIdentityService.getPublicIdentityFromId(userContextId);
+ },
+
observe(aCookie, aTopic, aData) {
if (aTopic != "cookie-changed")
return;
if (aCookie instanceof Components.interfaces.nsICookie) {
- if (SiteDataManager.isPrivateCookie(aCookie)) {
+ if (this._isPrivateCookie(aCookie)) {
return;
}
var strippedHost = this._makeStrippedHost(aCookie.host);
if (aData == "changed")
this._handleCookieChanged(aCookie, strippedHost);
else if (aData == "added")
this._handleCookieAdded(aCookie, strippedHost);
@@ -470,17 +474,17 @@ var gCookiesWindow = {
_loadCookies() {
var e = Services.cookies.enumerator;
var hostCount = { value: 0 };
this._hosts = {};
this._hostOrder = [];
while (e.hasMoreElements()) {
var cookie = e.getNext();
if (cookie && cookie instanceof Components.interfaces.nsICookie) {
- if (SiteDataManager.isPrivateCookie(cookie)) {
+ if (this._isPrivateCookie(cookie)) {
continue;
}
var strippedHost = this._makeStrippedHost(cookie.host);
this._addCookie(strippedHost, cookie, hostCount);
} else
break;
}