Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
"sas",
"@mozilla.org/storage/activity-service;1",
"nsIStorageActivityService"
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"TrackingDBService",
"@mozilla.org/tracking-db-service;1",
"nsITrackingDBService"
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"IdentityCredentialStorageService",
"@mozilla.org/browser/identity-credential-storage-service;1",
"nsIIdentityCredentialStorageService"
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"bounceTrackingProtection",
"@mozilla.org/bounce-tracking-protection;1",
"nsIBounceTrackingProtection"
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"isBounceTrackingProtectionEnabled",
"privacy.bounceTrackingProtection.enabled",
false
);
/**
* Test if host, OriginAttributes or principal belong to a baseDomain. Also
* considers partitioned storage by inspecting OriginAttributes partitionKey.
* @param options
* @param {string} [options.host] - Optional host to compare to base domain.
* @param {object} [options.originAttributes] - Optional origin attributes to
* inspect for aBaseDomain. If omitted, partitionKey will not be matched.
* @param {nsIPrincipal} [options.principal] - Optional principal to compare to
* base domain.
* @param {string} aBaseDomain - Domain to check for. Must be a valid, non-empty
* baseDomain string.
* @returns {boolean} Whether the host, originAttributes or principal matches
* the base domain.
*/
function hasBaseDomain(
{ host = null, originAttributes = null, principal = null },
aBaseDomain
) {
if (!aBaseDomain) {
throw new Error("Missing baseDomain.");
}
if (!host && !originAttributes && !principal) {
throw new Error(
"Missing host, originAttributes or principal to match with baseDomain."
);
}
if (principal && (host || originAttributes)) {
throw new Error(
"Can only pass either principal or host and originAttributes."
);
}
if (host && Services.eTLD.hasRootDomain(host, aBaseDomain)) {
return true;
}
if (principal?.baseDomain == aBaseDomain) {
return true;
}
originAttributes = originAttributes || principal?.originAttributes;
if (!originAttributes) {
return false;
}
return ChromeUtils.originAttributesMatchPattern(originAttributes, {
partitionKeyPattern: { baseDomain: aBaseDomain },
});
}
/**
* Compute the base domain from a given host. This is a wrapper around
* Services.eTLD.getBaseDomainFromHost which also supports IP addresses and
* hosts such as "localhost" which are considered valid base domains for
* principals and data storage.
* @param {string} aDomainOrHost - Domain or host to be converted. May already
* be a valid base domain.
* @returns {string} Base domain of the given host. Returns aDomainOrHost if
* already a base domain.
*/
function getBaseDomainWithFallback(aDomainOrHost) {
let result = aDomainOrHost;
try {
result = Services.eTLD.getBaseDomainFromHost(aDomainOrHost);
} 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 = aDomainOrHost;
} else {
throw e;
}
}
return result;
}
// Here is a list of methods cleaners may implement. These methods must return a
// Promise object.
// * deleteAll() - this method _must_ exist. When called, it deletes all the
// data owned by the cleaner.
// * deleteByPrincipal() - this method _must_ exist.
// * deleteByBaseDomain() - this method _must_ exist.
// * deleteByHost() - this method is implemented only if the cleaner knows
// how to delete data by host + originAttributes pattern. If
// not implemented, deleteAll() will be used as fallback.
// * deleteByRange() - this method is implemented only if the cleaner knows how
// to delete data by time range. It receives 2 time range
// parameters: aFrom/aTo. If not implemented, deleteAll() is
// used as fallback.
// * deleteByLocalFiles() - this method removes data held for local files and
// other hostless origins. If not implemented,
// **no fallback is used**, as for a number of
// cleaners, no such data will ever exist and
// therefore clearing it does not make sense.
// * deleteByOriginAttributes() - this method is implemented only if the cleaner
// knows how to delete data by originAttributes
// pattern.
// * cleanupAfterDeletionAtShutdown() - this method is implemented only if the
// cleaner needs a separate step after
// deletion. No-op if not implemented.
// Currently called via
// Sanitizer.maybeSanitizeSessionPrincipals().
const CookieCleaner = {
deleteByLocalFiles(aOriginAttributes) {
return new Promise(aResolve => {
Services.cookies.removeCookiesFromExactHost(
"",
JSON.stringify(aOriginAttributes)
);
aResolve();
});
},
deleteByHost(aHost, aOriginAttributes) {
return new Promise(aResolve => {
Services.cookies.removeCookiesFromExactHost(
aHost,
JSON.stringify(aOriginAttributes)
);
aResolve();
});
},
deleteByPrincipal(aPrincipal) {
// Fall back to clearing by host and OA pattern. This will over-clear, since
// any properties that are not explicitly set in aPrincipal.originAttributes
// will be wildcard matched.
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
async deleteByBaseDomain(aDomain) {
Services.cookies.cookies
.filter(({ rawHost, originAttributes }) =>
hasBaseDomain({ host: rawHost, originAttributes }, aDomain)
)
.forEach(cookie => {
Services.cookies.removeCookiesFromExactHost(
cookie.rawHost,
JSON.stringify(cookie.originAttributes)
);
});
},
deleteByRange(aFrom) {
return Services.cookies.removeAllSince(aFrom);
},
deleteByOriginAttributes(aOriginAttributesString) {
return new Promise(aResolve => {
try {
Services.cookies.removeCookiesWithOriginAttributes(
aOriginAttributesString
);
} catch (ex) {}
aResolve();
});
},
deleteAll() {
return new Promise(aResolve => {
Services.cookies.removeAll();
aResolve();
});
},
};
// A cleaner for clearing cookie banner handling exceptions.
const CookieBannerExceptionCleaner = {
async deleteAll() {
try {
Services.cookieBanners.removeAllDomainPrefs(false);
} catch (e) {
// Don't throw an error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
async deleteByPrincipal(aPrincipal) {
try {
Services.cookieBanners.removeDomainPref(aPrincipal.URI, false);
} catch (e) {
// Don't throw an error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
async deleteByBaseDomain(aDomain) {
try {
Services.cookieBanners.removeDomainPref(
Services.io.newURI("https://" + aDomain),
false
);
} catch (e) {
// Don't throw an error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
async deleteByHost(aHost, aOriginAttributes) {
try {
let isPrivate =
!!aOriginAttributes.privateBrowsingId &&
aOriginAttributes.privateBrowsingId !==
Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID;
Services.cookieBanners.removeDomainPref(
Services.io.newURI("https://" + aHost),
isPrivate
);
} catch (e) {
// Don't throw an error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
};
// A cleaner for cleaning cookie banner handling executed records.
const CookieBannerExecutedRecordCleaner = {
async deleteAll() {
try {
Services.cookieBanners.removeAllExecutedRecords(false);
} catch (e) {
// Don't throw an error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
async deleteByPrincipal(aPrincipal) {
try {
Services.cookieBanners.removeExecutedRecordForSite(
aPrincipal.baseDomain,
false
);
} catch (e) {
// Don't throw an error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
async deleteByBaseDomain(aDomain) {
try {
Services.cookieBanners.removeExecutedRecordForSite(aDomain, false);
} catch (e) {
// Don't throw an error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
async deleteByHost(aHost, aOriginAttributes) {
try {
let isPrivate =
!!aOriginAttributes.privateBrowsingId &&
aOriginAttributes.privateBrowsingId !==
Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID;
Services.cookieBanners.removeExecutedRecordForSite(aHost, isPrivate);
} catch (e) {
// Don't throw error if the cookie banner handling is disabled.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
},
};
// A cleaner for cleaning fingerprinting protection states.
const FingerprintingProtectionStateCleaner = {
async deleteAll() {
Services.rfp.cleanAllRandomKeys();
},
async deleteByPrincipal(aPrincipal) {
Services.rfp.cleanRandomKeyByPrincipal(aPrincipal);
},
async deleteByBaseDomain(aDomain) {
Services.rfp.cleanRandomKeyByDomain(aDomain);
},
async deleteByHost(aHost, aOriginAttributesPattern) {
Services.rfp.cleanRandomKeyByHost(
aHost,
JSON.stringify(aOriginAttributesPattern)
);
},
async deleteByOriginAttributes(aOriginAttributesString) {
Services.rfp.cleanRandomKeyByOriginAttributesPattern(
aOriginAttributesString
);
},
};
const CertCleaner = {
async deleteByHost(aHost, aOriginAttributes) {
let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(
Ci.nsICertOverrideService
);
overrideService.clearValidityOverride(aHost, -1, aOriginAttributes);
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
async deleteByBaseDomain(aBaseDomain) {
let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(
Ci.nsICertOverrideService
);
overrideService
.getOverrides()
.filter(({ asciiHost }) =>
hasBaseDomain({ host: asciiHost }, aBaseDomain)
)
.forEach(({ asciiHost, port }) =>
overrideService.clearValidityOverride(asciiHost, port, {})
);
},
async deleteAll() {
let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(
Ci.nsICertOverrideService
);
overrideService.clearAllOverrides();
},
};
const NetworkCacheCleaner = {
async deleteByHost(aHost, aOriginAttributes) {
// Delete data from both HTTP and HTTPS sites.
let httpURI = Services.io.newURI("http://" + aHost);
let httpsURI = Services.io.newURI("https://" + aHost);
let httpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
httpURI,
aOriginAttributes
);
let httpsPrincipal = Services.scriptSecurityManager.createContentPrincipal(
httpsURI,
aOriginAttributes
);
Services.cache2.clearOrigin(httpPrincipal);
Services.cache2.clearOrigin(httpsPrincipal);
},
async deleteByBaseDomain(aBaseDomain) {
Services.cache2.clearBaseDomain(aBaseDomain);
},
deleteByPrincipal(aPrincipal) {
return new Promise(aResolve => {
Services.cache2.clearOrigin(aPrincipal);
aResolve();
});
},
deleteByOriginAttributes(aOriginAttributesString) {
return new Promise(aResolve => {
Services.cache2.clearOriginAttributes(aOriginAttributesString);
aResolve();
});
},
deleteAll() {
return new Promise(aResolve => {
Services.cache2.clear();
aResolve();
});
},
};
const CSSCacheCleaner = {
async deleteByHost(aHost, aOriginAttributes) {
// Delete data from both HTTP and HTTPS sites.
let httpURI = Services.io.newURI("http://" + aHost);
let httpsURI = Services.io.newURI("https://" + aHost);
let httpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
httpURI,
aOriginAttributes
);
let httpsPrincipal = Services.scriptSecurityManager.createContentPrincipal(
httpsURI,
aOriginAttributes
);
ChromeUtils.clearStyleSheetCacheByPrincipal(httpPrincipal);
ChromeUtils.clearStyleSheetCacheByPrincipal(httpsPrincipal);
},
async deleteByPrincipal(aPrincipal) {
ChromeUtils.clearStyleSheetCacheByPrincipal(aPrincipal);
},
async deleteByBaseDomain(aBaseDomain) {
ChromeUtils.clearStyleSheetCacheByBaseDomain(aBaseDomain);
},
async deleteAll() {
ChromeUtils.clearStyleSheetCache();
},
};
const ImageCacheCleaner = {
async deleteByHost(aHost, aOriginAttributes) {
let imageCache = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools)
.getImgCacheForDocument(null);
// Delete data from both HTTP and HTTPS sites.
let httpURI = Services.io.newURI("http://" + aHost);
let httpsURI = Services.io.newURI("https://" + aHost);
let httpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
httpURI,
aOriginAttributes
);
let httpsPrincipal = Services.scriptSecurityManager.createContentPrincipal(
httpsURI,
aOriginAttributes
);
imageCache.removeEntriesFromPrincipalInAllProcesses(httpPrincipal);
imageCache.removeEntriesFromPrincipalInAllProcesses(httpsPrincipal);
},
async deleteByPrincipal(aPrincipal) {
let imageCache = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools)
.getImgCacheForDocument(null);
imageCache.removeEntriesFromPrincipalInAllProcesses(aPrincipal);
},
async deleteByBaseDomain(aBaseDomain) {
let imageCache = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools)
.getImgCacheForDocument(null);
imageCache.removeEntriesFromBaseDomainInAllProcesses(aBaseDomain);
},
deleteAll() {
return new Promise(aResolve => {
let imageCache = Cc["@mozilla.org/image/tools;1"]
.getService(Ci.imgITools)
.getImgCacheForDocument(null);
imageCache.clearCache(false); // true=chrome, false=content
aResolve();
});
},
};
const DownloadsCleaner = {
async _deleteInternal({ hostOrBaseDomain, principal, originAttributes }) {
originAttributes = originAttributes || principal?.originAttributes || {};
let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
list.removeFinished(({ source }) => {
if (
"userContextId" in originAttributes &&
"userContextId" in source &&
originAttributes.userContextId != source.userContextId
) {
return false;
}
if (
"privateBrowsingId" in originAttributes &&
!!originAttributes.privateBrowsingId != source.isPrivate
) {
return false;
}
let entryURI = Services.io.newURI(source.url);
if (hostOrBaseDomain) {
return Services.eTLD.hasRootDomain(entryURI.host, hostOrBaseDomain);
}
if (principal) {
return principal.equalsURI(entryURI);
}
return false;
});
},
async deleteByHost(aHost, aOriginAttributes) {
// Clearing by host also clears associated subdomains.
return this._deleteInternal({
hostOrBaseDomain: aHost,
originAttributes: aOriginAttributes,
});
},
deleteByPrincipal(aPrincipal) {
return this._deleteInternal({ principal: aPrincipal });
},
async deleteByBaseDomain(aBaseDomain) {
return this._deleteInternal({ hostOrBaseDomain: aBaseDomain });
},
deleteByRange(aFrom, aTo) {
// Convert microseconds back to milliseconds for date comparisons.
let rangeBeginMs = aFrom / 1000;
let rangeEndMs = aTo / 1000;
return lazy.Downloads.getList(lazy.Downloads.ALL).then(aList => {
aList.removeFinished(
aDownload =>
aDownload.startTime >= rangeBeginMs &&
aDownload.startTime <= rangeEndMs
);
});
},
deleteAll() {
return lazy.Downloads.getList(lazy.Downloads.ALL).then(aList => {
aList.removeFinished(null);
});
},
};
const PasswordsCleaner = {
deleteByHost(aHost) {
// Clearing by host also clears associated subdomains.
return this._deleteInternal(aLogin =>
Services.eTLD.hasRootDomain(aLogin.hostname, aHost)
);
},
deleteByPrincipal(aPrincipal) {
// Login origins don't contain any origin attributes.
return this._deleteInternal(
aLogin => aLogin.origin == aPrincipal.originNoSuffix
);
},
deleteByBaseDomain(aBaseDomain) {
return this._deleteInternal(aLogin =>
Services.eTLD.hasRootDomain(aLogin.hostname, aBaseDomain)
);
},
deleteAll() {
return this._deleteInternal(() => true);
},
async _deleteInternal(aCb) {
try {
let logins = await Services.logins.getAllLogins();
for (let login of logins) {
if (aCb(login)) {
Services.logins.removeLogin(login);
}
}
} catch (ex) {
// XXXehsan: is there a better way to do this rather than this
// hacky comparison?
if (
!ex.message.includes("User canceled Master Password entry") &&
ex.result != Cr.NS_ERROR_NOT_IMPLEMENTED
) {
throw new Error("Exception occured in clearing passwords: " + ex);
}
}
},
};
const MediaDevicesCleaner = {
async deleteByRange(aFrom) {
let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"].getService(
Ci.nsIMediaManagerService
);
mediaMgr.sanitizeDeviceIds(aFrom);
},
// TODO: We should call the MediaManager to clear by principal, rather than
// over-clearing for user requests or bailing out for programmatic calls.
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
// TODO: Same as above, but for base domain.
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
async deleteAll() {
let mediaMgr = Cc["@mozilla.org/mediaManagerService;1"].getService(
Ci.nsIMediaManagerService
);
mediaMgr.sanitizeDeviceIds(null);
},
};
const QuotaCleaner = {
/**
* Clear quota storage for matching principals.
* @param {function} filterFn - Filter function which is passed a principal.
* Return true to clear storage for given principal or false to skip it.
* @returns {Promise} - Resolves once all matching items have been cleared.
* Rejects on error.
*/
async _qmsClearStoragesForPrincipalsMatching(filterFn) {
// Clearing quota storage by first getting all entry origins and then
// iterating over them is not ideal, since we can not ensure an entirely
// consistent clearing state. Between fetching the origins and clearing
// them, additional entries could be added. This means we could end up with
// stray entries after the clearing operation. To fix this we would need to
// move the clearing code to the QuotaManager itself which could either
// prevent new writes while clearing or clean up any additional entries
// which get written during the clearing operation.
// Performance is also not ideal, since we iterate over storage multiple
// times for this two step process.
// See Bug 1719195.
let origins = await new Promise((resolve, reject) => {
Services.qms.listOrigins().callback = request => {
if (request.resultCode != Cr.NS_OK) {
reject({ message: "Deleting quota storages failed" });
return;
}
resolve(request.result);
};
});
let clearPromises = origins
// Parse origins into principals.
.map(Services.scriptSecurityManager.createContentPrincipalFromOrigin)
// Filter out principals that don't match the filterFn.
.filter(filterFn)
// Clear quota storage by principal and collect the promises.
.map(
principal =>
new Promise((resolve, reject) => {
let clearRequest =
Services.qms.clearStoragesForPrincipal(principal);
clearRequest.callback = () => {
if (clearRequest.resultCode != Cr.NS_OK) {
reject({ message: "Deleting quota storages failed" });
return;
}
resolve();
};
})
);
return Promise.all(clearPromises);
},
deleteByPrincipal(aPrincipal) {
// localStorage: The legacy LocalStorage implementation that will
// eventually be removed depends on this observer notification to clear by
// principal.
Services.obs.notifyObservers(
null,
"extension:purge-localStorage",
aPrincipal.host
);
// Clear sessionStorage
Services.sessionStorage.clearStoragesForOrigin(aPrincipal);
// ServiceWorkers: they must be removed before cleaning QuotaManager.
return lazy.ServiceWorkerCleanUp.removeFromPrincipal(aPrincipal)
.then(
_ => /* exceptionThrown = */ false,
_ => /* exceptionThrown = */ true
)
.then(exceptionThrown => {
// QuotaManager: In the event of a failure, we call reject to propagate
// the error upwards.
return new Promise((aResolve, aReject) => {
let req = Services.qms.clearStoragesForPrincipal(aPrincipal);
req.callback = () => {
if (exceptionThrown || req.resultCode != Cr.NS_OK) {
aReject({ message: "Delete by principal failed" });
} else {
aResolve();
}
};
});
});
},
async deleteByBaseDomain(aBaseDomain) {
// localStorage: The legacy LocalStorage implementation that will
// eventually be removed depends on this observer notification to clear by
// host. Some other subsystems like Reporting headers depend on this too.
Services.obs.notifyObservers(
null,
"extension:purge-localStorage",
aBaseDomain
);
// Clear sessionStorage
Services.obs.notifyObservers(
null,
"browser:purge-sessionStorage",
aBaseDomain
);
// Clear third-party storage partitioned under aBaseDomain.
// This notification is forwarded via the StorageObserver and consumed only
// by the SessionStorageManager and (legacy) LocalStorageManager.
// There is a similar (legacy) notification "clear-origin-attributes-data"
// which additionally clears data across various other storages unrelated to
// the QuotaCleaner.
Services.obs.notifyObservers(
null,
"dom-storage:clear-origin-attributes-data",
JSON.stringify({ partitionKeyPattern: { baseDomain: aBaseDomain } })
);
// ServiceWorkers must be removed before cleaning QuotaManager. We store
// potential errors so we can re-throw later, once all operations have
// completed.
let swCleanupError;
try {
await lazy.ServiceWorkerCleanUp.removeFromBaseDomain(aBaseDomain);
} catch (error) {
swCleanupError = error;
}
await this._qmsClearStoragesForPrincipalsMatching(principal =>
hasBaseDomain({ principal }, aBaseDomain)
);
// Re-throw any service worker cleanup errors.
if (swCleanupError) {
throw swCleanupError;
}
},
async deleteByHost(aHost) {
// XXX: The aOriginAttributes is expected to always be empty({}). Maybe have
// a debug assertion here to ensure that?
// localStorage: The legacy LocalStorage implementation that will
// eventually be removed depends on this observer notification to clear by
// host. Some other subsystems like Reporting headers depend on this too.
Services.obs.notifyObservers(null, "extension:purge-localStorage", aHost);
// Clear sessionStorage
Services.obs.notifyObservers(null, "browser:purge-sessionStorage", aHost);
// ServiceWorkers must be removed before cleaning QuotaManager. We store any
// errors so we can re-throw later once all operations have completed.
let swCleanupError;
try {
await lazy.ServiceWorkerCleanUp.removeFromHost(aHost);
} catch (error) {
swCleanupError = error;
}
await this._qmsClearStoragesForPrincipalsMatching(principal => {
try {
// deleteByHost has the semantics that "foo.example.com" should be
// wiped if we are provided an aHost of "example.com".
return Services.eTLD.hasRootDomain(principal.host, aHost);
} catch (e) {
// There is no host for the given principal.
return false;
}
});
// Re-throw any service worker cleanup errors.
if (swCleanupError) {
throw swCleanupError;
}
},
deleteByRange(aFrom, aTo) {
let principals = lazy.sas
.getActiveOrigins(aFrom, aTo)
.QueryInterface(Ci.nsIArray);
let promises = [];
for (let i = 0; i < principals.length; ++i) {
let principal = principals.queryElementAt(i, Ci.nsIPrincipal);
if (
!principal.schemeIs("http") &&
!principal.schemeIs("https") &&
!principal.schemeIs("file")
) {
continue;
}
promises.push(this.deleteByPrincipal(principal));
}
return Promise.all(promises);
},
deleteByOriginAttributes(aOriginAttributesString) {
// The legacy LocalStorage implementation that will eventually be removed.
// And it should've been cleared while notifying observers with
// clear-origin-attributes-data.
return lazy.ServiceWorkerCleanUp.removeFromOriginAttributes(
aOriginAttributesString
)
.then(
_ => /* exceptionThrown = */ false,
_ => /* exceptionThrown = */ true
)
.then(() => {
// QuotaManager: In the event of a failure, we call reject to propagate
// the error upwards.
return new Promise((aResolve, aReject) => {
let req = Services.qms.clearStoragesForOriginAttributesPattern(
aOriginAttributesString
);
req.callback = () => {
if (req.resultCode == Cr.NS_OK) {
aResolve();
} else {
aReject({ message: "Delete by origin attributes failed" });
}
};
});
});
},
async deleteAll() {
// localStorage
Services.obs.notifyObservers(null, "extension:purge-localStorage");
// sessionStorage
Services.obs.notifyObservers(null, "browser:purge-sessionStorage");
// ServiceWorkers must be removed before cleaning QuotaManager. We store any
// errors so we can re-throw later once all operations have completed.
let swCleanupError;
try {
await lazy.ServiceWorkerCleanUp.removeAll();
} catch (error) {
swCleanupError = error;
}
await this._qmsClearStoragesForPrincipalsMatching(
principal =>
principal.schemeIs("http") ||
principal.schemeIs("https") ||
principal.schemeIs("file")
);
// Re-throw any service worker cleanup errors.
if (swCleanupError) {
throw swCleanupError;
}
},
async cleanupAfterDeletionAtShutdown() {
const toBeRemovedDir = PathUtils.join(
PathUtils.profileDir,
Services.prefs.getStringPref("dom.quotaManager.storageName"),
"to-be-removed"
);
if (
!AppConstants.MOZ_BACKGROUNDTASKS ||
!Services.prefs.getBoolPref("dom.quotaManager.backgroundTask.enabled")
) {
await IOUtils.remove(toBeRemovedDir, { recursive: true });
return;
}
const runner = Cc["@mozilla.org/backgroundtasksrunner;1"].getService(
Ci.nsIBackgroundTasksRunner
);
runner.removeDirectoryInDetachedProcess(
toBeRemovedDir,
"",
"0",
"*", // wildcard
"Quota"
);
},
};
const PredictorNetworkCleaner = {
async deleteAll() {
// Predictive network data - like cache, no way to clear this per
// domain, so just trash it all
let np = Cc["@mozilla.org/network/predictor;1"].getService(
Ci.nsINetworkPredictor
);
np.reset();
},
// TODO: We should call the NetworkPredictor to clear by principal, rather
// than over-clearing for user requests or bailing out for programmatic calls.
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
// TODO: Same as above, but for base domain.
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
};
const PushNotificationsCleaner = {
/**
* Clear entries for aDomain including subdomains of aDomain.
* @param {string} aDomain - Domain to clear data for.
* @returns {Promise} a promise which resolves once data has been cleared.
*/
_deleteByRootDomain(aDomain) {
if (!Services.prefs.getBoolPref("dom.push.enabled", false)) {
return Promise.resolve();
}
return new Promise((aResolve, aReject) => {
let push = Cc["@mozilla.org/push/Service;1"].getService(
Ci.nsIPushService
);
// ClearForDomain also clears subdomains.
push.clearForDomain(aDomain, aStatus => {
if (!Components.isSuccessCode(aStatus)) {
aReject();
} else {
aResolve();
}
});
});
},
deleteByHost(aHost) {
// Will also clear entries for subdomains of aHost. Data is cleared across
// all origin attributes.
return this._deleteByRootDomain(aHost);
},
deleteByPrincipal(aPrincipal) {
// Will also clear entries for subdomains of the principal host. Data is
// cleared across all origin attributes.
return this._deleteByRootDomain(aPrincipal.host);
},
deleteByBaseDomain(aBaseDomain) {
return this._deleteByRootDomain(aBaseDomain);
},
deleteAll() {
if (!Services.prefs.getBoolPref("dom.push.enabled", false)) {
return Promise.resolve();
}
return new Promise((aResolve, aReject) => {
let push = Cc["@mozilla.org/push/Service;1"].getService(
Ci.nsIPushService
);
push.clearForDomain("*", aStatus => {
if (!Components.isSuccessCode(aStatus)) {
aReject();
} else {
aResolve();
}
});
});
},
};
const StorageAccessCleaner = {
// This is a special function to implement deleteUserInteractionForClearingHistory.
async deleteExceptPrincipals(aPrincipalsWithStorage, aFrom) {
// We compare by base domain in order to simulate the behavior
// from purging, Consider a scenario where the user is logged
// into sub.example.com but the cookies are on example.com. In this
// case, we will remove the user interaction for sub.example.com
// because its principal does not match the one with storage.
let baseDomainsWithStorage = new Set();
for (let principal of aPrincipalsWithStorage) {
baseDomainsWithStorage.add(principal.baseDomain);
}
for (let perm of Services.perms.getAllByTypeSince(
"storageAccessAPI",
// The permission manager uses milliseconds instead of microseconds
aFrom / 1000
)) {
if (!baseDomainsWithStorage.has(perm.principal.baseDomain)) {
Services.perms.removePermission(perm);
}
}
},
async deleteByPrincipal(aPrincipal) {
return Services.perms.removeFromPrincipal(aPrincipal, "storageAccessAPI");
},
_deleteInternal(filter) {
Services.perms.all
.filter(({ type }) => type == "storageAccessAPI")
.filter(filter)
.forEach(perm => {
try {
Services.perms.removePermission(perm);
} catch (ex) {
console.error(ex);
}
});
},
async deleteByHost(aHost) {
// Clearing by host also clears associated subdomains.
this._deleteInternal(({ principal }) => {
let toBeRemoved = false;
try {
toBeRemoved = Services.eTLD.hasRootDomain(principal.host, aHost);
} catch (ex) {}
return toBeRemoved;
});
},
async deleteByBaseDomain(aBaseDomain) {
this._deleteInternal(
({ principal }) => principal.baseDomain == aBaseDomain
);
},
async deleteByRange(aFrom) {
Services.perms.removeByTypeSince("storageAccessAPI", aFrom / 1000);
},
async deleteAll() {
Services.perms.removeByType("storageAccessAPI");
},
};
const HistoryCleaner = {
deleteByHost(aHost) {
if (!AppConstants.MOZ_PLACES) {
return Promise.resolve();
}
return lazy.PlacesUtils.history.removeByFilter({ host: "." + aHost });
},
deleteByPrincipal(aPrincipal) {
if (!AppConstants.MOZ_PLACES) {
return Promise.resolve();
}
return lazy.PlacesUtils.history.removeByFilter({ host: aPrincipal.host });
},
deleteByBaseDomain(aBaseDomain) {
return this.deleteByHost(aBaseDomain, {});
},
deleteByRange(aFrom, aTo) {
if (!AppConstants.MOZ_PLACES) {
return Promise.resolve();
}
return lazy.PlacesUtils.history.removeVisitsByFilter({
beginDate: new Date(aFrom / 1000),
endDate: new Date(aTo / 1000),
});
},
deleteAll() {
if (!AppConstants.MOZ_PLACES) {
return Promise.resolve();
}
return lazy.PlacesUtils.history.clear();
},
};
const SessionHistoryCleaner = {
async deleteByHost(aHost) {
// Session storage and history also clear subdomains of aHost.
Services.obs.notifyObservers(null, "browser:purge-sessionStorage", aHost);
Services.obs.notifyObservers(
null,
"browser:purge-session-history-for-domain",
aHost
);
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
deleteByBaseDomain(aBaseDomain) {
return this.deleteByHost(aBaseDomain, {});
},
async deleteByRange(aFrom) {
Services.obs.notifyObservers(
null,
"browser:purge-session-history",
String(aFrom)
);
},
async deleteAll() {
Services.obs.notifyObservers(null, "browser:purge-session-history");
},
};
const AuthTokensCleaner = {
// TODO: Bug 1726742
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
// TODO: Bug 1726742
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
async deleteAll() {
let sdr = Cc["@mozilla.org/security/sdr;1"].getService(
Ci.nsISecretDecoderRing
);
sdr.logoutAndTeardown();
},
};
const AuthCacheCleaner = {
// TODO: Bug 1726743
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
// TODO: Bug 1726743
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
deleteAll() {
return new Promise(aResolve => {
Services.obs.notifyObservers(null, "net:clear-active-logins");
aResolve();
});
},
};
const PermissionsCleaner = {
/**
* Delete permissions by either base domain or host.
* Clearing by host also clears associated subdomains.
* For example, clearing "example.com" will also clear permissions for
* "test.example.com" and "another.test.example.com".
* @param options
* @param {string} options.baseDomain - Base domain to delete permissions for.
* @param {string} options.host - Host to delete permissions for.
*/
async _deleteInternal({ baseDomain, host }) {
for (let perm of Services.perms.all) {
let toBeRemoved;
if (baseDomain) {
toBeRemoved = perm.principal.baseDomain == baseDomain;
} else {
try {
toBeRemoved = Services.eTLD.hasRootDomain(perm.principal.host, host);
} catch (ex) {
continue;
}
}
if (
!toBeRemoved &&
(perm.type.startsWith("3rdPartyStorage^") ||
perm.type.startsWith("3rdPartyFrameStorage^"))
) {
let parts = perm.type.split("^");
let uri;
try {
uri = Services.io.newURI(parts[1]);
} catch (ex) {
continue;
}
toBeRemoved = Services.eTLD.hasRootDomain(uri.host, baseDomain || host);
}
if (!toBeRemoved) {
continue;
}
try {
Services.perms.removePermission(perm);
} catch (ex) {
// Ignore entry
}
}
},
deleteByHost(aHost) {
return this._deleteInternal({ host: aHost });
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
deleteByBaseDomain(aBaseDomain) {
return this._deleteInternal({ baseDomain: aBaseDomain });
},
async deleteByRange(aFrom) {
Services.perms.removeAllSince(aFrom / 1000);
},
async deleteByOriginAttributes(aOriginAttributesString) {
Services.perms.removePermissionsWithAttributes(aOriginAttributesString);
},
async deleteAll() {
Services.perms.removeAll();
},
};
const PreferencesCleaner = {
deleteByHost(aHost) {
// Also clears subdomains of aHost.
return new Promise((aResolve, aReject) => {
let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
Ci.nsIContentPrefService2
);
cps2.removeBySubdomain(aHost, null, {
handleCompletion: aReason => {
if (aReason === cps2.COMPLETE_ERROR) {
aReject();
} else {
aResolve();
}
},
handleError() {},
});
});
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
deleteByBaseDomain(aBaseDomain) {
return this.deleteByHost(aBaseDomain, {});
},
async deleteByRange(aFrom) {
let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
Ci.nsIContentPrefService2
);
cps2.removeAllDomainsSince(aFrom / 1000, null);
},
async deleteAll() {
let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
Ci.nsIContentPrefService2
);
cps2.removeAllDomains(null);
},
};
const ClientAuthRememberCleaner = {
async deleteByHost(aHost, aOriginAttributes) {
let cars = Cc[
"@mozilla.org/security/clientAuthRememberService;1"
].getService(Ci.nsIClientAuthRememberService);
cars.deleteDecisionsByHost(aHost, aOriginAttributes);
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
async deleteByBaseDomain(aDomain) {
let cars = Cc[
"@mozilla.org/security/clientAuthRememberService;1"
].getService(Ci.nsIClientAuthRememberService);
cars
.getDecisions()
.filter(({ asciiHost, entryKey }) => {
// Get the origin attributes which are in the third component of the
// entryKey. ',' is used as the delimiter.
let originSuffixEncoded = entryKey.split(",")[2];
let originAttributes;
if (originSuffixEncoded) {
try {
// Decoding the suffix or parsing the origin attributes can fail. In
// this case we won't match the partitionKey, but we can still match
// the asciiHost.
let originSuffix = decodeURIComponent(originSuffixEncoded);
originAttributes =
ChromeUtils.CreateOriginAttributesFromOriginSuffix(originSuffix);
} catch (e) {
console.error(e);
}
}
return hasBaseDomain(
{
host: asciiHost,
originAttributes,
},
aDomain
);
})
.forEach(({ entryKey }) => cars.forgetRememberedDecision(entryKey));
},
async deleteAll() {
let cars = Cc[
"@mozilla.org/security/clientAuthRememberService;1"
].getService(Ci.nsIClientAuthRememberService);
cars.clearRememberedDecisions();
},
};
const HSTSCleaner = {
async deleteByHost(aHost, aOriginAttributes) {
let sss = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
let uri = Services.io.newURI("https://" + aHost);
sss.resetState(
uri,
aOriginAttributes,
Ci.nsISiteSecurityService.RootDomain
);
},
/**
* Adds brackets to a site if it's an IPv6 address.
* @param {string} aSite - (schemeless) site which may be an IPv6.
* @returns {string} bracketed IPv6 or site if site is not an IPv6.
*/
_maybeFixIpv6Site(aSite) {
// Not an IPv6 or already has brackets.
if (!aSite.includes(":") || aSite[0] == "[") {
return aSite;
}
return `[${aSite}]`;
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
async deleteByBaseDomain(aDomain) {
let sss = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
// Add brackets to IPv6 sites to ensure URI creation succeeds.
let uri = Services.io.newURI("https://" + this._maybeFixIpv6Site(aDomain));
sss.resetState(uri, {}, Ci.nsISiteSecurityService.BaseDomain);
},
async deleteAll() {
// Clear site security settings - no support for ranges in this
// interface either, so we clearAll().
let sss = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
sss.clearAll();
},
};
const EMECleaner = {
async deleteByHost(aHost, aOriginAttributes) {
let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(
Ci.mozIGeckoMediaPluginChromeService
);
mps.forgetThisSite(aHost, JSON.stringify(aOriginAttributes));
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
async deleteByBaseDomain(aBaseDomain) {
let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].getService(
Ci.mozIGeckoMediaPluginChromeService
);
mps.forgetThisBaseDomain(aBaseDomain);
},
deleteAll() {
// Not implemented.
return Promise.resolve();
},
};
const ReportsCleaner = {
deleteByHost(aHost) {
// Also clears subdomains of aHost.
return new Promise(aResolve => {
Services.obs.notifyObservers(null, "reporting:purge-host", aHost);
aResolve();
});
},
deleteByPrincipal(aPrincipal) {
return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
},
deleteByBaseDomain(aBaseDomain) {
return this.deleteByHost(aBaseDomain, {});
},
deleteAll() {
return new Promise(aResolve => {
Services.obs.notifyObservers(null, "reporting:purge-all");
aResolve();
});
},
};
const ContentBlockingCleaner = {
deleteAll() {
return lazy.TrackingDBService.clearAll();
},
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
deleteByRange(aFrom) {
return lazy.TrackingDBService.clearSince(aFrom);
},
};
/**
* The about:home startup cache, if it exists, might contain information
* about where the user has been, or what they've downloaded.
*/
const AboutHomeStartupCacheCleaner = {
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
deleteAll() {
// This cleaner only makes sense on Firefox desktop, which is the only
// application that uses the about:home startup cache.
if (!AppConstants.MOZ_BUILD_APP == "browser") {
return Promise.resolve();
}
return new Promise((aResolve, aReject) => {
let lci = Services.loadContextInfo.default;
let storage = Services.cache2.diskCacheStorage(lci);
let uri = Services.io.newURI("about:home");
try {
storage.asyncDoomURI(uri, "", {
onCacheEntryDoomed(aResult) {
if (
Components.isSuccessCode(aResult) ||
aResult == Cr.NS_ERROR_NOT_AVAILABLE
) {
aResolve();
} else {
aReject({
message: "asyncDoomURI for about:home failed",
});
}
},
});
} catch (e) {
aReject({
message: "Failed to doom about:home startup cache entry",
});
}
});
},
};
const PreflightCacheCleaner = {
// TODO: Bug 1727141: We should call the cache to clear by principal, rather
// than over-clearing for user requests or bailing out for programmatic calls.
async deleteByPrincipal(aPrincipal, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
// TODO: Bug 1727141 (see deleteByPrincipal).
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
await this.deleteAll();
},
async deleteAll() {
Cc[`@mozilla.org/network/protocol;1?name=http`]
.getService(Ci.nsIHttpProtocolHandler)
.clearCORSPreflightCache();
},
};
const IdentityCredentialStorageCleaner = {
async deleteAll() {
if (
Services.prefs.getBoolPref(
"dom.security.credentialmanagement.identity.enabled",
false
)
) {
lazy.IdentityCredentialStorageService.clear();
}
},
async deleteByPrincipal(aPrincipal) {
if (
Services.prefs.getBoolPref(
"dom.security.credentialmanagement.identity.enabled",
false
)
) {
lazy.IdentityCredentialStorageService.deleteFromPrincipal(aPrincipal);
}
},
async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
if (!aIsUserRequest) {
return;
}
if (
Services.prefs.getBoolPref(
"dom.security.credentialmanagement.identity.enabled",
false
)
) {
lazy.IdentityCredentialStorageService.deleteFromBaseDomain(aBaseDomain);
}
},
async deleteByRange(aFrom, aTo) {
if (
Services.prefs.getBoolPref(
"dom.security.credentialmanagement.identity.enabled",
false
)
) {
lazy.IdentityCredentialStorageService.deleteFromTimeRange(aFrom, aTo);
}
},
async deleteByHost(aHost, aOriginAttributes) {
if (
Services.prefs.getBoolPref(
"dom.security.credentialmanagement.identity.enabled",
false
)
) {
// Delete data from both HTTP and HTTPS sites.
let httpURI = Services.io.newURI("http://" + aHost);
let httpsURI = Services.io.newURI("https://" + aHost);
let httpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
httpURI,
aOriginAttributes
);
let httpsPrincipal =
Services.scriptSecurityManager.createContentPrincipal(
httpsURI,
aOriginAttributes
);
lazy.IdentityCredentialStorageService.deleteFromPrincipal(httpPrincipal);
lazy.IdentityCredentialStorageService.deleteFromPrincipal(httpsPrincipal);
}
},
async deleteByOriginAttributes(aOriginAttributesString) {
if (
Services.prefs.getBoolPref(
"dom.security.credentialmanagement.identity.enabled",
false
)
) {
lazy.IdentityCredentialStorageService.deleteFromOriginAttributesPattern(
aOriginAttributesString
);
}
},
};
const BounceTrackingProtectionStateCleaner = {
async deleteAll() {
if (!lazy.isBounceTrackingProtectionEnabled) {
return;
}
await lazy.bounceTrackingProtection.clearAll();
},
async deleteByPrincipal(aPrincipal) {
if (!lazy.isBounceTrackingProtectionEnabled) {
return;
}
let { baseDomain, originAttributes } = aPrincipal;
await lazy.bounceTrackingProtection.clearBySiteHostAndOA(
baseDomain,
originAttributes
);
},
async deleteByBaseDomain(aBaseDomain) {
if (!lazy.isBounceTrackingProtectionEnabled) {
return;
}
await lazy.bounceTrackingProtection.clearBySiteHost(aBaseDomain);
},
async deleteByRange(aFrom, aTo) {
if (!lazy.isBounceTrackingProtectionEnabled) {
return;
}
await lazy.bounceTrackingProtection.clearByTimeRange(aFrom, aTo);
},
async deleteByHost(aHost) {
if (!lazy.isBounceTrackingProtectionEnabled) {
return;
}
let baseDomain = getBaseDomainWithFallback(aHost);
await lazy.bounceTrackingProtection.clearBySiteHost(baseDomain);
},
async deleteByOriginAttributes(aOriginAttributesPatternString) {
if (!lazy.isBounceTrackingProtectionEnabled) {
return;
}
await lazy.bounceTrackingProtection.clearByOriginAttributesPattern(
aOriginAttributesPatternString
);
},
};
const StoragePermissionsCleaner = {
async deleteByRange(aFrom) {
// We lack the ability to clear by range, but can clear from a certain time to now
// We have to divice aFrom by 1000 to convert the time from ms to microseconds
Services.perms.removeByTypeSince("storage-access", aFrom / 1000);
Services.perms.removeByTypeSince("persistent-storage", aFrom / 1000);
},
async deleteByPrincipal(aPrincipal) {
Services.perms.removeFromPrincipal(aPrincipal, "storage-access");
Services.perms.removeFromPrincipal(aPrincipal, "persistent-storage");
},
async deleteByHost(aHost) {
let permissions = this._getStoragePermissions();
for (let perm of permissions) {
if (Services.eTLD.hasRootDomain(perm.principal.host, aHost)) {
Services.perms.removePermission(perm);
}
}
},
async deleteByBaseDomain(aBaseDomain) {
let permissions = this._getStoragePermissions();
for (let perm of permissions) {
if (perm.principal.baseDomain == aBaseDomain) {
Services.perms.removePermission(perm);
}
}
},
async deleteByLocalFiles() {
let permissions = this._getStoragePermissions();
for (let perm of permissions) {
if (perm.principal.schemeIs("file")) {
Services.perms.removePermission(perm);
}
}
},
async deleteAll() {
Services.perms.removeByType("storage-access");
Services.perms.removeByType("persistent-storage");
},
_getStoragePermissions() {
return Services.perms.getAllByTypes([
"storage-access",
"persistent-storage",
]);
},
};
// Here the map of Flags-Cleaners.
const FLAGS_MAP = [
{
flag: Ci.nsIClearDataService.CLEAR_CERT_EXCEPTIONS,
cleaners: [CertCleaner],
},
{ flag: Ci.nsIClearDataService.CLEAR_COOKIES, cleaners: [CookieCleaner] },
{
flag: Ci.nsIClearDataService.CLEAR_NETWORK_CACHE,
cleaners: [NetworkCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_IMAGE_CACHE,
cleaners: [ImageCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_CSS_CACHE,
cleaners: [CSSCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE,
cleaners: [ClientAuthRememberCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_DOWNLOADS,
cleaners: [DownloadsCleaner, AboutHomeStartupCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_PASSWORDS,
cleaners: [PasswordsCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES,
cleaners: [MediaDevicesCleaner],
},
{ flag: Ci.nsIClearDataService.CLEAR_DOM_QUOTA, cleaners: [QuotaCleaner] },
{
flag: Ci.nsIClearDataService.CLEAR_PREDICTOR_NETWORK_DATA,
cleaners: [PredictorNetworkCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_DOM_PUSH_NOTIFICATIONS,
cleaners: [PushNotificationsCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_HISTORY,
cleaners: [HistoryCleaner, AboutHomeStartupCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_SESSION_HISTORY,
cleaners: [SessionHistoryCleaner, AboutHomeStartupCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_AUTH_TOKENS,
cleaners: [AuthTokensCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
cleaners: [AuthCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_PERMISSIONS,
cleaners: [PermissionsCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_CONTENT_PREFERENCES,
cleaners: [PreferencesCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_HSTS,
cleaners: [HSTSCleaner],
},
{ flag: Ci.nsIClearDataService.CLEAR_EME, cleaners: [EMECleaner] },
{ flag: Ci.nsIClearDataService.CLEAR_REPORTS, cleaners: [ReportsCleaner] },
{
flag: Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS,
cleaners: [StorageAccessCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_CONTENT_BLOCKING_RECORDS,
cleaners: [ContentBlockingCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_PREFLIGHT_CACHE,
cleaners: [PreflightCacheCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_CREDENTIAL_MANAGER_STATE,
cleaners: [IdentityCredentialStorageCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXCEPTION,
cleaners: [CookieBannerExceptionCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD,
cleaners: [CookieBannerExecutedRecordCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE,
cleaners: [FingerprintingProtectionStateCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE,
cleaners: [BounceTrackingProtectionStateCleaner],
},
{
flag: Ci.nsIClearDataService.CLEAR_STORAGE_PERMISSIONS,
cleaners: [StoragePermissionsCleaner],
},
];
export function ClearDataService() {
this._initialize();
}
ClearDataService.prototype = Object.freeze({
classID: Components.ID("{0c06583d-7dd8-4293-b1a5-912205f779aa}"),
QueryInterface: ChromeUtils.generateQI(["nsIClearDataService"]),
_initialize() {
// Let's start all the service we need to cleanup data.
// This is mainly needed for GeckoView that doesn't start QMS on startup
// time.
if (!Services.qms) {
console.error("Failed initializiation of QuotaManagerService.");
}
},
deleteDataFromLocalFiles(aIsUserRequest, aFlags, aCallback) {
if (!aCallback) {
return Cr.NS_ERROR_INVALID_ARG;
}
return this._deleteInternal(aFlags, aCallback, aCleaner => {
// Some of the 'Cleaners' do not support clearing data for
// local files. Ignore those.
if (aCleaner.deleteByLocalFiles) {
// A generic originAttributes dictionary.
return aCleaner.deleteByLocalFiles({});
}
return Promise.resolve();
});
},
deleteDataFromHost(aHost, aIsUserRequest, aFlags, aCallback) {
if (!aHost || !aCallback) {
return Cr.NS_ERROR_INVALID_ARG;
}
return this._deleteInternal(aFlags, aCallback, aCleaner => {
// Some of the 'Cleaners' do not support to delete by principal. Let's
// use deleteAll() as fallback.
if (aCleaner.deleteByHost) {
// A generic originAttributes dictionary.
return aCleaner.deleteByHost(aHost, {});
}
// The user wants to delete data. Let's remove as much as we can.
if (aIsUserRequest) {
return aCleaner.deleteAll();
}
// We don't want to delete more than what is strictly required.
return Promise.resolve();
});
},
deleteDataFromBaseDomain(aDomainOrHost, aIsUserRequest, aFlags, aCallback) {
if (!aDomainOrHost || !aCallback) {
return Cr.NS_ERROR_INVALID_ARG;
}
// We may throw here if aDomainOrHost can't be converted to a base domain.
let baseDomain;
try {
baseDomain = getBaseDomainWithFallback(aDomainOrHost);
} catch (e) {
return Cr.NS_ERROR_FAILURE;
}
return this._deleteInternal(aFlags, aCallback, aCleaner =>
aCleaner.deleteByBaseDomain(baseDomain, aIsUserRequest)
);
},
deleteDataFromPrincipal(aPrincipal, aIsUserRequest, aFlags, aCallback) {
if (!aPrincipal || !aCallback) {
return Cr.NS_ERROR_INVALID_ARG;
}
return this._deleteInternal(aFlags, aCallback, aCleaner =>
aCleaner.deleteByPrincipal(aPrincipal, aIsUserRequest)
);
},
deleteDataInTimeRange(aFrom, aTo, aIsUserRequest, aFlags, aCallback) {
if (aFrom > aTo || !aCallback) {
return Cr.NS_ERROR_INVALID_ARG;
}
return this._deleteInternal(aFlags, aCallback, aCleaner => {
// Some of the 'Cleaners' do not support to delete by range. Let's use
// deleteAll() as fallback.
if (aCleaner.deleteByRange) {
return aCleaner.deleteByRange(aFrom, aTo);
}
// The user wants to delete data. Let's remove as much as we can.
if (aIsUserRequest) {
return aCleaner.deleteAll();
}
// We don't want to delete more than what is strictly required.
return Promise.resolve();
});
},
deleteData(aFlags, aCallback) {
if (!aCallback) {
return Cr.NS_ERROR_INVALID_ARG;
}
return this._deleteInternal(aFlags, aCallback, aCleaner => {
return aCleaner.deleteAll();
});
},
deleteDataFromOriginAttributesPattern(aPattern, aCallback) {
if (!aPattern) {
return Cr.NS_ERROR_INVALID_ARG;
}
let patternString = JSON.stringify(aPattern);
// XXXtt remove clear-origin-attributes-data entirely
Services.obs.notifyObservers(
null,
"clear-origin-attributes-data",
patternString
);
if (!aCallback) {
aCallback = {
onDataDeleted: () => {},
};
}
return this._deleteInternal(
Ci.nsIClearDataService.CLEAR_ALL,
aCallback,
aCleaner => {
if (aCleaner.deleteByOriginAttributes) {
return aCleaner.deleteByOriginAttributes(patternString);
}
// We don't want to delete more than what is strictly required.
return Promise.resolve();
}
);
},
deleteUserInteractionForClearingHistory(
aPrincipalsWithStorage,
aFrom,
aCallback
) {
if (!aCallback) {
return Cr.NS_ERROR_INVALID_ARG;
}
StorageAccessCleaner.deleteExceptPrincipals(aPrincipalsWithStorage, aFrom)
.then(() => {
aCallback.onDataDeleted(0);
})
.catch(() => {
// This is part of clearing storageAccessAPI permissions, thus return
// an appropriate error flag.
aCallback.onDataDeleted(Ci.nsIClearDataService.CLEAR_PERMISSIONS);
});
return Cr.NS_OK;
},
cleanupAfterDeletionAtShutdown(aFlags, aCallback) {
return this._deleteInternal(aFlags, aCallback, async aCleaner => {
if (aCleaner.cleanupAfterDeletionAtShutdown) {
await aCleaner.cleanupAfterDeletionAtShutdown();
}
});
},
// This internal method uses aFlags against FLAGS_MAP in order to retrieve a
// list of 'Cleaners'. For each of them, the aHelper callback retrieves a
// promise object. All these promise objects are resolved before calling
// onDataDeleted.
_deleteInternal(aFlags, aCallback, aHelper) {
let resultFlags = 0;
let promises = FLAGS_MAP.filter(c => aFlags & c.flag).map(c => {
return Promise.all(
c.cleaners.map(cleaner => {
return aHelper(cleaner).catch(e => {
console.error(e);
resultFlags |= c.flag;
});
})
);
// Let's collect the failure in resultFlags.
});
Promise.all(promises).then(() => {
aCallback.onDataDeleted(resultFlags);
});
return Cr.NS_OK;
},
});