summaryrefslogtreecommitdiffstats
path: root/toolkit/components/cleardata/ClearDataService.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/cleardata/ClearDataService.sys.mjs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/cleardata/ClearDataService.sys.mjs')
-rw-r--r--toolkit/components/cleardata/ClearDataService.sys.mjs2076
1 files changed, 2076 insertions, 0 deletions
diff --git a/toolkit/components/cleardata/ClearDataService.sys.mjs b/toolkit/components/cleardata/ClearDataService.sys.mjs
new file mode 100644
index 0000000000..ba34558f7e
--- /dev/null
+++ b/toolkit/components/cleardata/ClearDataService.sys.mjs
@@ -0,0 +1,2076 @@
+/* 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, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
+});
+
+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, aTo) {
+ 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, aOriginAttributes) {
+ // 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, aTo) {
+ 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, aOriginAttributes) {
+ // 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(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.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, aOriginAttributes) {
+ // 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, aOriginAttributes) {
+ // 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, aTo) {
+ Services.perms.removeByTypeSince("storageAccessAPI", aFrom / 1000);
+ },
+
+ async deleteAll() {
+ Services.perms.removeByType("storageAccessAPI");
+ },
+};
+
+const HistoryCleaner = {
+ deleteByHost(aHost, aOriginAttributes) {
+ 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, aOriginAttributes) {
+ // 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, aTo) {
+ 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, aOriginAttributes) {
+ return this._deleteInternal({ host: aHost });
+ },
+
+ deleteByPrincipal(aPrincipal) {
+ return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes);
+ },
+
+ deleteByBaseDomain(aBaseDomain) {
+ return this._deleteInternal({ baseDomain: aBaseDomain });
+ },
+
+ async deleteByRange(aFrom, aTo) {
+ Services.perms.removeAllSince(aFrom / 1000);
+ },
+
+ async deleteByOriginAttributes(aOriginAttributesString) {
+ Services.perms.removePermissionsWithAttributes(aOriginAttributesString);
+ },
+
+ async deleteAll() {
+ Services.perms.removeAll();
+ },
+};
+
+const PreferencesCleaner = {
+ deleteByHost(aHost, aOriginAttributes) {
+ // 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, aTo) {
+ 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, aOriginAttributes) {
+ // 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, aTo) {
+ 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, aIsUserRequest) {
+ 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, aIsUserRequest) {
+ if (!lazy.isBounceTrackingProtectionEnabled) {
+ return;
+ }
+ let { baseDomain, originAttributes } = aPrincipal;
+ await lazy.bounceTrackingProtection.clearBySiteHostAndOA(
+ baseDomain,
+ originAttributes
+ );
+ },
+
+ async deleteByBaseDomain(aBaseDomain, aIsUserRequest) {
+ 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, aOriginAttributes) {
+ 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
+ );
+ },
+};
+
+// 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],
+ },
+];
+
+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;
+ },
+});