/* 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, "bounceTrackingProtectionMode", "privacy.bounceTrackingProtection.mode", Ci.nsIBounceTrackingProtection.MODE_DISABLED ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "permissionManagerIsolateByPrivateBrowsing", "permissions.isolateBy.privateBrowsing", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "permissionManagerIsolateByUserContext", "permissions.isolateBy.userContext", false ); /** * Adds brackets to a host if it's an IPv6 address. * @param {string} host - Host which may be an IPv6. * @returns {string} bracketed IPv6 or host if host is not an IPv6. */ function maybeFixupIpv6(host) { if (!host?.includes(":")) { return host; } // don't fixup an ipv6 that already has [...] if (host.startsWith("[") && host.endsWith("]")) { return host; } return `[${host}]`; } /** * Test if (host, OriginAttributes) or principal belong to a (schemeless) site. * Also considers partitioned storage by inspecting OriginAttributes * partitionKey. * @param options * @param {string} [options.host] - Optional host to compare to site. * @param {object} [options.originAttributes] - Optional origin attributes to * inspect for aSchemelessSite. If omitted, partitionKey and * aOriginAttributesPattern will not be matched. * @param {nsIPrincipal} [options.principal] - Optional principal to match with * aSchemelessSite and aOriginAttributesPattern. * @param {string} aSchemelessSite - Domain to check for. Must be a valid, * non-empty baseDomain string. * @param {Object} [aOriginAttributesPattern] - Additional OriginAttributes * filtering using an OriginAttributesPattern. Defaults to {} which matches all. * @returns {boolean} Whether the (host, originAttributes) or principal matches * the site. */ function hasSite( { host = null, originAttributes = null, principal = null }, aSchemelessSite, aOriginAttributesPattern = {} ) { if (!aSchemelessSite) { throw new Error("Missing aSchemelessSite."); } if (!host && !originAttributes && !principal) { throw new Error( "Missing host, originAttributes or principal to match with aSchemelessSite." ); } if (principal && (host || originAttributes)) { throw new Error( "Can only pass either principal or host and originAttributes." ); } // If aSchemelessSite is an IPV6 host it will have brackets. Ensure that the // passed host has brackets too before comparing. host = maybeFixupIpv6(host); // If passed a host check if it belongs ot the given site. // originAttributes is optional. Only check for match if it's passed. if ( host && Services.eTLD.hasRootDomain(host, aSchemelessSite) && (!originAttributes || ChromeUtils.originAttributesMatchPattern( originAttributes, aOriginAttributesPattern )) ) { return true; } // If passed a principal check if it belongs to the given site. Also // check if the principal's OriginAttributes match our pattern. if ( maybeFixupIpv6(principal?.baseDomain) == aSchemelessSite && ChromeUtils.originAttributesMatchPattern( principal.originAttributes, aOriginAttributesPattern ) ) { return true; } // Additionally check for partitioned state under the top level // aSchemelessSite. We need to inspect the OriginAttributes partitionKey for // that. let oa = originAttributes ?? principal?.originAttributes; if (oa == null) { // No OriginAttributes passed in to compare with. return false; } // For matching partitioned state under aSchemelessSite we use a // PartitionKeyPattern. Merge it with the aOriginAttributesPattern from the // caller. let patternWithPartitionKey = { ...aOriginAttributesPattern, partitionKeyPattern: { baseDomain: aSchemelessSite }, }; return ChromeUtils.originAttributesMatchPattern(oa, patternWithPartitionKey); } // 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. // * deleteBySite() - 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.sanitizeOnShutdown() and // Sanitizer.onStartup() 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { Services.cookies.cookies .filter(({ rawHost, originAttributes }) => hasSite( { host: rawHost, originAttributes }, aSchemelessSite, aOriginAttributesPattern ) ) .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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { let { privateBrowsingId } = aOriginAttributesPattern; try { let uri = Services.io.newURI("https://" + aSchemelessSite); // privateBrowsingId unset clears both normal and private browsing. // Otherwise only clear either normal or private browsing depending on the // value. if ( privateBrowsingId == null || privateBrowsingId === Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID ) { Services.cookieBanners.removeDomainPref(uri, false); } if ( privateBrowsingId == null || privateBrowsingId !== Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID ) { Services.cookieBanners.removeDomainPref(uri, true); } } 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { let { privateBrowsingId } = aOriginAttributesPattern; try { // privateBrowsingId unset clears both normal and private browsing. // Otherwise only clear either normal or private browsing depending on the // value if ( privateBrowsingId == null || privateBrowsingId === Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID ) { Services.cookieBanners.removeExecutedRecordForSite( aSchemelessSite, false ); } if ( privateBrowsingId == null || privateBrowsingId !== Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID ) { Services.cookieBanners.removeExecutedRecordForSite( aSchemelessSite, true ); } } 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { Services.rfp.cleanRandomKeyBySite( aSchemelessSite, aOriginAttributesPattern ); }, 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( Ci.nsICertOverrideService ); overrideService .getOverrides() .filter(({ asciiHost, originAttributes }) => hasSite( { host: asciiHost, originAttributes }, aSchemelessSite, aOriginAttributesPattern ) ) .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.clearOriginsByPrincipal(httpPrincipal); Services.cache2.clearOriginsByPrincipal(httpsPrincipal); }, async deleteBySite(aSchemelessSite, _aOriginAttributesPattern) { // TODO: aOriginAttributesPattern Services.cache2.clearBaseDomain(aSchemelessSite); }, deleteByPrincipal(aPrincipal) { return new Promise(aResolve => { Services.cache2.clearOriginsByPrincipal(aPrincipal); aResolve(); }); }, deleteByOriginAttributes(aOriginAttributesString) { return new Promise(aResolve => { Services.cache2.clearOriginsByOriginAttributes(aOriginAttributesString); aResolve(); }); }, deleteAll() { return new Promise(aResolve => { Services.cache2.clear(); aResolve(); }); }, }; const createResourceCleaner = type => ({ 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 ); this.deleteByPrincipal(httpPrincipal); this.deleteByPrincipal(httpsPrincipal); }, async deleteByPrincipal(aPrincipal) { ChromeUtils.clearResourceCache({ types: [type], principal: aPrincipal, }); }, async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { ChromeUtils.clearResourceCache({ types: [type], schemelessSite: aSchemelessSite, pattern: aOriginAttributesPattern, }); }, async deleteAll() { ChromeUtils.clearResourceCache({ types: [type], }); }, }); const CSSCacheCleaner = createResourceCleaner("stylesheet"); const JSCacheCleaner = createResourceCleaner("script"); const ImageCacheCleaner = createResourceCleaner("image"); const MessagingLayerSecurityStateCleaner = { 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.clearMessagingLayerSecurityStateByPrincipal(httpsPrincipal); // The WebAPI doesn't allow for non-secure contexts but // we are keeping this out of caution. ChromeUtils.clearMessagingLayerSecurityStateByPrincipal(httpPrincipal); }, async deleteByPrincipal(aPrincipal) { ChromeUtils.clearMessagingLayerSecurityStateByPrincipal(aPrincipal); }, async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { ChromeUtils.clearMessagingLayerSecurityStateBySite( aSchemelessSite, aOriginAttributesPattern ); }, async deleteAll() { ChromeUtils.clearMessagingLayerSecurityState(); }, }; const DownloadsCleaner = { async _deleteInternal({ host, 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 (host) { return Services.eTLD.hasRootDomain(entryURI.host, host); } if (principal) { return principal.equalsURI(entryURI); } return false; }); }, async deleteByHost(aHost, aOriginAttributes) { // Clearing by host also clears associated subdomains. return this._deleteInternal({ host: aHost, originAttributes: aOriginAttributes, }); }, deleteByPrincipal(aPrincipal) { return this._deleteInternal({ principal: aPrincipal }); }, async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { let list = await lazy.Downloads.getList(lazy.Downloads.ALL); list.removeFinished(({ source }) => { if ( "userContextId" in aOriginAttributesPattern && "userContextId" in source && aOriginAttributesPattern.userContextId != source.userContextId ) { return false; } if ( "privateBrowsingId" in aOriginAttributesPattern && !!aOriginAttributesPattern.privateBrowsingId != source.isPrivate ) { return false; } let entryURI = Services.io.newURI(source.url); return Services.eTLD.getSchemelessSite(entryURI) == aSchemelessSite; }); }, 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 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 site. async deleteBySite( _aSchemelessSite, _aOriginAttributesPattern, 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { // 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. // TODO: aOriginAttributesPattern Services.obs.notifyObservers( null, "extension:purge-localStorage", aSchemelessSite ); // Clear sessionStorage let entry = Cc["@mozilla.org/clear-by-site-entry;1"].createInstance( Ci.nsIClearBySiteEntry ); entry.schemelessSite = aSchemelessSite; // Convert the pattern to a JSON string. entry.patternJSON = JSON.stringify(aOriginAttributesPattern); Services.obs.notifyObservers(entry, "browser:purge-sessionStorage"); // Clear third-party storage partitioned under aSchemelessSite. // 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({ ...aOriginAttributesPattern, partitionKeyPattern: { baseDomain: aSchemelessSite }, }) ); // 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.removeFromSite( aSchemelessSite, aOriginAttributesPattern ); } catch (error) { swCleanupError = error; } await this._qmsClearStoragesForPrincipalsMatching(principal => hasSite({ principal }, aSchemelessSite, aOriginAttributesPattern) ); // 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 tobeRemoveDirName = "to-be-removed"; const storageName = Services.prefs.getStringPref( "dom.quotaManager.storageName" ); if (!storageName) { throw new Error("storage name must not be empty"); } const toBeRemovedDir = PathUtils.join( PathUtils.profileDir, storageName, tobeRemoveDirName ); if ( !AppConstants.MOZ_BACKGROUNDTASKS || !Services.prefs.getBoolPref("dom.quotaManager.backgroundTask.enabled") ) { // Our behavior in this case differs from our use of the background-task below because // while the background-task will only try to empty the contents of the directory but // leave the directory itself intact, our call here will remove the directory. We // remove the directory here for reasons of implementation simplicity and because // we do not have to worry about the same race that the background task has to worry // about. Specifically, the background task needs to worry about gecko restarting and // racing on QM trying to move directories into the to-be-removed directory. But as long // as we are confident QM has fully processed its I/O thread, we know it should not be // trying to move new files into it because we are in the same process. await IOUtils.remove(toBeRemovedDir, { recursive: true }); return; } // return early if directory does not exist or empty if (!(await IOUtils.hasChildren(toBeRemovedDir, { ignoreAbsent: 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 deleteBySite( _aSchemelessSite, _aOriginAttributesPattern, 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. * @param {Object} aOriginAttributesPattern - Optional pattern to filter OriginAttributes. * @returns {Promise} a promise which resolves once data has been cleared. */ _deleteByRootDomain(aDomain, aOriginAttributesPattern = null) { 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, aOriginAttributesPattern, 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) { 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.clearForPrincipal(aPrincipal, aStatus => { if (!Components.isSuccessCode(aStatus)) { aReject(); } else { aResolve(); } }); }); }, deleteBySite(aSchemelessSite, aOriginAttributesPattern) { return this._deleteByRootDomain(aSchemelessSite, aOriginAttributesPattern); }, 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("*", null, 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { // If we don't isolate by private browsing / user context we need to clear // the pattern field. Otherwise permissions returned by the permission // manager will never match. The permission manager strips these fields when // their prefs are set to `false`. if (!lazy.permissionManagerIsolateByPrivateBrowsing) { delete aOriginAttributesPattern.privateBrowsingId; } if (!lazy.permissionManagerIsolateByUserContext) { delete aOriginAttributesPattern.userContextId; } this._deleteInternal(({ principal }) => hasSite({ principal }, aSchemelessSite, aOriginAttributesPattern) ); }, 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 }); }, deleteBySite(aSchemelessSite) { return this.deleteByHost(aSchemelessSite); }, 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); }, deleteBySite(aSchemelessSite, _aOriginAttributesPattern) { // TODO: aOriginAttributesPattern. return this.deleteByHost(aSchemelessSite, {}); }, 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 deleteBySite( _aSchemelessSite, _aOriginAttributesPattern, 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 deleteBySite( _aSchemelessSite, _aOriginAttributesPattern, aIsUserRequest ) { if (!aIsUserRequest) { return; } await this.deleteAll(); }, deleteAll() { return new Promise(aResolve => { Services.obs.notifyObservers(null, "net:clear-active-logins"); aResolve(); }); }, }; // Type of the shutdown exception permission. const SHUTDOWN_EXCEPTION_PERMISSION = "cookie"; const ShutdownExceptionsCleaner = { async _deleteInternal(filter) { Services.perms.all .filter(({ type }) => type == SHUTDOWN_EXCEPTION_PERMISSION) .filter(filter) .forEach(perm => { try { Services.perms.removePermission(perm); } catch (ex) { console.error(ex); } }); }, async deleteByHost(aHost) { this._deleteInternal(({ principal }) => { let { host: principalHost } = principal; if (!principalHost?.length) { return false; } return Services.eTLD.hasRootDomain(principal.host, aHost); }); }, async deleteByPrincipal(aPrincipal) { Services.perms.removeFromPrincipal( aPrincipal, SHUTDOWN_EXCEPTION_PERMISSION ); }, async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { // If we don't isolate by private browsing / user context we need to clear // the pattern field. Otherwise permissions returned by the permission // manager will never match. The permission manager strips these fields when // their prefs are set to `false`. if (!lazy.permissionManagerIsolateByPrivateBrowsing) { delete aOriginAttributesPattern.privateBrowsingId; } if (!lazy.permissionManagerIsolateByUserContext) { delete aOriginAttributesPattern.userContextId; } this._deleteInternal(({ principal }) => hasSite({ principal }, aSchemelessSite, aOriginAttributesPattern) ); }, async deleteByRange(aFrom) { Services.perms.removeByTypeSince( SHUTDOWN_EXCEPTION_PERMISSION, aFrom / 1000 ); }, async deleteByOriginAttributes(aOriginAttributesString) { Services.perms.removePermissionsWithAttributes( aOriginAttributesString, [SHUTDOWN_EXCEPTION_PERMISSION], [] ); }, async deleteAll() { Services.perms.removeByType(SHUTDOWN_EXCEPTION_PERMISSION); }, }; const PermissionsCleaner = { _deleteInternal(filter) { Services.perms.all // Skip shutdown exception permission because it is handled by ShutDownExceptionsCleaner .filter(({ type }) => type != SHUTDOWN_EXCEPTION_PERMISSION) .filter(filter) .forEach(perm => { try { Services.perms.removePermission(perm); } catch (ex) { console.error(ex); } }); }, _thirdPartyStoragePermissionMatchesHost(permissionType, aHost) { if ( !permissionType.startsWith("3rdPartyStorage^") && !permissionType.startsWith("3rdPartyFrameStorage^") ) { return false; } let [, site] = permissionType.split("^"); let uri; try { uri = Services.io.newURI(site); } catch (ex) { return false; } return Services.eTLD.hasRootDomain(uri.host, aHost); }, _getPrincipalHost(principal) { try { return principal.host; } catch (e) { return null; } }, async deleteByHost(aHost) { this._deleteInternal(({ principal, type }) => { let principalHost = this._getPrincipalHost(principal); if (!principalHost?.length) { return false; } if (Services.eTLD.hasRootDomain(principalHost, aHost)) { return true; } return this._thirdPartyStoragePermissionMatchesHost(type, aHost); }); }, async deleteByPrincipal(aPrincipal) { this._deleteInternal(({ principal, type }) => { if (principal.equals(aPrincipal)) { return true; } let principalHost = this._getPrincipalHost(aPrincipal); if (!principalHost?.length) { return false; } return this._thirdPartyStoragePermissionMatchesHost(type, principalHost); }); }, async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { // If we don't isolate by private browsing / user context we need to clear // the pattern field. Otherwise permissions returned by the permission // manager will never match. The permission manager strips these fields when // their prefs are set to `false`. if (!lazy.permissionManagerIsolateByPrivateBrowsing) { delete aOriginAttributesPattern.privateBrowsingId; } if (!lazy.permissionManagerIsolateByUserContext) { delete aOriginAttributesPattern.userContextId; } this._deleteInternal( ({ principal, type }) => hasSite({ principal }, aSchemelessSite, aOriginAttributesPattern) || this._thirdPartyStoragePermissionMatchesHost(type, aSchemelessSite) ); }, async deleteByRange(aFrom) { Services.perms.removeAllSinceWithTypeExceptions(aFrom / 1000, [ SHUTDOWN_EXCEPTION_PERMISSION, ]); }, async deleteByOriginAttributes(aOriginAttributesString) { Services.perms.removePermissionsWithAttributes( aOriginAttributesString, [], [SHUTDOWN_EXCEPTION_PERMISSION] ); }, async deleteAll() { Services.perms.removeAllExceptTypes([SHUTDOWN_EXCEPTION_PERMISSION]); }, }; const PreferencesCleaner = { deleteByHost(aHost, aOriginAttributes = {}) { aOriginAttributes = ChromeUtils.fillNonDefaultOriginAttributes(aOriginAttributes); let loadContext; if ( aOriginAttributes.privateBrowsingId == Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID ) { loadContext = Cu.createLoadContext(); } else { loadContext = Cu.createPrivateLoadContext(); } // 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, loadContext, { handleCompletion: aReason => { if (aReason === cps2.COMPLETE_ERROR) { aReject(); } else { aResolve(); } }, }); }); }, deleteByPrincipal(aPrincipal) { return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes); }, async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { // If aOriginAttributesPattern does not specify private or normal browsing // clear both. let loadContext = null; // If the pattern filters by normal or private browsing mode only clear that mode. if (aOriginAttributesPattern.privateBrowsingId != null) { // The default private browsing ID is 0 which is non private browsing mode // / normal mode. let isPrivateBrowsing = aOriginAttributesPattern.privateBrowsingId != Ci.nsIScriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID; loadContext = isPrivateBrowsing ? Cu.createPrivateLoadContext() : Cu.createLoadContext(); } let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); await new Promise((aResolve, aReject) => { cps2.removeBySubdomain(aSchemelessSite, loadContext, { handleCompletion: aReason => { if (aReason === cps2.COMPLETE_ERROR) { aReject(); } else { aResolve(); } }, }); }); }, async deleteByRange(aFrom) { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); await new Promise((aResolve, aReject) => { cps2.removeAllDomainsSince(aFrom / 1000, null, { handleCompletion: aReason => { if (aReason === cps2.COMPLETE_ERROR) { aReject(); } else { aResolve(); } }, }); }); }, async deleteAll() { let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( Ci.nsIContentPrefService2 ); await new Promise((aResolve, aReject) => { cps2.removeAllDomains(null, { handleCompletion: aReason => { if (aReason === cps2.COMPLETE_ERROR) { aReject(); } else { aResolve(); } }, }); }); }, }; 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { 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 hasSite( { host: asciiHost, originAttributes, }, aSchemelessSite, aOriginAttributesPattern ); }) .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 ); }, deleteByPrincipal(aPrincipal) { return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes); }, async deleteBySite(aSchemelessSite, _aOriginAttributesPattern) { // TODO: aOriginAttributesPattern. 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://" + aSchemelessSite); 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 deleteBySite(aSchemelessSite, _aOriginAttributesPattern) { // TODO: aOriginAttributesPattern. let mps = Cc["@mozilla.org/gecko-media-plugin-service;1"].getService( Ci.mozIGeckoMediaPluginChromeService ); mps.forgetThisBaseDomain(aSchemelessSite); }, 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); }, deleteBySite(aSchemelessSite, _aOriginAttributesPattern) { // TODO: aOriginAttributesPattern. return this.deleteByHost(aSchemelessSite, {}); }, 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 deleteBySite( _aSchemelessSite, _aOriginAttributesPattern, 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 deleteBySite( _aSchemelessSite, _aOriginAttributesPattern, 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 deleteBySite( _aSchemelessSite, _aOriginAttributesPattern, 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 deleteBySite( aSchemelessSite, _aOriginAttributesPattern, aIsUserRequest ) { // TODO: aOriginAttributesPattern. if (!aIsUserRequest) { return; } if ( Services.prefs.getBoolPref( "dom.security.credentialmanagement.identity.enabled", false ) ) { lazy.IdentityCredentialStorageService.deleteFromBaseDomain( aSchemelessSite ); } }, 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.bounceTrackingProtectionMode == Ci.nsIBounceTrackingProtection.MODE_DISABLED ) { return; } lazy.bounceTrackingProtection.clearAll(); }, async deleteByPrincipal(aPrincipal) { if ( lazy.bounceTrackingProtectionMode == Ci.nsIBounceTrackingProtection.MODE_DISABLED ) { return; } let { baseDomain, originAttributes } = aPrincipal; lazy.bounceTrackingProtection.clearBySiteHostAndOriginAttributes( baseDomain, originAttributes ); }, async deleteBySite(aSchemelessSite, aOriginAttributesPattern) { if ( lazy.bounceTrackingProtectionMode == Ci.nsIBounceTrackingProtection.MODE_DISABLED ) { return; } lazy.bounceTrackingProtection.clearBySiteHostAndOriginAttributesPattern( aSchemelessSite, aOriginAttributesPattern ); }, async deleteByRange(aFrom, aTo) { if ( lazy.bounceTrackingProtectionMode == Ci.nsIBounceTrackingProtection.MODE_DISABLED ) { return; } lazy.bounceTrackingProtection.clearByTimeRange(aFrom, aTo); }, async deleteByHost(aHost, aOriginAttributesPattern = {}) { if ( lazy.bounceTrackingProtectionMode == Ci.nsIBounceTrackingProtection.MODE_DISABLED ) { return; } let baseDomain = Services.eTLD.getSchemelessSiteFromHost(aHost); lazy.bounceTrackingProtection.clearBySiteHostAndOriginAttributesPattern( baseDomain, aOriginAttributesPattern ); }, async deleteByOriginAttributes(aOriginAttributesPatternString) { if ( lazy.bounceTrackingProtectionMode == Ci.nsIBounceTrackingProtection.MODE_DISABLED ) { return; } 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 // Convert aFrom from microseconds to ms Services.perms.removeByTypeSince("storage-access", aFrom / 1000); let persistentStoragePermissions = Services.perms.getAllByTypeSince( "persistent-storage", aFrom / 1000 ); persistentStoragePermissions.forEach(perm => { // If it is an Addon Principal, do nothing. // We want their persistant-storage permissions to remain (Bug 1907732) if (this._isAddonPrincipal(perm.principal)) { return; } Services.perms.removePermission(perm); }); }, async deleteByPrincipal(aPrincipal) { Services.perms.removeFromPrincipal(aPrincipal, "storage-access"); // Only remove persistent-storage if it is not an extension principal (Bug 1907732) if (!this._isAddonPrincipal(aPrincipal)) { 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 deleteBySite(aSchemelessSite, aOriginAttributesPattern) { // If we don't isolate by private browsing / user context we need to clear // the pattern field. Otherwise permissions returned by the permission // manager will never match. The permission manager strips these fields when // their prefs are set to `false`. if (!lazy.permissionManagerIsolateByPrivateBrowsing) { delete aOriginAttributesPattern.privateBrowsingId; } if (!lazy.permissionManagerIsolateByUserContext) { delete aOriginAttributesPattern.userContextId; } let permissions = this._getStoragePermissions(); for (let perm of permissions) { let { principal } = perm; if (hasSite({ principal }, aSchemelessSite, aOriginAttributesPattern)) { 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"); // We don't want to clear the persistent-storage permission from addons (Bug 1907732) let persistentStoragePermissions = Services.perms.getAllByTypes([ "persistent-storage", ]); persistentStoragePermissions.forEach(perm => { if (this._isAddonPrincipal(perm.principal)) { return; } Services.perms.removePermission(perm); }); }, _getStoragePermissions() { let storagePermissions = Services.perms.getAllByTypes([ "storage-access", "persistent-storage", ]); return storagePermissions.filter( permission => !this._isAddonPrincipal(permission.principal) || permission.type == "storage-access" ); }, _isAddonPrincipal(aPrincipal) { return ( // AddonPolicy() returns a WebExtensionPolicy that has been registered before, // typically during extension startup. Since Disabled or uninstalled add-ons // don't appear there, we should use schemeIs instead aPrincipal.schemeIs("moz-extension") ); }, }; // 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_MESSAGING_LAYER_SECURITY_STATE, cleaners: [MessagingLayerSecurityStateCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_JS_CACHE, cleaners: [JSCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE, cleaners: [ClientAuthRememberCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_DOWNLOADS, cleaners: [DownloadsCleaner, AboutHomeStartupCacheCleaner], }, { 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, SessionHistoryCleaner, AboutHomeStartupCacheCleaner, ], }, { flag: Ci.nsIClearDataService.CLEAR_AUTH_TOKENS, cleaners: [AuthTokensCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_AUTH_CACHE, cleaners: [AuthCacheCleaner], }, { flag: Ci.nsIClearDataService.CLEAR_SITE_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], }, { flag: Ci.nsIClearDataService.CLEAR_SHUTDOWN_EXCEPTIONS, cleaners: [ShutdownExceptionsCleaner], }, ]; 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(); }); }, deleteDataFromSite( aSchemelessSite, aOriginAttributesPattern, aIsUserRequest, aFlags, aCallback ) { if (!aSchemelessSite?.length || !aCallback) { return Cr.NS_ERROR_INVALID_ARG; } // For debug builds validate aSchemelessSite. if (AppConstants.DEBUG) { let schemelessSiteComputed = Services.eTLD.getSchemelessSiteFromHost(aSchemelessSite); if (schemelessSiteComputed != aSchemelessSite) { throw new Error( `deleteDataFromSite called with invalid aSchemelessSite '${aSchemelessSite}'. Expected site is '${schemelessSiteComputed}'` ); } } return this._deleteInternal(aFlags, aCallback, aCleaner => aCleaner.deleteBySite( aSchemelessSite, aOriginAttributesPattern, aIsUserRequest ) ); }, deleteDataFromSiteAndOriginAttributesPatternString( aSchemelessSite, aOriginAttributesPatternString, aIsUserRequest, aFlags, aCallback ) { if (!aSchemelessSite || !aCallback) { return Cr.NS_ERROR_INVALID_ARG; } // Parse the pattern string. let originAttributesPattern = {}; if (aOriginAttributesPatternString?.length) { originAttributesPattern = JSON.parse(aOriginAttributesPatternString); } // Call the other variant which expects a OriginAttributesPattern object. return this.deleteDataFromSite( aSchemelessSite, originAttributesPattern, aIsUserRequest, aFlags, aCallback ); }, 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(); } }); }, hostMatchesSite( aHost, aOriginAttributes, aSchemelessSite, aOriginAttributesPattern = {} ) { return hasSite( { host: aHost, originAttributes: aOriginAttributes }, aSchemelessSite, aOriginAttributesPattern ); }, // 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; }, });