diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/cleardata | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/cleardata')
37 files changed, 7781 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..88d1835f3c --- /dev/null +++ b/toolkit/components/cleardata/ClearDataService.sys.mjs @@ -0,0 +1,1794 @@ +/* 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" +); + +/** + * 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 }, + }); +} + +// 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(); + }); + }, +}; + +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 = 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 storageDir = PathUtils.join( + PathUtils.profileDir, + Services.prefs.getStringPref("dom.quotaManager.storageName") + ); + + if ( + !AppConstants.MOZ_BACKGROUNDTASKS || + !Services.prefs.getBoolPref("dom.quotaManager.backgroundTask.enabled") + ) { + await IOUtils.remove(PathUtils.join(storageDir, "to-be-removed"), { + recursive: true, + }); + return; + } + + const runner = Cc["@mozilla.org/backgroundtasksrunner;1"].getService( + Ci.nsIBackgroundTasksRunner + ); + + runner.removeDirectoryInDetachedProcess( + storageDir, + "to-be-removed", + "0", + "", + "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^")) { + 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 + ); + }, + + deleteByPrincipal(aPrincipal) { + return this.deleteByHost(aPrincipal.host, aPrincipal.originAttributes); + }, + + async deleteByBaseDomain(aDomain) { + let sss = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + let uri = Services.io.newURI("https://" + 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 + ); + } + }, +}; + +// 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], + }, +]; + +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 = Services.eTLD.getBaseDomainFromHost(aDomainOrHost); + + 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); + }); + 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; + }, +}); diff --git a/toolkit/components/cleardata/PrincipalsCollector.sys.mjs b/toolkit/components/cleardata/PrincipalsCollector.sys.mjs new file mode 100644 index 0000000000..2b5917c6ce --- /dev/null +++ b/toolkit/components/cleardata/PrincipalsCollector.sys.mjs @@ -0,0 +1,178 @@ +/* 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"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "serviceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +let logConsole; +function log(msg) { + if (!logConsole) { + logConsole = console.createInstance({ + prefix: "** PrincipalsCollector.jsm", + maxLogLevelPref: "browser.sanitizer.loglevel", + }); + } + + logConsole.log(msg); +} + +/** + * A helper module to collect all principals that have any of the following: + * * cookies + * * quota storage (indexedDB, localStorage) + * * service workers + * + * Note that in case of cookies, because these are not strictly associated with a + * full origin (including scheme), the https:// scheme will be used as a convention, + * so when the cookie hostname is .example.com the principal will have the origin + * https://example.com. Origin Attributes from cookies are copied to the principal. + * + * This class is not a singleton and needs to be instantiated using the constructor + * before usage. The class instance will cache the last list of principals. + * + * There is currently no `refresh` method, though you are free to add one. + */ +export class PrincipalsCollector { + // Indicating that we are in the process of collecting principals, + // that might take some time + #pendingCollection = null; + /** + * Creates a new PrincipalsCollector. + */ + constructor() { + this.principals = null; + } + + /** + * Checks whether the passed in principal has a scheme that is considered by the + * PrincipalsCollector. This is used to avoid including principals for non-web + * entities such as moz-extension. + * + * @param {nsIPrincipal} the principal to check + * @returns {boolean} + */ + static isSupportedPrincipal(principal) { + return ["http", "https", "file"].some(scheme => principal.schemeIs(scheme)); + } + + /** + * Fetches and collects all principals with cookies and/or site data (see module + * description). Originally for usage in Sanitizer.jsm to compute principals to be + * cleared on shutdown based on user settings. + * + * This operation might take a while to complete on big profiles. + * DO NOT call or await this in a way that makes it block user interaction, or you + * risk several painful seconds or possibly even minutes of lag. + * + * This function will cache its result and return the same list on second call, + * even if the actual number of principals with cookies and site data changed. + * + * @param {Object} [optional] progress A Sanitizer.jsm progress object that will be + * updated to reflect the current step of fetching principals. + * @returns {Array<nsIPrincipal>} the list of principals + */ + async getAllPrincipals(progress = {}) { + // Here is the list of principals with site data. + if (this.principals) { + return this.principals; + } + // Gathering all principals might take a while, + // to ensure this process is only done once, we queue + // the incomming calls here in case we are not yet done gathering principals + if (!this.#pendingCollection) { + this.#pendingCollection = this._getAllPrincipalsInternal(progress); + this.principals = await this.#pendingCollection; + this.#pendingCollection = null; + return this.principals; + } + await this.#pendingCollection; + return this.principals; + } + + async _getAllPrincipalsInternal(progress = {}) { + progress.step = "principals-quota-manager"; + let principals = await new Promise(resolve => { + Services.qms.listOrigins().callback = request => { + progress.step = "principals-quota-manager-listOrigins"; + if (request.resultCode != Cr.NS_OK) { + // We are probably shutting down. We don't want to propagate the + // error, rejecting the promise. + resolve([]); + return; + } + + let principalsMap = new Map(); + for (const origin of request.result) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + if (PrincipalsCollector.isSupportedPrincipal(principal)) { + principalsMap.set(principal.origin, principal); + } + } + + progress.step = "principals-quota-manager-completed"; + resolve(principalsMap); + }; + }).catch(ex => { + console.error("QuotaManagerService promise failed: ", ex); + return []; + }); + + progress.step = "principals-service-workers"; + let serviceWorkers = lazy.serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + // We don't need to check the scheme. SW are just exposed to http/https URLs. + principals.set(sw.principal.origin, sw.principal); + } + + // Let's take the list of unique hosts+OA from cookies. + progress.step = "principals-cookies"; + let cookies = Services.cookies.cookies; + let hosts = new Set(); + for (let cookie of cookies) { + hosts.add( + cookie.rawHost + + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + } + + progress.step = "principals-host-cookie"; + hosts.forEach(host => { + // Cookies and permissions are handled by origin/host. Doesn't matter if we + // use http: or https: schema here. + let principal; + try { + principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://" + host + ); + } catch (e) { + log( + `ERROR: Could not create content principal for host '${host}' ${e.message}` + ); + } + if (principal) { + principals.set(principal.origin, principal); + } + }); + + principals = Array.from(principals.values()); + progress.step = "total-principals:" + principals.length; + return principals; + } +} diff --git a/toolkit/components/cleardata/ServiceWorkerCleanUp.sys.mjs b/toolkit/components/cleardata/ServiceWorkerCleanUp.sys.mjs new file mode 100644 index 0000000000..1c49b2be08 --- /dev/null +++ b/toolkit/components/cleardata/ServiceWorkerCleanUp.sys.mjs @@ -0,0 +1,87 @@ +/* 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"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "serviceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + throw new Error( + "ServiceWorkerCleanUp.jsm can only be used in the parent process" + ); +} + +function unregisterServiceWorker(aSW) { + return new Promise(resolve => { + let unregisterCallback = { + unregisterSucceeded: resolve, + unregisterFailed: resolve, // We don't care about failures. + QueryInterface: ChromeUtils.generateQI([ + "nsIServiceWorkerUnregisterCallback", + ]), + }; + lazy.serviceWorkerManager.propagateUnregister( + aSW.principal, + unregisterCallback, + aSW.scope + ); + }); +} + +function unregisterServiceWorkersMatching(filterFn) { + let promises = []; + let serviceWorkers = lazy.serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (filterFn(sw)) { + promises.push(unregisterServiceWorker(sw)); + } + } + return Promise.all(promises); +} + +export const ServiceWorkerCleanUp = { + removeFromHost(aHost) { + return unregisterServiceWorkersMatching(sw => + Services.eTLD.hasRootDomain(sw.principal.host, aHost) + ); + }, + + removeFromBaseDomain(aBaseDomain) { + // Service workers are disabled in partitioned contexts. This means we don't + // have to check for a partitionKey, but only look at the top level base + // domain. If this ever changes we need to update this method to account for + // partitions. See Bug 1495241. + return unregisterServiceWorkersMatching( + sw => sw.principal.baseDomain == aBaseDomain + ); + }, + + removeFromPrincipal(aPrincipal) { + return unregisterServiceWorkersMatching(sw => + sw.principal.equals(aPrincipal) + ); + }, + + removeFromOriginAttributes(aOriginAttributesString) { + lazy.serviceWorkerManager.removeRegistrationsByOriginAttributes( + aOriginAttributesString + ); + return Promise.resolve(); + }, + + removeAll() { + return unregisterServiceWorkersMatching(() => true); + }, +}; diff --git a/toolkit/components/cleardata/SiteDataTestUtils.sys.mjs b/toolkit/components/cleardata/SiteDataTestUtils.sys.mjs new file mode 100644 index 0000000000..8117f23aec --- /dev/null +++ b/toolkit/components/cleardata/SiteDataTestUtils.sys.mjs @@ -0,0 +1,409 @@ +/* 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 { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +/** + * This module assists with tasks around testing functionality that shows + * or clears site data. + * + * Please note that you will have to clean up changes made manually, for + * example using SiteDataTestUtils.clear(). + */ +export var SiteDataTestUtils = { + /** + * Makes an origin have persistent data storage. + * + * @param {String} origin - the origin of the site to give persistent storage + * + * @returns a Promise that resolves when storage was persisted + */ + persist(origin, value = Services.perms.ALLOW_ACTION) { + return new Promise(resolve => { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + Services.perms.addFromPrincipal(principal, "persistent-storage", value); + Services.qms.persist(principal).callback = () => resolve(); + }); + }, + + /** + * Adds a new blob entry to a dummy indexedDB database for the specified origin. + * + * @param {String} origin - the origin of the site to add test data for + * @param {Number} size [optional] - the size of the entry in bytes + * + * @returns a Promise that resolves when the data was added successfully. + */ + addToIndexedDB(origin, size = 1024) { + return new Promise(resolve => { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1); + request.onupgradeneeded = function (e) { + let db = e.target.result; + db.createObjectStore("TestStore"); + }; + request.onsuccess = function (e) { + let db = e.target.result; + let tx = db.transaction("TestStore", "readwrite"); + let store = tx.objectStore("TestStore"); + tx.oncomplete = resolve; + let buffer = new ArrayBuffer(size); + let blob = new Blob([buffer]); + store.add(blob, Cu.now()); + }; + }); + }, + + /** + * Adds a new cookie for the specified origin or host + path + oa, with the + * specified contents. The cookie will be valid for one day. + * @param {object} options + * @param {String} [options.origin] - Origin of the site to add test data for. + * If set, overrides host, path and originAttributes args. + * @param {String} [options.host] - Host of the site to add test data for. + * @param {String} [options.path] - Path to set cookie for. + * @param {Object} [options.originAttributes] - Object of origin attributes to + * set cookie for. + * @param {String} [options.name] - Cookie name + * @param {String} [options.value] - Cookie value + */ + addToCookies({ + origin, + host, + path = "path", + originAttributes = {}, + name = "foo", + value = "bar", + }) { + if (origin) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + host = principal.host; + path = principal.URI.pathQueryRef; + originAttributes = Object.keys(originAttributes).length + ? originAttributes + : principal.originAttributes; + } + + Services.cookies.add( + host, + path, + name, + value, + false, + false, + false, + Math.floor(Date.now() / 1000) + 24 * 60 * 60, + originAttributes, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_UNSET + ); + }, + + /** + * Adds a new localStorage entry for the specified origin, with the specified contents. + * + * @param {String} origin - the origin of the site to add test data for + * @param {String} [key] - the localStorage key + * @param {String} [value] - the localStorage value + */ + addToLocalStorage(origin, key = "foo", value = "bar") { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let storage = Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + storage.setItem(key, value); + }, + + /** + * Checks whether the given origin is storing data in localStorage + * + * @param {String} origin - the origin of the site to check + * @param {{key: String, value: String}[]} [testEntries] - An array of entries + * to test for. + * + * @returns {Boolean} whether the origin has localStorage data + */ + hasLocalStorage(origin, testEntries) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + let storage = Services.domStorageManager.createStorage( + null, + principal, + principal, + "" + ); + if (!storage.length) { + return false; + } + if (!testEntries) { + return true; + } + return ( + storage.length >= testEntries.length && + testEntries.every(({ key, value }) => storage.getItem(key) == value) + ); + }, + + /** + * Adds a new serviceworker with the specified path. Note that this + * method will open a new tab at the domain of the SW path to that effect. + * + * @param {String} path - the path to the service worker to add. + * + * @returns a Promise that resolves when the service worker was registered + */ + addServiceWorker(path) { + let uri = Services.io.newURI(path); + // Register a dummy ServiceWorker. + return BrowserTestUtils.withNewTab(uri.prePath, async function (browser) { + return browser.ownerGlobal.SpecialPowers.spawn( + browser, + [{ path }], + async ({ path: p }) => { + // eslint-disable-next-line no-undef + let r = await content.navigator.serviceWorker.register(p); + return new Promise(resolve => { + let worker = r.installing || r.waiting || r.active; + if (worker.state == "activated") { + resolve(); + } else { + worker.addEventListener("statechange", () => { + if (worker.state == "activated") { + resolve(); + } + }); + } + }); + } + ); + }); + }, + + hasCookies(origin, testEntries = null, testPBMCookies = false) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + + let cookies; + if (testPBMCookies) { + // This needs to be updated when adding support for multiple PBM contexts. + let originAttributes = { privateBrowsingId: 1 }; + cookies = Services.cookies.getCookiesWithOriginAttributes( + JSON.stringify(originAttributes) + ); + } else { + cookies = Services.cookies.cookies; + } + + let filterFn = cookie => { + return ( + ChromeUtils.isOriginAttributesEqual( + principal.originAttributes, + cookie.originAttributes + ) && cookie.host.includes(principal.host) + ); + }; + + // Return on first cookie found for principal. + if (!testEntries) { + return cookies.some(filterFn); + } + + // Collect all cookies that match the principal + cookies = cookies.filter(filterFn); + + if (cookies.length < testEntries.length) { + return false; + } + + // This code isn't very efficient. It should only be used for testing + // a small amount of cookies. + return testEntries.every(({ key, value }) => + cookies.some(cookie => cookie.name == key && cookie.value == value) + ); + }, + + hasIndexedDB(origin) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + return new Promise(resolve => { + let data = true; + let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1); + request.onupgradeneeded = function (e) { + data = false; + }; + request.onsuccess = function (e) { + resolve(data); + }; + }); + }, + + _getCacheStorage(where, lci) { + switch (where) { + case "disk": + return Services.cache2.diskCacheStorage(lci); + case "memory": + return Services.cache2.memoryCacheStorage(lci); + case "pin": + return Services.cache2.pinningCacheStorage(lci); + } + return null; + }, + + hasCacheEntry(path, where, lci = Services.loadContextInfo.default) { + let storage = this._getCacheStorage(where, lci); + return storage.exists(Services.io.newURI(path), ""); + }, + + addCacheEntry(path, where, lci = Services.loadContextInfo.default) { + return new Promise(resolve => { + function CacheListener() {} + CacheListener.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + + onCacheEntryCheck(entry) { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isnew, status) { + resolve(); + }, + }; + + let storage = this._getCacheStorage(where, lci); + storage.asyncOpenURI( + Services.io.newURI(path), + "", + Ci.nsICacheStorage.OPEN_NORMALLY, + new CacheListener() + ); + }); + }, + + /** + * Checks whether the specified origin has registered ServiceWorkers. + * + * @param {String} origin - the origin of the site to check + * + * @returns {Boolean} whether or not the site has ServiceWorkers. + */ + hasServiceWorkers(origin) { + let serviceWorkers = lazy.swm.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.principal.origin == origin) { + return true; + } + } + return false; + }, + + /** + * Waits for a ServiceWorker to be registered. + * + * @param {String} the url of the ServiceWorker to wait for + * + * @returns a Promise that resolves when a ServiceWorker at the + * specified location has been registered. + */ + promiseServiceWorkerRegistered(url) { + if (!(url instanceof Ci.nsIURI)) { + url = Services.io.newURI(url); + } + + return new Promise(resolve => { + let listener = { + onRegister: registration => { + if (registration.principal.host != url.host) { + return; + } + lazy.swm.removeListener(listener); + resolve(registration); + }, + }; + lazy.swm.addListener(listener); + }); + }, + + /** + * Waits for a ServiceWorker to be unregistered. + * + * @param {String} the url of the ServiceWorker to wait for + * + * @returns a Promise that resolves when a ServiceWorker at the + * specified location has been unregistered. + */ + promiseServiceWorkerUnregistered(url) { + if (!(url instanceof Ci.nsIURI)) { + url = Services.io.newURI(url); + } + + return new Promise(resolve => { + let listener = { + onUnregister: registration => { + if (registration.principal.host != url.host) { + return; + } + lazy.swm.removeListener(listener); + resolve(registration); + }, + }; + lazy.swm.addListener(listener); + }); + }, + + /** + * Gets the current quota usage for the specified origin. + * + * @returns a Promise that resolves to an integer with the total + * amount of disk usage by a given origin. + */ + getQuotaUsage(origin) { + return new Promise(resolve => { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); + Services.qms.getUsageForPrincipal(principal, request => + resolve(request.result.usage) + ); + }); + }, + + /** + * Cleans up all site data. + */ + clear() { + return new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_COOKIES | + Ci.nsIClearDataService.CLEAR_ALL_CACHES | + Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES | + Ci.nsIClearDataService.CLEAR_DOM_STORAGES | + Ci.nsIClearDataService.CLEAR_PREDICTOR_NETWORK_DATA | + Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE | + Ci.nsIClearDataService.CLEAR_EME | + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, + resolve + ); + }); + }, +}; diff --git a/toolkit/components/cleardata/components.conf b/toolkit/components/cleardata/components.conf new file mode 100644 index 0000000000..63aa889c10 --- /dev/null +++ b/toolkit/components/cleardata/components.conf @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'js_name': 'clearData', + 'cid': '{0c06583d-7dd8-4293-b1a5-912205f779aa}', + 'contract_ids': ['@mozilla.org/clear-data-service;1'], + 'interfaces': ['nsIClearDataService'], + 'esModule': 'resource://gre/modules/ClearDataService.sys.mjs', + 'constructor': 'ClearDataService', + }, +] diff --git a/toolkit/components/cleardata/moz.build b/toolkit/components/cleardata/moz.build new file mode 100644 index 0000000000..dba93b6079 --- /dev/null +++ b/toolkit/components/cleardata/moz.build @@ -0,0 +1,36 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +TESTING_JS_MODULES += [ + "SiteDataTestUtils.sys.mjs", +] + +XPIDL_SOURCES += [ + "nsIClearDataService.idl", +] + +XPIDL_MODULE = "toolkit_cleardata" + +EXTRA_JS_MODULES += [ + "ClearDataService.sys.mjs", + "PrincipalsCollector.sys.mjs", + "ServiceWorkerCleanUp.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] + +include("/ipc/chromium/chromium-config.mozbuild") + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Data Sanitization") + +FINAL_LIBRARY = "xul" diff --git a/toolkit/components/cleardata/nsIClearDataService.idl b/toolkit/components/cleardata/nsIClearDataService.idl new file mode 100644 index 0000000000..a1dd3a83f3 --- /dev/null +++ b/toolkit/components/cleardata/nsIClearDataService.idl @@ -0,0 +1,336 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIPrincipal; +interface nsIClearDataCallback; + +/** + * nsIClearDataService + * + * Provides methods for cleaning data from a nsIPrincipal and/or from a time + * range. + */ +[scriptable, uuid(6ef3ef16-a502-4576-9fb4-919f1c40bf61)] +interface nsIClearDataService : nsISupports +{ + /** + * Delete data owned by local files or other hostless schemes. + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + Note that not all flags will make sense (e.g. we can't clear + certificates for local files). Nonsensical flags will be + ignored. + * @param aCallback this callback will be executed when the operation is + * completed. + */ + void deleteDataFromLocalFiles(in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete data owned by a host. For instance: mozilla.org. Data from any + * possible originAttributes will be deleted. + * @param aHost the host to be used. + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback this callback will be executed when the operation is + * completed. + * @deprecated Use deleteDataFromBaseDomain instead. + */ + void deleteDataFromHost(in AUTF8String aHost, + in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete data owned by or partitioned under a baseDomain (eTLD+1). For + * instance: mozilla.org. Deletes data across all origin attributes. For + * partitioned storage we clear both, data of the baseDomain in 1st-party and + * 3rd-party context. + * When handling user requests for clearing data using this method is + * preferred over deleteDataFromPrincipal, since origins may share information + * with their site (e.g. cookies) that are not deleted by principal. + * @param aDomainOrHost the domain or host to be used. Will be converted to + * baseDomain if needed. + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback this callback will be executed when the operation is + * completed. + * @throws Throws if base domain can't be computed from aDomainOrHost. Callers + * may fall back to clearing by principal or host. + */ + void deleteDataFromBaseDomain(in AUTF8String aDomainOrHost, + in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete data owned by a principal. + * @param aPrincipal the nsIPrincipal to be used. + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback ths callback will be executed when the operation is + * completed. + */ + void deleteDataFromPrincipal(in nsIPrincipal aPrincipal, + in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete all data in a time range. Limit excluded. + * @param aFrom microseconds from the epoch + * @param aTo microseconds from the epoch + * @param aIsUserRequest true if this request comes from a user interaction. + * This information is important because if true, it's probably better + * to remove more than less, for privacy reason. If false (e.g. + * Clear-Site-Data header), we don't want to delete more than what is + * strictly required. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback ths callback will be executed when the operation is + * completed. + */ + void deleteDataInTimeRange(in PRTime aFrom, in PRTime aTo, + in bool aIsUserRequest, + in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete all data from any host, in any time range. + * @param aFlags List of flags. See below the accepted values. + * @param aCallback ths callback will be executed when the operation is + * completed. + */ + void deleteData(in uint32_t aFlags, + in nsIClearDataCallback aCallback); + + /** + * Delete all data from an OriginAttributesPatternDictionary. + * @param aOriginAttributesPattern the originAttributes dictionary. + * @param aCallback the optional callback will be executed when the operation + * is completed. + */ + void deleteDataFromOriginAttributesPattern(in jsval aOriginAttributesPattern, + [optional] in nsIClearDataCallback aCallback); + + /** + * This is a helper function to clear storageAccessAPI permissions + * in a way that will not result in users getting logged out by + * cookie purging. To that end we only clear permissions for principals + * whose base domain does not have any storage associated with it. + * + * The principals to be considered will need to be passed by the API consumer. + * It is recommended to use PrincipalsCollector.jsm for that. + * + * @param aPrincipalsWithStorage principals to be excluded from clearing + * @param aFrom microseconds from the epoch + * @param aCallback the optional callback will be executed when the operation + * is completed. + */ + void deleteUserInteractionForClearingHistory(in Array<nsIPrincipal> aPrincipalsWithStorage, + [optional] in PRTime aFrom, + [optional] in nsIClearDataCallback aCallback); + + /** + * Some cleaners, namely QuotaCleaner, can opt in and treat things as deleted + * without actually removing files at shutdown. This function will trigger + * actual removal of them. + */ + void cleanupAfterDeletionAtShutdown(in uint32_t aFlags, in nsIClearDataCallback aCallback); + + /************************************************************************** + * Listed below are the various flags which may be or'd together. + */ + + /** + * Delete cookies. + */ + const uint32_t CLEAR_COOKIES = 1 << 0; + + /** + * Network Cache. + */ + const uint32_t CLEAR_NETWORK_CACHE = 1 << 1; + + /** + * Image cache. + */ + const uint32_t CLEAR_IMAGE_CACHE = 1 << 2; + + /** + * Completed downloads. + */ + const uint32_t CLEAR_DOWNLOADS = 1 << 4; + + /** + * Stored passwords. + */ + const uint32_t CLEAR_PASSWORDS = 1 << 5; + + /** + * Media devices. + */ + const uint32_t CLEAR_MEDIA_DEVICES = 1 << 6; + + /** + * LocalStorage, IndexedDB, ServiceWorkers, DOM Cache and so on. + */ + const uint32_t CLEAR_DOM_QUOTA = 1 << 7; + + /** + * Predictor network data + */ + const uint32_t CLEAR_PREDICTOR_NETWORK_DATA = 1 << 8; + + /** + * DOM Push notifications + */ + const uint32_t CLEAR_DOM_PUSH_NOTIFICATIONS = 1 << 9; + + /** + * Places history + */ + const uint32_t CLEAR_HISTORY = 1 << 10; + + /** + * Session history + */ + const uint32_t CLEAR_SESSION_HISTORY = 1 << 11; + + /** + * Auth tokens + */ + const uint32_t CLEAR_AUTH_TOKENS = 1 << 12; + + /** + * Login cache + */ + const uint32_t CLEAR_AUTH_CACHE = 1 << 13; + + /** + * Site permissions + */ + const uint32_t CLEAR_PERMISSIONS = 1 << 14; + + /** + * Site preferences + */ + const uint32_t CLEAR_CONTENT_PREFERENCES = 1 << 15; + + /** + * Clear HSTS data + */ + const uint32_t CLEAR_HSTS = 1 << 16; + + /** + * Media plugin data + */ + const uint32_t CLEAR_EME = 1 << 17; + + /** + * Reporting API reports. + */ + const uint32_t CLEAR_REPORTS = 1 << 18; + + /** + * StorageAccessAPI flag, which indicates user interaction. + */ + const uint32_t CLEAR_STORAGE_ACCESS = 1 << 19; + + /** + * Clear Cert Exceptions. + */ + const uint32_t CLEAR_CERT_EXCEPTIONS = 1 << 20; + + /** + * Clear entries in the content blocking database. + */ + const uint32_t CLEAR_CONTENT_BLOCKING_RECORDS = 1 << 21; + + /** + * Clear the in-memory CSS cache. + */ + const uint32_t CLEAR_CSS_CACHE = 1 << 22; + + /** + * Clear the CORS preflight cache. + */ + const uint32_t CLEAR_PREFLIGHT_CACHE = 1 << 23; + + /** + * Forget descision about clients authentification certificate + */ + const uint32_t CLEAR_CLIENT_AUTH_REMEMBER_SERVICE = 1 << 24; + + /** + * Clear state associated with FedCM + */ + const uint32_t CLEAR_CREDENTIAL_MANAGER_STATE = 1 << 24; + + /** + * Use this value to delete all the data. + */ + const uint32_t CLEAR_ALL = 0xFFFFFFFF; + + /************************************************************************** + * The following flags are helpers: they combine some of the previous flags + * in a more convenient way. + */ + + /** + * Delete all the possible caches. + */ + const uint32_t CLEAR_ALL_CACHES = CLEAR_NETWORK_CACHE | CLEAR_IMAGE_CACHE | + CLEAR_CSS_CACHE | CLEAR_PREFLIGHT_CACHE | CLEAR_HSTS; + + /** + * Delete all DOM storages + */ + const uint32_t CLEAR_DOM_STORAGES = CLEAR_DOM_QUOTA | CLEAR_DOM_PUSH_NOTIFICATIONS | CLEAR_REPORTS; + + /** + * Helper flag for forget about site + */ + const uint32_t CLEAR_FORGET_ABOUT_SITE = + CLEAR_HISTORY | CLEAR_SESSION_HISTORY | CLEAR_ALL_CACHES | + CLEAR_COOKIES | CLEAR_EME | CLEAR_DOWNLOADS | + CLEAR_PERMISSIONS | CLEAR_DOM_STORAGES | CLEAR_CONTENT_PREFERENCES | + CLEAR_PREDICTOR_NETWORK_DATA | CLEAR_DOM_PUSH_NOTIFICATIONS | + CLEAR_CLIENT_AUTH_REMEMBER_SERVICE | CLEAR_REPORTS | CLEAR_CERT_EXCEPTIONS | + CLEAR_CREDENTIAL_MANAGER_STATE; +}; + +/** + * This is a companion interface for + * nsIClearDataService::deleteDataFromPrincipal(). + */ +[function, scriptable, uuid(e225517b-24c5-498a-b9fb-9993e341a398)] +interface nsIClearDataCallback : nsISupports +{ + /** + * Called to indicate that the data cleaning is completed. + * @param aFailedFlags this value contains the flags that failed during the + * cleanup. If nothing failed, aFailedFlags will be 0. + */ + void onDataDeleted(in uint32_t aFailedFlags); +}; diff --git a/toolkit/components/cleardata/tests/browser/browser.ini b/toolkit/components/cleardata/tests/browser/browser.ini new file mode 100644 index 0000000000..7b1f709e81 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser.ini @@ -0,0 +1,20 @@ +[browser_auth_tokens.js] +[browser_css_cache.js] +https_first_disabled = true +support-files = + file_css_cache.css + file_css_cache.html +[browser_image_cache.js] +https_first_disabled = true +support-files = + file_image_cache.html + file_image_cache.jpg +[browser_preflight_cache.js] +https_first_disabled = true +support-files = + file_cors_preflight.sjs +[browser_serviceworkers.js] +[browser_sessionStorage.js] +https_first_disabled = true +[browser_quota.js] +support-files = worker.js diff --git a/toolkit/components/cleardata/tests/browser/browser_auth_tokens.js b/toolkit/components/cleardata/tests/browser/browser_auth_tokens.js new file mode 100644 index 0000000000..e62c8b4ac4 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_auth_tokens.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for AuthTokensCleaner. + */ + +const TEST_SECRET = "secret"; +const TEST_PRINCIPAL = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://example.com" + ); +const TEST_CLEAR_DATA_FLAGS = Services.clearData.CLEAR_AUTH_TOKENS; + +const pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService( + Ci.nsIPK11TokenDB +); + +const { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); + +function testLoggedIn(isLoggedIn) { + Assert.equal( + pk11db.getInternalKeyToken().isLoggedIn(), + isLoggedIn, + `Should ${isLoggedIn ? "" : "not "}be logged in` + ); + pk11db.getInternalKeyToken().isLoggedIn(); +} + +function clearData({ deleteBy = "all", hasUserInput = false } = {}) { + return new Promise(resolve => { + if (deleteBy == "principal") { + Services.clearData.deleteDataFromPrincipal( + TEST_PRINCIPAL, + hasUserInput, + TEST_CLEAR_DATA_FLAGS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + } else if (deleteBy == "baseDomain") { + Services.clearData.deleteDataFromBaseDomain( + TEST_PRINCIPAL.baseDomain, + hasUserInput, + TEST_CLEAR_DATA_FLAGS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + } else { + Services.clearData.deleteData(TEST_CLEAR_DATA_FLAGS, value => { + Assert.equal(value, 0); + resolve(); + }); + } + }); +} + +function runTest({ deleteBy, hasUserInput }) { + testLoggedIn(false); + + info("Setup primary password and login"); + LoginTestUtils.primaryPassword.enable(true); + testLoggedIn(true); + + info( + `Clear AuthTokensCleaner data for ${deleteBy}, hasUserInput: ${hasUserInput}` + ); + clearData({ deleteBy, hasUserInput }); + + // The auth tokens cleaner cannot delete by principal or baseDomain + // (yet). If this method is called, it will check whether the used requested + // the clearing. If the user requested clearing, it will over-clear (clear + // all data). If the request didn't come from the user, for example from the + // PurgeTrackerService, it will not clear anything to avoid clearing storage + // unrelated to the baseDomain or principal. + let isCleared = deleteBy == "all" || hasUserInput; + testLoggedIn(!isCleared); + + // Cleanup + let sdr = Cc["@mozilla.org/security/sdr;1"].getService( + Ci.nsISecretDecoderRing + ); + sdr.logoutAndTeardown(); + LoginTestUtils.primaryPassword.disable(); +} + +add_task(async function test_deleteAll() { + runTest({ deleteBy: "all" }); +}); + +add_task(async function test_deleteByPrincipal() { + for (let hasUserInput of [false, true]) { + runTest({ deleteBy: "principal", hasUserInput }); + } +}); + +add_task(async function test_deleteByBaseDomain() { + for (let hasUserInput of [false, true]) { + runTest({ deleteBy: "baseDomain", hasUserInput }); + } +}); diff --git a/toolkit/components/cleardata/tests/browser/browser_css_cache.js b/toolkit/components/cleardata/tests/browser/browser_css_cache.js new file mode 100644 index 0000000000..47088e5011 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_css_cache.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE_DOMAIN_A = "example.com"; +const ORIGIN_A = `https://${BASE_DOMAIN_A}`; +const ORIGIN_A_HTTP = `http://${BASE_DOMAIN_A}`; +const ORIGIN_A_SUB = `https://test1.${BASE_DOMAIN_A}`; + +const BASE_DOMAIN_B = "example.org"; +const ORIGIN_B = `https://${BASE_DOMAIN_B}`; +const ORIGIN_B_HTTP = `http://${BASE_DOMAIN_B}`; +const ORIGIN_B_SUB = `https://test1.${BASE_DOMAIN_B}`; + +const TEST_ROOT_DIR = getRootDirectory(gTestPath); + +// Stylesheets are cached per process, so we need to keep tabs open for the +// duration of a test. +let tabs = {}; + +function getTestURLForOrigin(origin) { + return ( + TEST_ROOT_DIR.replace("chrome://mochitests/content", origin) + + "file_css_cache.html" + ); +} + +async function testCached(origin, isCached) { + let url = getTestURLForOrigin(origin); + + let numParsed; + + let tab = tabs[origin]; + let loadedPromise; + if (!tab) { + info("Creating new tab for " + url); + tab = BrowserTestUtils.addTab(gBrowser, url); + loadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tabs[origin] = tab; + } else { + loadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + tab.linkedBrowser.reload(); + } + await loadedPromise; + + numParsed = await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + return SpecialPowers.getDOMWindowUtils(content).parsedStyleSheets; + }); + + // Stylesheets is cached if numParsed is 0. + is(!numParsed, isCached, `${origin} is${isCached ? " " : " not "}cached`); +} + +async function addTestTabs() { + await testCached(ORIGIN_A, false); + await testCached(ORIGIN_A_SUB, false); + await testCached(ORIGIN_A_HTTP, false); + await testCached(ORIGIN_B, false); + await testCached(ORIGIN_B_SUB, false); + await testCached(ORIGIN_B_HTTP, false); + // Test that the cache has been populated. + await testCached(ORIGIN_A, true); + await testCached(ORIGIN_A_SUB, true); + await testCached(ORIGIN_A_HTTP, true); + await testCached(ORIGIN_B, true); + await testCached(ORIGIN_B_SUB, true); + await testCached(ORIGIN_B_HTTP, true); +} + +async function cleanupTestTabs() { + Object.values(tabs).forEach(BrowserTestUtils.removeTab); + tabs = {}; +} + +add_task(async function test_deleteByPrincipal() { + await addTestTabs(); + + // Clear data for content principal of A + info("Clearing cache for principal " + ORIGIN_A); + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN_A), + false, + Ci.nsIClearDataService.CLEAR_CSS_CACHE, + resolve + ); + }); + + // Only the cache entry for ORIGIN_A should have been cleared. + await testCached(ORIGIN_A, false); + await testCached(ORIGIN_A_SUB, true); + await testCached(ORIGIN_A_HTTP, true); + await testCached(ORIGIN_B, true); + await testCached(ORIGIN_B_SUB, true); + await testCached(ORIGIN_B_HTTP, true); + + // Cleanup + cleanupTestTabs(); + ChromeUtils.clearStyleSheetCache(); +}); + +add_task(async function test_deleteByBaseDomain() { + await addTestTabs(); + + // Clear data for base domain of A. + info("Clearing cache for base domain " + BASE_DOMAIN_A); + await new Promise(resolve => { + Services.clearData.deleteDataFromBaseDomain( + BASE_DOMAIN_A, + false, + Ci.nsIClearDataService.CLEAR_CSS_CACHE, + resolve + ); + }); + + // All entries for A should have been cleared. + await testCached(ORIGIN_A, false); + await testCached(ORIGIN_A_SUB, false); + await testCached(ORIGIN_A_HTTP, false); + // Entries for B should still exist. + await testCached(ORIGIN_B, true); + await testCached(ORIGIN_B_SUB, true); + await testCached(ORIGIN_B_HTTP, true); + + // Cleanup + cleanupTestTabs(); + ChromeUtils.clearStyleSheetCache(); +}); diff --git a/toolkit/components/cleardata/tests/browser/browser_image_cache.js b/toolkit/components/cleardata/tests/browser/browser_image_cache.js new file mode 100644 index 0000000000..d7116d2502 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_image_cache.js @@ -0,0 +1,219 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE_DOMAIN_A = "example.com"; +const ORIGIN_A = `https://${BASE_DOMAIN_A}`; +const ORIGIN_A_HTTP = `http://${BASE_DOMAIN_A}`; +const ORIGIN_A_SUB = `https://test1.${BASE_DOMAIN_A}`; + +const BASE_DOMAIN_B = "example.org"; +const ORIGIN_B = `https://${BASE_DOMAIN_B}`; +const ORIGIN_B_HTTP = `http://${BASE_DOMAIN_B}`; +const ORIGIN_B_SUB = `https://test1.${BASE_DOMAIN_B}`; + +const TEST_ROOT_DIR = getRootDirectory(gTestPath); + +// Images are cached per process, so we need to keep tabs open for the +// duration of a test. +let originToTabs = {}; + +function getTestURLForOrigin(origin) { + return TEST_ROOT_DIR.replace("chrome://mochitests/content", origin); +} + +async function testCached(origin, isCached, testPartitioned = false) { + let tabs = originToTabs[origin]; + + for (let tab of tabs) { + // For the partition test we inspect the cache state of the iframe. + let browsingContext = testPartitioned + ? tab.linkedBrowser.browsingContext.children[0] + : tab.linkedBrowser.browsingContext; + let actualCached = await SpecialPowers.spawn(browsingContext, [], () => { + let imgUrl = content.document.querySelector("img").src; + let imageCache = SpecialPowers.Cc[ + "@mozilla.org/image/tools;1" + ].getService(Ci.imgITools); + let uri = SpecialPowers.Services.io.newURI(imgUrl); + let properties = imageCache + .getImgCacheForDocument(content.document) + .findEntryProperties(uri, content.document); + return !!properties; + }); + + let msg = `${origin}${isCached ? " " : " not "}cached`; + if (testPartitioned) { + msg = "Partitioned under " + msg; + } + + is(actualCached, isCached, msg); + } +} + +/** + * Creates tabs and loads images in first party and third party context. + * @returns {Promise} - Promise which resolves once all tabs are initialized, + * {@link originToTabs} is populated and (sub-)resources have loaded. + */ +function addTestTabs() { + // Adding two tabs for ORIGIN_A so we can test clearing for a principal + // cross-process (non-fission). + let promises = [ + [ORIGIN_A, ORIGIN_B], + [ORIGIN_A, ORIGIN_B], + [ORIGIN_A_SUB, ORIGIN_B_SUB], + [ORIGIN_A_HTTP, ORIGIN_B_HTTP], + [ORIGIN_B, ORIGIN_A], + [ORIGIN_B_SUB, ORIGIN_A_SUB], + [ORIGIN_B_HTTP, ORIGIN_A_HTTP], + ].map(async ([firstParty, thirdParty]) => { + let urlFirstParty = + getTestURLForOrigin(firstParty) + "file_image_cache.html"; + let urlThirdParty = + getTestURLForOrigin(thirdParty) + "file_image_cache.html"; + + info("Creating new tab for " + urlFirstParty); + let tab = BrowserTestUtils.addTab(gBrowser, urlFirstParty); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("Creating iframe for " + urlThirdParty); + await SpecialPowers.spawn(tab.linkedBrowser, [urlThirdParty], async url => { + let iframe = content.document.createElement("iframe"); + iframe.src = url; + + let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load", false); + content.document.body.appendChild(iframe); + await loadPromise; + }); + + let tabs = originToTabs[firstParty]; + if (!tabs) { + tabs = []; + originToTabs[firstParty] = tabs; + } + tabs.push(tab); + }); + + return Promise.all(promises); +} + +function cleanup() { + Object.values(originToTabs).flat().forEach(BrowserTestUtils.removeTab); + originToTabs = {}; + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(null); + imageCache.clearCache(false); + imageCache.clearCache(true); +} + +add_setup(function () { + cleanup(); +}); + +add_task(async function test_deleteByPrincipal() { + await addTestTabs(); + + // Clear data for content principal of A + info("Clearing cache for principal " + ORIGIN_A); + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN_A), + false, + Ci.nsIClearDataService.CLEAR_IMAGE_CACHE, + resolve + ); + }); + + // Only the cache entry for ORIGIN_A should have been cleared. + await testCached(ORIGIN_A, false); + await testCached(ORIGIN_A_SUB, true); + await testCached(ORIGIN_A_HTTP, true); + await testCached(ORIGIN_B, true); + await testCached(ORIGIN_B_SUB, true); + await testCached(ORIGIN_B_HTTP, true); + + // No partitioned cache should have been cleared. + await testCached(ORIGIN_A, true, true); + await testCached(ORIGIN_A_SUB, true, true); + await testCached(ORIGIN_A_HTTP, true, true); + // TODO: ImageCacheCleaner deleteByPrincipal does not look at the cache key's + // isolationKey and thus over-clears. In this case it clears cache for A + // partitioned under B, even though the test principal does not set a + // partitionKey. + // See Bug 1713088. + await testCached(ORIGIN_B, false, true); + await testCached(ORIGIN_B_SUB, true, true); + await testCached(ORIGIN_B_HTTP, true, true); + + cleanup(); +}); + +add_task(async function test_deleteByBaseDomain() { + await addTestTabs(); + + // Clear data for base domain of A. + info("Clearing cache for base domain " + BASE_DOMAIN_A); + await new Promise(resolve => { + Services.clearData.deleteDataFromBaseDomain( + BASE_DOMAIN_A, + false, + Ci.nsIClearDataService.CLEAR_IMAGE_CACHE, + resolve + ); + }); + + // All entries for A should have been cleared. + await testCached(ORIGIN_A, false); + await testCached(ORIGIN_A_SUB, false); + await testCached(ORIGIN_A_HTTP, false); + // Entries for B should still exist. + await testCached(ORIGIN_B, true); + await testCached(ORIGIN_B_SUB, true); + await testCached(ORIGIN_B_HTTP, true); + + // All partitioned entries for B under A should have been cleared. + await testCached(ORIGIN_A, false, true); + await testCached(ORIGIN_A_SUB, false, true); + await testCached(ORIGIN_A_HTTP, false, true); + + // All partitioned entries of A under B should have been cleared. + await testCached(ORIGIN_B, false, true); + await testCached(ORIGIN_B_SUB, false, true); + await testCached(ORIGIN_B_HTTP, false, true); + + cleanup(); +}); + +add_task(async function test_deleteAll() { + await addTestTabs(); + + // Clear the whole image cache across processes. + info("Clearing cache for base domain " + BASE_DOMAIN_A); + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_IMAGE_CACHE, + resolve + ); + }); + + // All entries should have been cleared. + await testCached(ORIGIN_A, false); + await testCached(ORIGIN_A_SUB, false); + await testCached(ORIGIN_A_HTTP, false); + await testCached(ORIGIN_B, false); + await testCached(ORIGIN_B_SUB, false); + await testCached(ORIGIN_B_HTTP, false); + + // All partitioned entries should have been cleared. + await testCached(ORIGIN_A, false, true); + await testCached(ORIGIN_A_SUB, false, true); + await testCached(ORIGIN_A_HTTP, false, true); + await testCached(ORIGIN_B, false, true); + await testCached(ORIGIN_B_SUB, false, true); + await testCached(ORIGIN_B_HTTP, false, true); + + cleanup(); +}); diff --git a/toolkit/components/cleardata/tests/browser/browser_preflight_cache.js b/toolkit/components/cleardata/tests/browser/browser_preflight_cache.js new file mode 100644 index 0000000000..d3eabb9e38 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_preflight_cache.js @@ -0,0 +1,166 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +const uuidGenerator = Services.uuid; + +const ORIGIN_A = "http://example.net"; +const ORIGIN_B = "http://example.org"; + +const PREFLIGHT_URL_PATH = + "/browser/toolkit/components/cleardata/tests/browser/file_cors_preflight.sjs"; + +const PREFLIGHT_URL_A = ORIGIN_A + PREFLIGHT_URL_PATH; +const PREFLIGHT_URL_B = ORIGIN_B + PREFLIGHT_URL_PATH; + +function testPreflightCached(browser, url, token, isCached) { + return SpecialPowers.spawn( + browser, + [url, token, isCached], + async (url, token, isCached) => { + let response = await content.fetch( + new content.Request(`${url}?token=${token}`, { + mode: "cors", + method: "GET", + headers: [["x-test-header", "check"]], + }) + ); + + let expected = isCached ? "0" : "1"; + is( + await response.text(), + expected, + `Preflight cache for ${url} ${isCached ? "HIT" : "MISS"}.` + ); + } + ); +} + +async function testDeleteAll( + clearDataFlag, + { deleteBy = "all", hasUserInput = false } = {} +) { + await BrowserTestUtils.withNewTab("http://example.com", async browser => { + let token = uuidGenerator.generateUUID().toString(); + + // Populate the preflight cache. + await testPreflightCached(browser, PREFLIGHT_URL_A, token, false); + await testPreflightCached(browser, PREFLIGHT_URL_B, token, false); + // Cache should be populated. + await testPreflightCached(browser, PREFLIGHT_URL_A, token, true); + await testPreflightCached(browser, PREFLIGHT_URL_B, token, true); + + await new Promise(resolve => { + if (deleteBy == "principal") { + Services.clearData.deleteDataFromPrincipal( + browser.contentPrincipal, + hasUserInput, + clearDataFlag, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + } else if (deleteBy == "baseDomain") { + Services.clearData.deleteDataFromBaseDomain( + browser.contentPrincipal.baseDomain, + hasUserInput, + clearDataFlag, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + } else { + Services.clearData.deleteData(clearDataFlag, value => { + Assert.equal(value, 0); + resolve(); + }); + } + }); + + // The preflight cache cleaner cannot delete by principal or baseDomain + // (Bug 1727141). If this method is called, it will check whether the used + // requested the clearing. If the user requested clearing, it will + // over-clear (clear all data). If the request didn't come from the user, + // for example from the PurgeTrackerService, it will not clear anything to + // avoid clearing storage unrelated to the baseDomain or principal. + let clearedAll = deleteBy == "all" || hasUserInput; + + // Cache should be cleared. + await testPreflightCached(browser, PREFLIGHT_URL_A, token, !clearedAll); + await testPreflightCached(browser, PREFLIGHT_URL_B, token, !clearedAll); + }); + + SiteDataTestUtils.clear(); +} + +add_task(async function test_deleteAll() { + // The cleaner should be called when we target all cleaners, all cache + // cleaners, or just the preflight cache. + let { CLEAR_ALL, CLEAR_ALL_CACHES, CLEAR_PREFLIGHT_CACHE } = + Ci.nsIClearDataService; + + for (let flag of [CLEAR_ALL, CLEAR_ALL_CACHES, CLEAR_PREFLIGHT_CACHE]) { + await testDeleteAll(flag); + } +}); + +add_task(async function test_deleteByPrincipal() { + // The cleaner should be called when we target all cleaners, all cache + // cleaners, or just the preflight cache. + let { CLEAR_ALL, CLEAR_ALL_CACHES, CLEAR_PREFLIGHT_CACHE } = + Ci.nsIClearDataService; + + for (let flag of [CLEAR_ALL, CLEAR_ALL_CACHES, CLEAR_PREFLIGHT_CACHE]) { + for (let hasUserInput of [true, false]) { + await testDeleteAll(flag, { deleteBy: "principal", hasUserInput }); + } + } +}); + +add_task(async function test_deletePrivateBrowsingCache() { + async function deletePrivateBrowsingCache(token) { + const browser = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + const tab = (browser.gBrowser.selectedTab = BrowserTestUtils.addTab( + browser.gBrowser, + "http://example.com" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + // Populate the preflight cache and make sure it isn't populated right now + await testPreflightCached(tab.linkedBrowser, PREFLIGHT_URL_A, token, false); + await testPreflightCached(tab.linkedBrowser, PREFLIGHT_URL_B, token, false); + // Cache should be populated. + await testPreflightCached(tab.linkedBrowser, PREFLIGHT_URL_A, token, true); + await testPreflightCached(tab.linkedBrowser, PREFLIGHT_URL_B, token, true); + + await browser.close(); + } + + // Disable https_first mode to not upgrade the connection of the main page + // and get "Blocked loading mixed active content" for the CORS request + // making this test case fail. Another solution would be to change all URLs + // to https. + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first_pbm", false]], + }); + + let token = uuidGenerator.generateUUID().toString(); + + // Make sure the CORS preflight cache is cleared between two private + // browsing sessions. Calling this function twice to see if the cache isn't + // populated anymore after the first call. + await deletePrivateBrowsingCache(token); + await deletePrivateBrowsingCache(token); + + await SpecialPowers.clearUserPref("dom.security.https_first_pbm"); +}); diff --git a/toolkit/components/cleardata/tests/browser/browser_quota.js b/toolkit/components/cleardata/tests/browser/browser_quota.js new file mode 100644 index 0000000000..94ff223773 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_quota.js @@ -0,0 +1,318 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This function adds the quota storage by simpleDB (one of quota clients +// managed by the QuotaManager). In this function, a directory +// ${profile}/storage/default/${origin}/sdb/ and a file inside are expected to +// be added. +async function addQuotaStorage(principal) { + let connection = Cc["@mozilla.org/dom/sdb-connection;1"].createInstance( + Ci.nsISDBConnection + ); + + connection.init(principal); + + await new Promise((aResolve, aReject) => { + let request = connection.open("db"); + request.callback = request => { + if (request.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject(request.resultCode); + } + }; + }); + + await new Promise((aResolve, aReject) => { + let request = connection.write(new ArrayBuffer(1)); + request.callback = request => { + if (request.resultCode == Cr.NS_OK) { + aResolve(); + } else { + aReject(request.resultCode); + } + }; + }); +} + +function getPrincipal(url, attr = {}) { + let uri = Services.io.newURI(url); + let ssm = Services.scriptSecurityManager; + + return ssm.createContentPrincipal(uri, attr); +} + +function getProfileDir() { + let directoryService = Services.dirsvc; + + return directoryService.get("ProfD", Ci.nsIFile); +} + +function getRelativeFile(relativePath) { + let profileDir = getProfileDir(); + + let file = profileDir.clone(); + relativePath.split("/").forEach(function (component) { + file.append(component); + }); + + return file; +} + +function getPath(origin) { + // Sanitizing + let regex = /[:\/]/g; + return "storage/default/" + origin.replace(regex, "+"); +} + +// This function checks if the origin has the quota storage by checking whether +// the origin directory of that origin exists or not. +function hasQuotaStorage(origin, attr) { + let path = getPath(origin); + if (attr) { + let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(origin), + attr + ); + path += principal.originSuffix; + } + + let file = getRelativeFile(path); + return file.exists(); +} + +async function runTest(sites, deleteDataFunc) { + info(`Adding quota storage`); + for (let site of sites) { + const principal = getPrincipal(site.origin, site.originAttributes); + await addQuotaStorage(principal); + } + + info(`Verifying ${deleteDataFunc.name}`); + let site; + while ((site = sites.shift())) { + await new Promise(aResolve => { + deleteDataFunc(...site.args, value => { + Assert.equal(value, 0); + aResolve(); + }); + }); + + ok( + !hasQuotaStorage(site.origin, site.originAttributes), + `${site.origin} has no quota storage` + ); + sites.forEach(remainSite => + ok( + hasQuotaStorage(remainSite.origin, remainSite.originAttributes), + `${remainSite.origin} has quota storage` + ) + ); + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.quotaManager.testing", true], + ["dom.simpleDB.enabled", true], + ], + }); +}); + +const ORG_DOMAIN = "example.org"; +const ORG_DOMAIN_SUB = `test.${ORG_DOMAIN}`; +const ORG_ORIGIN = `https://${ORG_DOMAIN}`; +const ORG_ORIGIN_SUB = `https://${ORG_DOMAIN_SUB}`; +const COM_DOMAIN = "example.com"; +const COM_ORIGIN = `https://${COM_DOMAIN}`; +const LH_DOMAIN = "localhost"; +const FOO_DOMAIN = "foo.com"; + +add_task(async function test_deleteFromHost() { + const sites = [ + { + args: [ORG_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: ORG_ORIGIN, + }, + { + args: [COM_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: COM_ORIGIN, + }, + { + args: [LH_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: `http://${LH_DOMAIN}:8000`, + }, + { + args: [FOO_DOMAIN, true, Ci.nsIClearDataService.CLEAR_DOM_QUOTA], + origin: `http://${FOO_DOMAIN}`, + originAttributes: { userContextId: 1 }, + }, + ]; + + await runTest(sites, Services.clearData.deleteDataFromHost); +}); + +add_task(async function test_deleteFromPrincipal() { + const sites = [ + { + args: [ + getPrincipal(ORG_ORIGIN), + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + ], + origin: ORG_ORIGIN, + }, + { + args: [ + getPrincipal(COM_ORIGIN), + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + ], + origin: COM_ORIGIN, + }, + ]; + + await runTest(sites, Services.clearData.deleteDataFromPrincipal); +}); + +add_task(async function test_deleteFromOriginAttributes() { + const ORG_OA = { userContextId: 1 }; + const COM_OA = { userContextId: 2 }; + const sites = [ + { + args: [ORG_OA], + origin: ORG_ORIGIN, + originAttributes: ORG_OA, + }, + { + args: [COM_OA], + origin: COM_ORIGIN, + originAttributes: COM_OA, + }, + ]; + + await runTest( + sites, + Services.clearData.deleteDataFromOriginAttributesPattern + ); +}); + +add_task(async function test_deleteAll() { + info(`Adding quota storage`); + await addQuotaStorage(getPrincipal(ORG_ORIGIN)); + await addQuotaStorage(getPrincipal(COM_ORIGIN)); + + info(`Verifying deleteData`); + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + ok(!hasQuotaStorage(ORG_ORIGIN), `${ORG_ORIGIN} has no quota storage`); + ok(!hasQuotaStorage(COM_ORIGIN), `${COM_ORIGIN} has no quota storage`); +}); + +add_task(async function test_deleteSubdomain() { + const ANOTHER_ORIGIN = `https://wwww.${ORG_DOMAIN}`; + info(`Adding quota storage`); + await addQuotaStorage(getPrincipal(ORG_ORIGIN)); + await addQuotaStorage(getPrincipal(ANOTHER_ORIGIN)); + + info(`Verifying deleteDataFromHost for subdomain`); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + ORG_DOMAIN, + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + ok(!hasQuotaStorage(ORG_ORIGIN), `${ORG_ORIGIN} has no quota storage`); + ok(!hasQuotaStorage(COM_ORIGIN), `${ANOTHER_ORIGIN} has no quota storage`); +}); + +function getOAWithPartitionKey(topLevelBaseDomain, originAttributes = {}) { + if (!topLevelBaseDomain) { + return originAttributes; + } + return { + ...originAttributes, + partitionKey: `(https,${topLevelBaseDomain})`, + }; +} + +add_task(async function test_deleteBaseDomain() { + info("Adding quota storage"); + await addQuotaStorage(getPrincipal(ORG_ORIGIN)); + await addQuotaStorage(getPrincipal(ORG_ORIGIN_SUB)); + await addQuotaStorage(getPrincipal(COM_ORIGIN)); + + info("Adding partitioned quota storage"); + // Partitioned + await addQuotaStorage( + getPrincipal(COM_ORIGIN, getOAWithPartitionKey(ORG_DOMAIN)) + ); + await addQuotaStorage( + getPrincipal(COM_ORIGIN, getOAWithPartitionKey(FOO_DOMAIN)) + ); + await addQuotaStorage( + getPrincipal(ORG_ORIGIN, getOAWithPartitionKey(COM_DOMAIN)) + ); + + info(`Verifying deleteDataFromBaseDomain`); + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + ORG_DOMAIN, + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + ok(!hasQuotaStorage(ORG_ORIGIN), `${ORG_ORIGIN} has no quota storage`); + ok( + !hasQuotaStorage(ORG_ORIGIN_SUB), + `${ORG_ORIGIN_SUB} has no quota storage` + ); + ok(hasQuotaStorage(COM_ORIGIN), `${COM_ORIGIN} has quota storage`); + + // Partitioned + ok( + !hasQuotaStorage(COM_ORIGIN, getOAWithPartitionKey(ORG_DOMAIN)), + `${COM_ORIGIN} under ${ORG_DOMAIN} has no quota storage` + ); + ok( + hasQuotaStorage(COM_ORIGIN, getOAWithPartitionKey(FOO_DOMAIN)), + `${COM_ORIGIN} under ${FOO_DOMAIN} has quota storage` + ); + ok( + !hasQuotaStorage(ORG_ORIGIN, getOAWithPartitionKey(COM_DOMAIN)), + `${ORG_ORIGIN} under ${COM_DOMAIN} has no quota storage` + ); + + // Cleanup + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); +}); diff --git a/toolkit/components/cleardata/tests/browser/browser_serviceworkers.js b/toolkit/components/cleardata/tests/browser/browser_serviceworkers.js new file mode 100644 index 0000000000..ad501aed70 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_serviceworkers.js @@ -0,0 +1,287 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +async function addServiceWorker(origin) { + let swURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "worker.js"; + + let registered = SiteDataTestUtils.promiseServiceWorkerRegistered(swURL); + await SiteDataTestUtils.addServiceWorker(swURL); + await registered; + + ok(true, `${origin} has a service worker`); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +add_task(async function test_deleteFromHost() { + await addServiceWorker("https://example.com"); + await addServiceWorker("https://example.org"); + await addServiceWorker("https://test1.example.org"); + + let unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://example.com" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.com", + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.com"), + "example.com has no service worker" + ); + ok( + SiteDataTestUtils.hasServiceWorkers("https://example.org"), + "example.org has a service worker" + ); + ok( + SiteDataTestUtils.hasServiceWorkers("https://test1.example.org"), + "test1.example.org has a service worker" + ); + + // Clearing the subdomain should not clear the base domain. + unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test1.example.org" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "test1.example.org", + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.org"), + "test1.example.org has no service worker" + ); + ok( + SiteDataTestUtils.hasServiceWorkers("https://example.org"), + "example.org has a service worker" + ); + + unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://example.org" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.org", + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.org"), + "example.org has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.com"), + "example.com has no service worker" + ); +}); + +add_task(async function test_deleteFromBaseDomain() { + await addServiceWorker("https://example.com"); + await addServiceWorker("https://test1.example.com"); + await addServiceWorker("https://test2.example.com"); + await addServiceWorker("https://example.org"); + + let unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://example.com" + ); + let unregisteredSub1 = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test1.example.com" + ); + let unregisteredSub2 = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test1.example.com" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.com", + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await Promise.all([unregistered, unregisteredSub1, unregisteredSub2]); + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.com"), + "example.com has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.com"), + "test1.example.com has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test2.example.com"), + "test2.example.com has no service worker" + ); + ok( + SiteDataTestUtils.hasServiceWorkers("https://example.org"), + "example.org has a service worker" + ); + + unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://example.org" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.org", + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.org"), + "example.org has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://example.com"), + "example.com has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.com"), + "test1.example.com has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test2.example.com"), + "test2.example.com has no service worker" + ); +}); + +add_task(async function test_deleteFromPrincipal() { + await addServiceWorker("https://test1.example.com"); + await addServiceWorker("https://test1.example.org"); + + let unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test1.example.com" + ); + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://test1.example.com/" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.com"), + "test1.example.com has no service worker" + ); + ok( + SiteDataTestUtils.hasServiceWorkers("https://test1.example.org"), + "test1.example.org has a service worker" + ); + + unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test1.example.org" + ); + principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://test1.example.org/" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.org"), + "test1.example.org has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test1.example.com"), + "test1.example.com has no service worker" + ); +}); + +add_task(async function test_deleteAll() { + await addServiceWorker("https://test2.example.com"); + await addServiceWorker("https://test2.example.org"); + + let unregistered = SiteDataTestUtils.promiseServiceWorkerUnregistered( + "https://test2.example.com" + ); + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + await unregistered; + + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test2.example.com"), + "test2.example.com has no service worker" + ); + ok( + !SiteDataTestUtils.hasServiceWorkers("https://test2.example.org"), + "test2.example.org has no service worker" + ); + + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/cleardata/tests/browser/browser_sessionStorage.js b/toolkit/components/cleardata/tests/browser/browser_sessionStorage.js new file mode 100644 index 0000000000..013ae0fa92 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/browser_sessionStorage.js @@ -0,0 +1,235 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BASE_DOMAIN_A = "example.com"; +const ORIGIN_A = `https://${BASE_DOMAIN_A}`; +const ORIGIN_A_HTTP = `http://${BASE_DOMAIN_A}`; +const ORIGIN_A_SUB = `https://test1.${BASE_DOMAIN_A}`; + +const BASE_DOMAIN_B = "example.org"; +const ORIGIN_B = `https://${BASE_DOMAIN_B}`; +const ORIGIN_B_HTTP = `http://${BASE_DOMAIN_B}`; +const ORIGIN_B_SUB = `https://test1.${BASE_DOMAIN_B}`; + +const TEST_ROOT_DIR = getRootDirectory(gTestPath); + +// Session storage is only valid for the lifetime of a tab, so we need to keep +// tabs open for the duration of a test. +let originToTabs = {}; + +function getTestURLForOrigin(origin) { + return TEST_ROOT_DIR.replace("chrome://mochitests/content", origin); +} + +function getTestEntryName(origin, partitioned) { + return `${origin}${partitioned ? "_partitioned" : ""}`; +} + +async function testHasEntry(originFirstParty, isSet, originThirdParty) { + let tabs = originToTabs[originFirstParty]; + + for (let tab of tabs) { + // For the partition test we inspect the sessionStorage of the iframe. + let browsingContext = originThirdParty + ? tab.linkedBrowser.browsingContext.children[0] + : tab.linkedBrowser.browsingContext; + let actualSet = await SpecialPowers.spawn( + browsingContext, + [ + getTestEntryName( + originThirdParty || originFirstParty, + !!originThirdParty + ), + ], + key => { + return !!content.sessionStorage.getItem(key); + } + ); + + let msg = `${originFirstParty}${isSet ? " " : " not "}set`; + if (originThirdParty) { + msg = "Partitioned under " + msg; + } + + is(actualSet, isSet, msg); + } +} + +/** + * Creates tabs and sets sessionStorage entries in first party and third party + * context. + * @returns {Promise} - Promise which resolves once all tabs are initialized, + * {@link originToTabs} is populated and (sub-)resources have loaded. + */ +function addTestTabs() { + let promises = [ + [ORIGIN_A, ORIGIN_B], + [ORIGIN_A_SUB, ORIGIN_B_SUB], + [ORIGIN_A_HTTP, ORIGIN_B_HTTP], + [ORIGIN_B, ORIGIN_A], + [ORIGIN_B_SUB, ORIGIN_A_SUB], + [ORIGIN_B_HTTP, ORIGIN_A_HTTP], + ].map(async ([firstParty, thirdParty]) => { + info("Creating new tab for " + firstParty); + let tab = BrowserTestUtils.addTab(gBrowser, firstParty); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info("Creating iframe for " + thirdParty); + await SpecialPowers.spawn( + tab.linkedBrowser, + [getTestEntryName(firstParty, false), thirdParty], + async (storageKey, url) => { + // Set unpartitioned sessionStorage for firstParty. + content.sessionStorage.setItem(storageKey, "foo"); + + let iframe = content.document.createElement("iframe"); + iframe.src = url; + + let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load", false); + content.document.body.appendChild(iframe); + await loadPromise; + } + ); + + await SpecialPowers.spawn( + tab.linkedBrowser.browsingContext.children[0], + [getTestEntryName(thirdParty, true)], + async storageKey => { + // Set sessionStorage in partitioned third-party iframe. + content.sessionStorage.setItem(storageKey, "foo"); + } + ); + + let tabs = originToTabs[firstParty]; + if (!tabs) { + tabs = []; + originToTabs[firstParty] = tabs; + } + tabs.push(tab); + }); + + return Promise.all(promises); +} + +function cleanup() { + Object.values(originToTabs).flat().forEach(BrowserTestUtils.removeTab); + originToTabs = {}; + Services.obs.notifyObservers(null, "browser:purge-sessionStorage"); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior", 5]], + }); + cleanup(); +}); + +add_task(async function test_deleteByBaseDomain() { + await addTestTabs(); + + info("Clearing sessionStorage for base domain A " + BASE_DOMAIN_A); + await new Promise(resolve => { + Services.clearData.deleteDataFromBaseDomain( + BASE_DOMAIN_A, + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + resolve + ); + }); + + info("All entries for A should have been cleared."); + await testHasEntry(ORIGIN_A, false); + await testHasEntry(ORIGIN_A_SUB, false); + await testHasEntry(ORIGIN_A_HTTP, false); + + info("Entries for B should still exist."); + await testHasEntry(ORIGIN_B, true); + await testHasEntry(ORIGIN_B_SUB, true); + await testHasEntry(ORIGIN_B_HTTP, true); + + info("All partitioned entries for B under A should have been cleared."); + await testHasEntry(ORIGIN_A, false, ORIGIN_B); + await testHasEntry(ORIGIN_A_SUB, false, ORIGIN_B_SUB); + await testHasEntry(ORIGIN_A_HTTP, false, ORIGIN_B_HTTP); + + info("All partitioned entries of A under B should have been cleared."); + await testHasEntry(ORIGIN_B, false, ORIGIN_A); + await testHasEntry(ORIGIN_B_SUB, false, ORIGIN_A_SUB); + await testHasEntry(ORIGIN_B_HTTP, false, ORIGIN_A_HTTP); + + cleanup(); +}); + +add_task(async function test_deleteAll() { + await addTestTabs(); + + info("Clearing sessionStorage for base domain A " + BASE_DOMAIN_A); + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + resolve + ); + }); + + info("All entries should have been cleared."); + await testHasEntry(ORIGIN_A, false); + await testHasEntry(ORIGIN_A_SUB, false); + await testHasEntry(ORIGIN_A_HTTP, false); + await testHasEntry(ORIGIN_B, false); + await testHasEntry(ORIGIN_B_SUB, false); + await testHasEntry(ORIGIN_B_HTTP, false); + + info("All partitioned entries should have been cleared."); + await testHasEntry(ORIGIN_A, false, ORIGIN_B); + await testHasEntry(ORIGIN_A_SUB, false, ORIGIN_B_SUB); + await testHasEntry(ORIGIN_A_HTTP, false, ORIGIN_B_HTTP); + await testHasEntry(ORIGIN_B, false, ORIGIN_A); + await testHasEntry(ORIGIN_B_SUB, false, ORIGIN_A_SUB); + await testHasEntry(ORIGIN_B_HTTP, false, ORIGIN_A_HTTP); + + cleanup(); +}); + +add_task(async function test_deleteFromPrincipal() { + await addTestTabs(); + + info("Clearing sessionStorage for partitioned principal A " + BASE_DOMAIN_A); + + let principalA = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(ORIGIN_A), + { partitionKey: `(https,${BASE_DOMAIN_B})` } + ); + + info("principal: " + principalA.origin); + info("principal partitionKey " + principalA.originAttributes.partitionKey); + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + principalA, + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + resolve + ); + }); + + info("Unpartitioned entries should still exist."); + await testHasEntry(ORIGIN_A, true); + await testHasEntry(ORIGIN_A_SUB, true); + await testHasEntry(ORIGIN_A_HTTP, true); + await testHasEntry(ORIGIN_B, true); + await testHasEntry(ORIGIN_B_SUB, true); + await testHasEntry(ORIGIN_B_HTTP, true); + + info("Only entries of principal should have been cleared."); + await testHasEntry(ORIGIN_A, true, ORIGIN_B); + await testHasEntry(ORIGIN_A_SUB, true, ORIGIN_B_SUB); + await testHasEntry(ORIGIN_A_HTTP, true, ORIGIN_B_HTTP); + + await testHasEntry(ORIGIN_B, false, ORIGIN_A); + + await testHasEntry(ORIGIN_B_SUB, true, ORIGIN_A_SUB); + await testHasEntry(ORIGIN_B_HTTP, true, ORIGIN_A_HTTP); + + cleanup(); +}); diff --git a/toolkit/components/cleardata/tests/browser/file_cors_preflight.sjs b/toolkit/components/cleardata/tests/browser/file_cors_preflight.sjs new file mode 100644 index 0000000000..0f7041aaa7 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/file_cors_preflight.sjs @@ -0,0 +1,40 @@ +/* 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/. */ + +"use strict"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + let query = new URLSearchParams(request.queryString); + let token = query.get("token"); + + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Headers", "x-test-header", false); + + if (request.method == "OPTIONS") { + response.setHeader( + "Access-Control-Allow-Methods", + request.getHeader("Access-Control-Request-Method"), + false + ); + response.setHeader("Access-Control-Max-Age", "20", false); + + setState(token, token); + } else { + let test_op = request.getHeader("x-test-header"); + + if (test_op == "check") { + let value = getState(token); + + if (value) { + response.write("1"); + setState(token, ""); + } else { + response.write("0"); + } + } + } +} diff --git a/toolkit/components/cleardata/tests/browser/file_css_cache.css b/toolkit/components/cleardata/tests/browser/file_css_cache.css new file mode 100644 index 0000000000..2ceb1b7e0b --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/file_css_cache.css @@ -0,0 +1,3 @@ +:root { + background-color: lime; +} diff --git a/toolkit/components/cleardata/tests/browser/file_css_cache.html b/toolkit/components/cleardata/tests/browser/file_css_cache.html new file mode 100644 index 0000000000..b382bc1887 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/file_css_cache.html @@ -0,0 +1,6 @@ +<!doctype html> +<meta charset="utf-8"> +<head> + <link rel="stylesheet" href="file_css_cache.css"> +</head> +<body></body> diff --git a/toolkit/components/cleardata/tests/browser/file_image_cache.html b/toolkit/components/cleardata/tests/browser/file_image_cache.html new file mode 100644 index 0000000000..37439a8fa7 --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/file_image_cache.html @@ -0,0 +1,7 @@ +<!doctype html> +<meta charset="utf-8"> +<head> +</head> +<body> + <img src="file_image_cache.jpg"> +</body> diff --git a/toolkit/components/cleardata/tests/browser/file_image_cache.jpg b/toolkit/components/cleardata/tests/browser/file_image_cache.jpg Binary files differnew file mode 100644 index 0000000000..48c454d27c --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/file_image_cache.jpg diff --git a/toolkit/components/cleardata/tests/browser/worker.js b/toolkit/components/cleardata/tests/browser/worker.js new file mode 100644 index 0000000000..aa8a83a4ce --- /dev/null +++ b/toolkit/components/cleardata/tests/browser/worker.js @@ -0,0 +1 @@ +// Empty script for testing service workers diff --git a/toolkit/components/cleardata/tests/marionette/manifest.ini b/toolkit/components/cleardata/tests/marionette/manifest.ini new file mode 100644 index 0000000000..be03a9aff0 --- /dev/null +++ b/toolkit/components/cleardata/tests/marionette/manifest.ini @@ -0,0 +1,2 @@ +[test_moved_origin_directory_cleanup.py] +[test_service_worker_at_shutdown.py] diff --git a/toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py b/toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py new file mode 100644 index 0000000000..e0a066577e --- /dev/null +++ b/toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py @@ -0,0 +1,134 @@ +# 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/. + +from pathlib import Path + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + + +class MovedOriginDirectoryCleanupTestCase(MarionetteTestCase): + def setUp(self): + super().setUp() + self.marionette.enforce_gecko_prefs( + { + "privacy.sanitize.sanitizeOnShutdown": True, + "privacy.clearOnShutdown.offlineApps": True, + "dom.quotaManager.backgroundTask.enabled": False, + } + ) + self.moved_origin_directory = ( + Path(self.marionette.profile_path) / "storage" / "to-be-removed" / "foo" + ) + self.moved_origin_directory.mkdir(parents=True, exist_ok=True) + + # Add a cookie to get a principal to be cleaned up + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + Services.cookies.add( + "example.local", + "path", + "name", + "value", + false, + false, + false, + Math.floor(Date.now() / 1000) + 24 * 60 * 60, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_UNSET + ); + """ + ) + + def removeAllCookies(self): + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + Services.cookies.removeAll(); + """ + ) + + def test_ensure_cleanup_by_quit(self): + self.assertTrue( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must exist", + ) + + # Cleanup happens via Sanitizer.sanitizeOnShutdown + self.marionette.quit() + + self.assertFalse( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must disappear", + ) + + def test_ensure_cleanup_at_crashed_restart(self): + self.assertTrue( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must exist", + ) + + # Pending sanitization is added via Sanitizer.onStartup + # "offlineApps" corresponds to CLEAR_DOM_STORAGES + Wait(self.marionette).until( + lambda _: ( + "offlineApps" in self.marionette.get_pref("privacy.sanitize.pending"), + ), + message="privacy.sanitize.pending must include offlineApps", + ) + + # Cleanup happens via Sanitizer.onStartup after restart + self.marionette.restart(in_app=False) + + Wait(self.marionette).until( + lambda _: not self.moved_origin_directory.exists(), + message="to-be-removed subdirectory must disappear", + ) + + def test_ensure_cleanup_by_quit_with_background_task(self): + self.assertTrue( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must exist", + ) + + self.marionette.set_pref("dom.quotaManager.backgroundTask.enabled", True) + + # Cleanup happens via Sanitizer.sanitizeOnShutdown + self.marionette.quit() + + Wait(self.marionette).until( + lambda _: not self.moved_origin_directory.exists(), + message="to-be-removed subdirectory must disappear", + ) + + def test_ensure_no_cleanup_when_disabled(self): + self.assertTrue( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must exist", + ) + + self.marionette.set_pref("privacy.sanitize.sanitizeOnShutdown", False) + self.marionette.quit() + + self.assertTrue( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must not disappear", + ) + + def test_ensure_no_cleanup_when_no_cookie(self): + self.assertTrue( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must exist", + ) + + self.removeAllCookies() + + self.marionette.quit() + + self.assertTrue( + self.moved_origin_directory.exists(), + "to-be-removed subdirectory must not disappear", + ) diff --git a/toolkit/components/cleardata/tests/marionette/test_service_worker_at_shutdown.py b/toolkit/components/cleardata/tests/marionette/test_service_worker_at_shutdown.py new file mode 100644 index 0000000000..9738c0ea17 --- /dev/null +++ b/toolkit/components/cleardata/tests/marionette/test_service_worker_at_shutdown.py @@ -0,0 +1,62 @@ +# 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/. + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + + +class ServiceWorkerAtShutdownTestCase(MarionetteTestCase): + def setUp(self): + super(ServiceWorkerAtShutdownTestCase, self).setUp() + self.install_service_worker() + self.set_pref_to_delete_site_data_on_shutdown() + + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + super(ServiceWorkerAtShutdownTestCase, self).tearDown() + + def install_service_worker(self): + install_url = self.marionette.absolute_url( + "serviceworker/install_serviceworker.html" + ) + self.marionette.navigate(install_url) + # Make sure 'install_url' is not loaded on startup, it would reinstall the service worker + dummy_url = self.marionette.absolute_url("dummy.html") + self.marionette.navigate(dummy_url) + Wait(self.marionette).until(lambda _: self.is_service_worker_registered) + + def set_pref_to_delete_site_data_on_shutdown(self): + self.marionette.set_pref("privacy.sanitize.sanitizeOnShutdown", True) + self.marionette.set_pref("privacy.clearOnShutdown.offlineApps", True) + + def test_unregistering_service_worker_when_clearing_data(self): + self.marionette.restart(clean=False, in_app=True) + self.assertFalse(self.is_service_worker_registered) + + @property + def is_service_worker_registered(self): + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + """ + let serviceWorkerManager = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(arguments[0]); + + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.principal.origin == principal.origin) { + return true; + } + } + return false; + """, + script_args=(self.marionette.absolute_url(""),), + ) diff --git a/toolkit/components/cleardata/tests/unit/head.js b/toolkit/components/cleardata/tests/unit/head.js new file mode 100644 index 0000000000..5e73b8a789 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/head.js @@ -0,0 +1,27 @@ +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +function run_test() { + do_get_profile(); + run_next_test(); +} + +function getOAWithPartitionKey( + { scheme = "https", topLevelBaseDomain, port = null } = {}, + originAttributes = {} +) { + if (!topLevelBaseDomain || !scheme) { + return originAttributes; + } + + return { + ...originAttributes, + partitionKey: `(${scheme},${topLevelBaseDomain}${port ? `,${port}` : ""})`, + }; +} diff --git a/toolkit/components/cleardata/tests/unit/test_basic.js b/toolkit/components/cleardata/tests/unit/test_basic.js new file mode 100644 index 0000000000..3634483ee4 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_basic.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Basic test for nsIClearDataService module. + */ + +"use strict"; + +add_task(async function test_basic() { + Assert.ok(!!Services.clearData); + + await new Promise(aResolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => { + Assert.equal(value, 0); + aResolve(); + }); + }); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_certs.js b/toolkit/components/cleardata/tests/unit/test_certs.js new file mode 100644 index 0000000000..3ff538d5a8 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_certs.js @@ -0,0 +1,233 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService +); +const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +const CERT_TEST = + "MIHhMIGcAgEAMA0GCSqGSIb3DQEBBQUAMAwxCjAIBgNVBAMTAUEwHhcNMTEwMzIzMjMyNTE3WhcNMTEwNDIyMjMyNTE3WjAMMQowCAYDVQQDEwFBMEwwDQYJKoZIhvcNAQEBBQADOwAwOAIxANFm7ZCfYNJViaDWTFuMClX3+9u18VFGiyLfM6xJrxir4QVtQC7VUC/WUGoBUs9COQIDAQABMA0GCSqGSIb3DQEBBQUAAzEAx2+gIwmuYjJO5SyabqIm4lB1MandHH1HQc0y0tUFshBOMESTzQRPSVwPn77a6R9t"; + +add_task(async function () { + Assert.ok(Services.clearData); + + const TEST_URI = Services.io.newURI("http://test.com/"); + const ANOTHER_TEST_URI = Services.io.newURI("https://example.com/"); + const YET_ANOTHER_TEST_URI = Services.io.newURI("https://example.test/"); + let cert = certDB.constructX509FromBase64(CERT_TEST); + let flags = Ci.nsIClearDataService.CLEAR_CERT_EXCEPTIONS; + + ok(cert, "Cert was created"); + + Assert.ok( + !overrideService.hasMatchingOverride( + TEST_URI.asciiHost, + TEST_URI.port, + {}, + cert, + {} + ), + `Should not have override for ${TEST_URI.asciiHost}:${TEST_URI.port} yet` + ); + + overrideService.rememberValidityOverride( + TEST_URI.asciiHost, + TEST_URI.port, + {}, + cert, + flags, + false + ); + + Assert.ok( + overrideService.hasMatchingOverride( + TEST_URI.asciiHost, + TEST_URI.port, + {}, + cert, + {} + ), + `Should have override for ${TEST_URI.asciiHost}:${TEST_URI.port} now` + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + TEST_URI.asciiHostPort, + true /* user request */, + flags, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !overrideService.hasMatchingOverride( + TEST_URI.asciiHost, + TEST_URI.port, + {}, + cert, + {} + ), + `Should not have override for ${TEST_URI.asciiHost}:${TEST_URI.port} now` + ); + + for (let uri of [TEST_URI, ANOTHER_TEST_URI, YET_ANOTHER_TEST_URI]) { + overrideService.rememberValidityOverride( + uri.asciiHost, + uri.port, + { privateBrowsingId: 1 }, + cert, + flags, + false + ); + Assert.ok( + overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + { privateBrowsingId: 1 }, + cert, + {} + ), + `Should have added override for ${uri.asciiHost}:${uri.port} with private browsing ID` + ); + Assert.ok( + !overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + { privateBrowsingId: 2 }, + cert, + {} + ), + `Should not have added override for ${uri.asciiHost}:${uri.port} with private browsing ID 2` + ); + Assert.ok( + !overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + {}, + cert, + {} + ), + `Should not have added override for ${uri.asciiHost}:${uri.port}` + ); + overrideService.rememberValidityOverride( + uri.asciiHost, + uri.port, + {}, + cert, + flags, + false + ); + Assert.ok( + overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + {}, + cert, + {} + ), + `Should have added override for ${uri.asciiHost}:${uri.port}` + ); + } + + await new Promise(aResolve => { + Services.clearData.deleteData(flags, value => { + Assert.equal(value, 0); + aResolve(); + }); + }); + + for (let uri of [TEST_URI, ANOTHER_TEST_URI, YET_ANOTHER_TEST_URI]) { + Assert.ok( + !overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + {}, + cert, + {} + ), + `Should have removed override for ${uri.asciiHost}:${uri.port}` + ); + Assert.ok( + !overrideService.hasMatchingOverride( + uri.asciiHost, + uri.port, + { privateBrowsingId: 1 }, + cert, + {} + ), + `Should have removed override for ${uri.asciiHost}:${uri.port} with private browsing attribute` + ); + } +}); + +add_task(async function test_deleteByBaseDomain() { + let toClear = [ + Services.io.newURI("https://example.com"), + Services.io.newURI("http://example.com:8080"), + Services.io.newURI("http://test1.example.com"), + Services.io.newURI("http://foo.bar.example.com"), + ]; + + let toKeep = [ + Services.io.newURI("https://example.org"), + Services.io.newURI("http://test1.example.org"), + Services.io.newURI("http://foo.bar.example.org"), + Services.io.newURI("http://example.test"), + ]; + + let all = toClear.concat(toKeep); + + let cert = certDB.constructX509FromBase64(CERT_TEST); + ok(cert, "Cert was created"); + + all.forEach(({ asciiHost, port }) => { + Assert.ok( + !overrideService.hasMatchingOverride(asciiHost, port, {}, cert, {}), + `Should not have override for ${asciiHost}:${port} yet` + ); + + overrideService.rememberValidityOverride(asciiHost, port, {}, cert, false); + + Assert.ok( + overrideService.hasMatchingOverride(asciiHost, port, {}, cert, {}), + `Should have override for ${asciiHost}:${port} now` + ); + }); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.com", + true /* user request */, + Ci.nsIClearDataService.CLEAR_CERT_EXCEPTIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + toClear.forEach(({ asciiHost, port }) => + Assert.ok( + !overrideService.hasMatchingOverride(asciiHost, port, {}, cert, {}), + `Should have cleared override for ${asciiHost}:${port}` + ) + ); + + toKeep.forEach(({ asciiHost, port }) => + Assert.ok( + overrideService.hasMatchingOverride(asciiHost, port, {}, cert, {}), + `Should have kept override for ${asciiHost}:${port}` + ) + ); + + // Cleanup + overrideService.clearAllOverrides(); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_cookies.js b/toolkit/components/cleardata/tests/unit/test_cookies.js new file mode 100644 index 0000000000..4bcb6d725a --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_cookies.js @@ -0,0 +1,393 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for cookies. + */ + +"use strict"; + +add_task(async function test_all_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "example.net", + "path", + "name", + "value", + true /* secure */, + true /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0); +}); + +add_task(async function test_range_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "example.net", + "path", + "name", + "value", + true /* secure */, + true /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + // The cookie is out of time range here. + let from = Date.now() + 60 * 60; + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + from * 1000, + expiry * 2000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + // Now we delete all. + from = Date.now() - 60 * 60; + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + from * 1000, + expiry * 2000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0); +}); + +add_task(async function test_principal_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "example.net", + "path", + "name", + "value", + true /* secure */, + true /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + let uri = Services.io.newURI("http://example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 1); + + // Now we delete all. + uri = Services.io.newURI("http://example.net"); + principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_COOKIES, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal(Services.cookies.countCookiesFromHost("example.net"), 0); +}); + +add_task(async function test_localfile_cookies() { + const expiry = Date.now() + 24 * 60 * 60; + Services.cookies.add( + "", // local file + "path", + "name", + "value", + false /* secure */, + false /* http only */, + false /* session */, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + + Assert.notEqual(Services.cookies.countCookiesFromHost(""), 0); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromLocalFiles( + true, + Ci.nsIClearDataService.CLEAR_COOKIES, + aResolve + ); + }); + Assert.equal(Services.cookies.countCookiesFromHost(""), 0); +}); + +// The following tests ensure we properly clear (partitioned/unpartitioned) +// cookies when using deleteDataFromBaseDomain and deleteDataFromHost. + +function getTestCookieName(host, topLevelBaseDomain) { + if (!topLevelBaseDomain) { + return host; + } + return `${host}_${topLevelBaseDomain}`; +} + +function setTestCookie({ + host, + topLevelBaseDomain = null, + originAttributes = {}, +}) { + SiteDataTestUtils.addToCookies({ + host, + name: getTestCookieName(host, topLevelBaseDomain), + originAttributes: getOAWithPartitionKey( + { topLevelBaseDomain }, + originAttributes + ), + }); +} + +function setTestCookies() { + // First party cookies + setTestCookie({ host: "example.net" }); + setTestCookie({ host: "test.example.net" }); + setTestCookie({ host: "example.org" }); + + // Third-party partitioned cookies. + setTestCookie({ host: "example.com", topLevelBaseDomain: "example.net" }); + setTestCookie({ + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + }); + setTestCookie({ host: "example.net", topLevelBaseDomain: "example.org" }); + setTestCookie({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + }); + + // Ensure we have the correct cookie test state. + // Not using countCookiesFromHost because it doesn't see partitioned cookies. + testCookieExists({ host: "example.net" }); + testCookieExists({ host: "test.example.net" }); + testCookieExists({ host: "example.org" }); + + testCookieExists({ host: "example.com", topLevelBaseDomain: "example.net" }); + testCookieExists({ + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + }); + testCookieExists({ host: "example.net", topLevelBaseDomain: "example.org" }); + testCookieExists({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + }); +} + +function testCookieExists({ + host, + topLevelBaseDomain = null, + expected = true, + originAttributes = {}, +}) { + let exists = Services.cookies.cookieExists( + host, + "path", + getTestCookieName(host, topLevelBaseDomain), + getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes) + ); + let message = `Cookie ${expected ? "is set" : "is not set"} for ${host}`; + if (topLevelBaseDomain) { + message += ` partitioned under ${topLevelBaseDomain}`; + } + Assert.equal(exists, expected, message); + return exists; +} + +/** + * Tests deleting (partitioned) cookies by base domain. + */ +add_task(async function test_baseDomain_cookies() { + Services.cookies.removeAll(); + setTestCookies(); + + // Clear cookies of example.net including partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_COOKIES, + aResolve + ); + }); + + testCookieExists({ host: "example.net", expected: false }); + testCookieExists({ host: "test.example.net", expected: false }); + testCookieExists({ host: "example.org" }); + + testCookieExists({ + host: "example.com", + topLevelBaseDomain: "example.net", + expected: false, + }); + testCookieExists({ + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + expected: false, + }); + testCookieExists({ + host: "example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + testCookieExists({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + + // Cleanup + Services.cookies.removeAll(); +}); + +/** + * Tests deleting (non-partitioned) cookies by host. + */ +add_task(async function test_host_cookies() { + Services.cookies.removeAll(); + setTestCookies(); + + // Clear cookies of example.net without partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_COOKIES, + aResolve + ); + }); + + testCookieExists({ host: "example.net", expected: false }); + testCookieExists({ host: "test.example.net" }); + testCookieExists({ host: "example.org" }); + // Third-party partitioned cookies under example.net should not be cleared. + testCookieExists({ host: "example.com", topLevelBaseDomain: "example.net" }); + setTestCookie({ + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + }); + // Third-party partitioned cookies of example.net should be removed, because + // CookieCleaner matches with host, but any partition key (oa = {}) via + // removeCookiesFromExactHost. + testCookieExists({ + host: "example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + testCookieExists({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + }); + + // Cleanup + Services.cookies.removeAll(); +}); + +/** + * Tests that we correctly clear data when given a subdomain. + */ +add_task(async function test_baseDomain_cookies_subdomain() { + Services.cookies.removeAll(); + setTestCookies(); + + // Clear cookies of test.example.net including partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "test.example.net", + false, + Ci.nsIClearDataService.CLEAR_COOKIES, + aResolve + ); + }); + + testCookieExists({ host: "example.net", expected: false }); + testCookieExists({ host: "test.example.net", expected: false }); + testCookieExists({ host: "example.org" }); + + testCookieExists({ + host: "example.com", + topLevelBaseDomain: "example.net", + expected: false, + }); + setTestCookie({ + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + expected: false, + }); + testCookieExists({ + host: "example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + testCookieExists({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + + // Cleanup + Services.cookies.removeAll(); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_downloads.js b/toolkit/components/cleardata/tests/unit/test_downloads.js new file mode 100644 index 0000000000..72de763ce3 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_downloads.js @@ -0,0 +1,310 @@ +/** + * Tests for downloads. + */ + +"use strict"; + +const { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" +); +const { FileTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/FileTestUtils.sys.mjs" +); + +const TEST_TARGET_FILE_NAME = "test-download.txt"; +let fileURL; +let downloadList; + +function createFileURL() { + if (!fileURL) { + const file = Services.dirsvc.get("TmpD", Ci.nsIFile); + file.append("foo.txt"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + fileURL = Services.io.newFileURI(file); + } + + return fileURL; +} + +async function createDownloadList() { + if (!downloadList) { + Downloads._promiseListsInitialized = null; + Downloads._lists = {}; + Downloads._summaries = {}; + + downloadList = await Downloads.getList(Downloads.ALL); + } + + return downloadList; +} + +add_task(async function test_all_downloads() { + const url = createFileURL(); + const list = await createDownloadList(); + + // First download. + let download = await Downloads.createDownload({ + source: { url: url.spec, isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + let view; + let removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + let items = await list.getAll(); + Assert.equal(items.length, 1); + + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 0); +}); + +add_task(async function test_range_downloads() { + const url = createFileURL(); + const list = await createDownloadList(); + + let download = await Downloads.createDownload({ + source: { url: url.spec, isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + // Start + cancel. I need to have a startTime value. + await download.start(); + await download.cancel(); + + let items = await list.getAll(); + Assert.equal(items.length, 1); + + let view; + let removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + await new Promise(resolve => { + Services.clearData.deleteDataInTimeRange( + download.startTime.getTime() * 1000, + download.startTime.getTime() * 1000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 0); +}); + +add_task(async function test_principal_downloads() { + const list = await createDownloadList(); + + let download = await Downloads.createDownload({ + source: { url: "http://example.net", isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + download = await Downloads.createDownload({ + source: { url: "http://example.com", isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + let items = await list.getAll(); + Assert.equal(items.length, 2); + + let view; + let removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + let uri = Services.io.newURI("http://example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 1); + + removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 0); +}); + +add_task(async function test_basedomain_downloads() { + const list = await createDownloadList(); + + let download = await Downloads.createDownload({ + source: { url: "http://example.net", isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + download = await Downloads.createDownload({ + source: { url: "http://test.example.net", isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + download = await Downloads.createDownload({ + source: { url: "https://foo.bar.example.net", isPrivate: true }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + download = await Downloads.createDownload({ + source: { url: "http://example.com", isPrivate: false }, + target: { path: FileTestUtils.getTempFile(TEST_TARGET_FILE_NAME).path }, + }); + Assert.ok(!!download); + list.add(download); + + let items = await list.getAll(); + Assert.equal(items.length, 4); + + let view; + let removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + await new Promise(resolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.net", + true /* user request */, + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 1); + + removePromise = new Promise(resolve => { + view = { + onDownloadAdded() {}, + onDownloadChanged() {}, + onDownloadRemoved() { + resolve(); + }, + }; + }); + + await list.addView(view); + + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOWNLOADS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + await removePromise; + + items = await list.getAll(); + Assert.equal(items.length, 0); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_identity_credential_storage.js b/toolkit/components/cleardata/tests/unit/test_identity_credential_storage.js new file mode 100644 index 0000000000..13369fc787 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_identity_credential_storage.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "IdentityCredentialStorageService", + "@mozilla.org/browser/identity-credential-storage-service;1", + "nsIIdentityCredentialStorageService" +); + +do_get_profile(); + +add_task(async function test_deleteByRange() { + Services.prefs.setBoolPref( + "dom.security.credentialmanagement.identity.enabled", + true + ); + const expiry = Date.now() + 24 * 60 * 60; + let rpPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://rp.com/"), + {} + ); + let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI("https://idp.com/"), + {} + ); + const credentialID = "ID"; + + // Test initial value + let registered = {}; + let allowLogout = {}; + IdentityCredentialStorageService.getState( + rpPrincipal, + idpPrincipal, + credentialID, + registered, + allowLogout + ); + Assert.ok(!registered.value, "Should not be registered initially."); + Assert.ok(!allowLogout.value, "Should not allow logout initially."); + + // Set and read a value + IdentityCredentialStorageService.setState( + rpPrincipal, + idpPrincipal, + credentialID, + true, + true + ); + + IdentityCredentialStorageService.getState( + rpPrincipal, + idpPrincipal, + credentialID, + registered, + allowLogout + ); + Assert.ok(registered.value, "Should be registered by set."); + Assert.ok(allowLogout.value, "Should now allow logout by set."); + + let from = Date.now() + 60 * 60; + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + from * 1000, + expiry * 1000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_CREDENTIAL_MANAGER_STATE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + IdentityCredentialStorageService.getState( + rpPrincipal, + idpPrincipal, + credentialID, + registered, + allowLogout + ); + + Assert.ok( + registered.value, + "Should be existing since the value is not deleted" + ); + + from = Date.now() - 60 * 60; + + await new Promise(aResolve => { + Services.clearData.deleteDataInTimeRange( + from * 1000, + expiry * 1000, + true /* user request */, + Ci.nsIClearDataService.CLEAR_CREDENTIAL_MANAGER_STATE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + IdentityCredentialStorageService.getState( + rpPrincipal, + idpPrincipal, + credentialID, + registered, + allowLogout + ); + Assert.ok(!registered.value, "Should not be existing"); + + Services.prefs.clearUserPref( + "dom.security.credentialmanagement.identity.enabled" + ); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_network_cache.js b/toolkit/components/cleardata/tests/unit/test_network_cache.js new file mode 100644 index 0000000000..bb54cdc6a8 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_network_cache.js @@ -0,0 +1,316 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test clearing cache. + */ + +"use strict"; + +function getPartitionedLoadContextInfo( + { scheme, topLevelBaseDomain, port }, + originAttributes = {} +) { + return Services.loadContextInfo.custom( + false, + getOAWithPartitionKey( + { scheme, topLevelBaseDomain, port }, + originAttributes + ) + ); +} + +add_task(async function test_deleteFromHost() { + await SiteDataTestUtils.addCacheEntry("http://example.com/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.com/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.addCacheEntry("http://example.org/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.org/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.com", + true, + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache is cleared" + ); + + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteFromPrincipal() { + await SiteDataTestUtils.addCacheEntry("http://example.com/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.com/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.addCacheEntry("http://example.org/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.org/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.com/" + ); + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true, + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache is cleared" + ); + + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.clear(); +}); + +add_task(async function test_deleteFromBaseDomain() { + for (let cacheType of ["disk", "memory"]) { + await SiteDataTestUtils.addCacheEntry("http://example.com/", cacheType); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", cacheType), + `The ${cacheType} cache has an entry.` + ); + + await SiteDataTestUtils.addCacheEntry("http://example.org/", cacheType); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", cacheType), + `The ${cacheType} cache has an entry.` + ); + + // Partitioned cache. + await SiteDataTestUtils.addCacheEntry( + "http://example.com/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.org" }) + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry( + "http://example.com/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.org" }) + ), + `The ${cacheType} cache has a partitioned entry` + ); + await SiteDataTestUtils.addCacheEntry( + "http://example.org/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.com" }) + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry( + "http://example.org/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.com" }) + ), + `The ${cacheType} cache has a partitioned entry` + ); + + // Clear an unrelated base domain. + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "foo.com", + true, + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + // Should still have all cache entries. + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", cacheType), + `The ${cacheType} cache has an entry.` + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", cacheType), + `The ${cacheType} cache has an entry.` + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry( + "http://example.com/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.org" }) + ), + `The ${cacheType} cache has a partitioned entry` + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry( + "http://example.org/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.com" }) + ), + `The ${cacheType} cache has a partitioned entry` + ); + + // Clear data for example.com + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.com", + true, + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", cacheType), + `The ${cacheType} cache is cleared.` + ); + + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", cacheType), + `The ${cacheType} cache has an entry.` + ); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry( + "http://example.com/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.org" }) + ), + `The ${cacheType} cache is cleared.` + ); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry( + "http://example.org/", + cacheType, + getPartitionedLoadContextInfo({ topLevelBaseDomain: "example.com" }) + ), + `The ${cacheType} cache is cleared.` + ); + await SiteDataTestUtils.clear(); + } +}); + +add_task(async function test_deleteAll() { + await SiteDataTestUtils.addCacheEntry("http://example.com/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.com/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache has an entry" + ); + + await SiteDataTestUtils.addCacheEntry("http://example.org/", "disk"); + await SiteDataTestUtils.addCacheEntry("http://example.org/", "memory"); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache has an entry" + ); + Assert.ok( + SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache has an entry" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_NETWORK_CACHE, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.com/", "memory"), + "The memory cache is cleared" + ); + + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.org/", "disk"), + "The disk cache is cleared" + ); + Assert.ok( + !SiteDataTestUtils.hasCacheEntry("http://example.org/", "memory"), + "The memory cache is cleared" + ); + + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_passwords.js b/toolkit/components/cleardata/tests/unit/test_passwords.js new file mode 100644 index 0000000000..895c135754 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_passwords.js @@ -0,0 +1,89 @@ +/** + * Tests for passwords. + */ + +"use strict"; + +const URL = "http://example.com"; + +const { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); + +add_task(async function test_principal_downloads() { + // Store the strings "user" and "pass" using similarly looking glyphs. + let loginInfo = LoginTestUtils.testData.formLogin({ + origin: URL, + formActionOrigin: URL, + username: "admin", + password: "12345678", + usernameField: "field_username", + passwordField: "field_password", + }); + await Services.logins.addLoginAsync(loginInfo); + + Assert.equal(countLogins(URL), 1); + + let uri = Services.io.newURI(URL); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_PASSWORDS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + Assert.equal(countLogins(URL), 0); + + LoginTestUtils.clearData(); +}); + +add_task(async function test_all() { + // Store the strings "user" and "pass" using similarly looking glyphs. + let loginInfo = LoginTestUtils.testData.formLogin({ + origin: URL, + formActionOrigin: URL, + username: "admin", + password: "12345678", + usernameField: "field_username", + passwordField: "field_password", + }); + await Services.logins.addLoginAsync(loginInfo); + + Assert.equal(countLogins(URL), 1); + + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PASSWORDS, + value => { + Assert.equal(value, 0); + resolve(); + } + ); + }); + + Assert.equal(countLogins(URL), 0); + + LoginTestUtils.clearData(); +}); + +function countLogins(origin) { + let count = 0; + const logins = Services.logins.getAllLogins(); + for (const login of logins) { + if (login.origin == origin) { + ++count; + } + } + + return count; +} diff --git a/toolkit/components/cleardata/tests/unit/test_permissions.js b/toolkit/components/cleardata/tests/unit/test_permissions.js new file mode 100644 index 0000000000..e3b7df60a7 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_permissions.js @@ -0,0 +1,424 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for permissions + */ + +"use strict"; + +add_task(async function test_all_permissions() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) == null + ); +}); + +add_task(async function test_principal_permissions() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + const anotherUri = Services.io.newURI("https://example.com"); + const anotherPrincipal = + Services.scriptSecurityManager.createContentPrincipal(anotherUri, {}); + + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) == null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); + +function addTestPermissions() { + Services.perms.removeAll(); + + PermissionTestUtils.add( + "https://example.net", + "geo", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "http://example.net", + "cookie", + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://bar.example.net", + "geo", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://foo.bar.example.net", + "geo", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://example.com", + "3rdPartyStorage^https://example.net", + Services.perms.ALLOW_ACTION + ); + + PermissionTestUtils.add( + "https://example.com", + "cookie", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "http://example.com", + "geo", + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + PermissionTestUtils.getPermissionObject("https://example.net", "geo", true) + .capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject( + "http://example.net", + "cookie", + true + ).capability, + Services.perms.DENY_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject( + "https://bar.example.net", + "geo", + true + ).capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject( + "https://foo.bar.example.net", + "geo", + true + ).capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject( + "https://example.com", + "3rdPartyStorage^https://example.net", + true + ).capability, + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + PermissionTestUtils.getPermissionObject( + "https://example.com", + "cookie", + true + ).capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject("http://example.com", "geo", true) + .capability, + Services.perms.ALLOW_ACTION + ); +} + +add_task(async function test_basedomain_permissions() { + for (let domain of [ + "example.net", + "test.example.net", + "foo.bar.example.net", + ]) { + addTestPermissions(); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + domain, + true /* user request */, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + // Should have cleared all entries associated with the base domain. + Assert.ok( + !PermissionTestUtils.getPermissionObject( + "https://example.net", + "geo", + true + ) + ); + Assert.ok( + !PermissionTestUtils.getPermissionObject( + "http://example.net", + "cookie", + true + ) + ); + Assert.ok( + !PermissionTestUtils.getPermissionObject( + "https://bar.example.net", + "geo", + true + ) + ); + Assert.ok( + !PermissionTestUtils.getPermissionObject( + "https://foo.bar.example.net", + "geo", + true + ) + ); + Assert.ok( + !PermissionTestUtils.getPermissionObject( + "https://example.com", + "3rdPartyStorage^https://example.net", + true + ) + ); + + // Unrelated entries should still exist. + Assert.equal( + PermissionTestUtils.getPermissionObject( + "https://example.com", + "cookie", + true + ).capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject("http://example.com", "geo", true) + .capability, + Services.perms.ALLOW_ACTION + ); + } + + Services.perms.removeAll(); +}); + +add_task(async function test_host_permissions() { + addTestPermissions(); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "bar.example.net", + true /* user request */, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + // Should have cleared all entries associated with the host and its + // subdomains. + Assert.ok( + !PermissionTestUtils.getPermissionObject( + "https://bar.example.net", + "geo", + true + ) + ); + Assert.ok( + !PermissionTestUtils.getPermissionObject( + "https://foo.bar.example.net", + "geo", + true + ) + ); + + // Unrelated entries should still exist. + Assert.equal( + PermissionTestUtils.getPermissionObject("https://example.net", "geo", true) + .capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject( + "http://example.net", + "cookie", + true + ).capability, + Services.perms.DENY_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject( + "https://example.com", + "3rdPartyStorage^https://example.net", + true + ).capability, + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + PermissionTestUtils.getPermissionObject( + "https://example.com", + "cookie", + true + ).capability, + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.getPermissionObject("http://example.com", "geo", true) + .capability, + Services.perms.ALLOW_ACTION + ); + + Services.perms.removeAll(); +}); + +add_task(async function test_3rdpartystorage_permissions() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + + const anotherUri = Services.io.newURI("https://example.com"); + const anotherPrincipal = + Services.scriptSecurityManager.createContentPrincipal(anotherUri, {}); + Services.perms.addFromPrincipal( + anotherPrincipal, + "cookie", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "3rdPartyStorage^https://example.net", + Services.perms.ALLOW_ACTION + ); + + const oneMoreUri = Services.io.newURI("https://example.org"); + const oneMorePrincipal = + Services.scriptSecurityManager.createContentPrincipal(oneMoreUri, {}); + Services.perms.addFromPrincipal( + oneMorePrincipal, + "cookie", + Services.perms.ALLOW_ACTION + ); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject( + anotherPrincipal, + "3rdPartyStorage^https://example.net", + true + ) != null + ); + Assert.ok( + Services.perms.getPermissionObject(oneMorePrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.ok( + Services.perms.getPermissionObject(principal, "cookie", true) == null + ); + Assert.ok( + Services.perms.getPermissionObject(anotherPrincipal, "cookie", true) != null + ); + Assert.ok( + Services.perms.getPermissionObject( + anotherPrincipal, + "3rdPartyStorage^https://example.net", + true + ) == null + ); + Assert.ok( + Services.perms.getPermissionObject(oneMorePrincipal, "cookie", true) != null + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_quota.js b/toolkit/components/cleardata/tests/unit/test_quota.js new file mode 100644 index 0000000000..e916c33d93 --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_quota.js @@ -0,0 +1,560 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for the QuotaCleaner. + */ + +"use strict"; + +// The following tests ensure we properly clear (partitioned/unpartitioned) +// localStorage and indexedDB when using deleteDataFromBaseDomain, +// deleteDataFromHost and deleteDataFromPrincipal. + +// Skip localStorage tests when using legacy localStorage. The legacy +// localStorage implementation does not support clearing data by principal. See +// Bug 1688221, Bug 1688665. +const skipLocalStorageTests = Services.prefs.getBoolPref( + "dom.storage.enable_unsupported_legacy_implementation" +); + +// XXX(krosylight): xpcshell does not support background tasks +const skipCleanupAfterDeletionAtShutdownTests = Services.prefs.getBoolPref( + "dom.quotaManager.backgroundTask.enabled" +); + +/** + * Create an origin with partitionKey. + * @param {String} host - Host portion of origin to create. + * @param {String} [topLevelBaseDomain] - Optional first party base domain to use for partitionKey. + * @param {Object} [originAttributes] - Optional object of origin attributes to + * set. If topLevelBaseDomain is passed, the partitionKey will be overwritten. + * @returns {String} Origin with suffix. + */ +function getOrigin(host, topLevelBaseDomain, originAttributes = {}) { + return getPrincipal(host, topLevelBaseDomain, originAttributes).origin; +} + +function getPrincipal(host, topLevelBaseDomain, originAttributes = {}) { + originAttributes = getOAWithPartitionKey( + { topLevelBaseDomain }, + originAttributes + ); + let principal = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`https://${host}`), + originAttributes + ); + return principal; +} + +function getTestEntryName(host, topLevelBaseDomain) { + if (!topLevelBaseDomain) { + return host; + } + return `${host}_${topLevelBaseDomain}`; +} + +function setTestEntry({ + storageType, + host, + topLevelBaseDomain = null, + originAttributes = {}, +}) { + let origin = getOrigin(host, topLevelBaseDomain, originAttributes); + if (storageType == "localStorage") { + SiteDataTestUtils.addToLocalStorage( + origin, + getTestEntryName(host, topLevelBaseDomain), + "bar" + ); + return; + } + SiteDataTestUtils.addToIndexedDB(origin); +} + +async function testEntryExists({ + storageType, + host, + topLevelBaseDomain = null, + expected = true, + originAttributes = {}, +}) { + let exists; + let origin = getOrigin(host, topLevelBaseDomain, originAttributes); + if (storageType == "localStorage") { + exists = SiteDataTestUtils.hasLocalStorage(origin, [ + { key: getTestEntryName(host, topLevelBaseDomain), value: "bar" }, + ]); + } else { + exists = await SiteDataTestUtils.hasIndexedDB(origin); + } + + let message = `${storageType} entry ${ + expected ? "is set" : "is not set" + } for ${host}`; + if (topLevelBaseDomain) { + message += ` partitioned under ${topLevelBaseDomain}`; + } + Assert.equal(exists, expected, message); + return exists; +} + +const TEST_ORIGINS = [ + // First party + { host: "example.net" }, + { host: "test.example.net" }, + { host: "example.org" }, + + // Third-party partitioned. + { host: "example.com", topLevelBaseDomain: "example.net" }, + { + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + }, + { host: "example.net", topLevelBaseDomain: "example.org" }, + { host: "test.example.net", topLevelBaseDomain: "example.org" }, +]; + +async function setTestEntries(storageType) { + for (const origin of TEST_ORIGINS) { + setTestEntry({ storageType, ...origin }); + } + + // Ensure we have the correct storage test state. + for (const origin of TEST_ORIGINS) { + await testEntryExists({ storageType, ...origin }); + } +} + +/** + * Run the base domain test with either localStorage or indexedDB. + * @param {('localStorage'|'indexedDB')} storageType + */ +async function runTestBaseDomain(storageType) { + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + await setTestEntries(storageType); + + // Clear entries of example.net including partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + await testEntryExists({ storageType, host: "example.net", expected: false }); + await testEntryExists({ + storageType, + host: "test.example.net", + expected: false, + }); + await testEntryExists({ storageType, host: "example.org" }); + + await testEntryExists({ + storageType, + host: "example.com", + topLevelBaseDomain: "example.net", + expected: false, + }); + await testEntryExists({ + storageType, + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + expected: false, + }); + await testEntryExists({ + storageType, + host: "example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + await testEntryExists({ + storageType, + host: "test.example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + + // Cleanup + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); +} + +/** + * Run the host test with either localStorage or indexedDB. + * @param {('localStorage'|'indexedDB')} storageType + */ +async function runTestHost(storageType) { + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + await setTestEntries(storageType); + + // Clear entries of example.net without partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + await testEntryExists({ storageType, host: "example.net", expected: false }); + // QuotaCleaner#deleteByHost also clears subdomains. + await testEntryExists({ + storageType, + host: "test.example.net", + expected: false, + }); + await testEntryExists({ storageType, host: "example.org" }); + + await testEntryExists({ + storageType, + host: "example.com", + topLevelBaseDomain: "example.net", + expected: true, + }); + await testEntryExists({ + storageType, + host: "example.com", + topLevelBaseDomain: "example.net", + originAttributes: { userContextId: 1 }, + expected: true, + }); + // QuotaCleaner#deleteByHost ignores partitionKey. + await testEntryExists({ + storageType, + host: "example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + await testEntryExists({ + storageType, + host: "test.example.net", + topLevelBaseDomain: "example.org", + expected: false, + }); + + // Cleanup + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); +} + +/** + * Run the principal test with either localStorage or indexedDB. + * @param {('localStorage'|'indexedDB')} storageType + */ +async function runTestPrincipal(storageType) { + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + // First party + setTestEntry({ storageType, host: "example.net" }); + setTestEntry({ + storageType, + host: "example.net", + originAttributes: { userContextId: 2 }, + }); + setTestEntry({ + storageType, + host: "example.net", + originAttributes: { privateBrowsingId: 1 }, + }); + setTestEntry({ storageType, host: "test.example.net" }); + setTestEntry({ storageType, host: "example.org" }); + + // Third-party partitioned. + setTestEntry({ + storageType, + host: "example.net", + topLevelBaseDomain: "example.com", + }); + + // Ensure we have the correct storage test state. + await testEntryExists({ storageType, host: "example.net" }); + await testEntryExists({ + storageType, + host: "example.net", + originAttributes: { userContextId: 2 }, + }); + await testEntryExists({ + storageType, + host: "example.net", + originAttributes: { privateBrowsingId: 1 }, + }); + await testEntryExists({ storageType, host: "test.example.net" }); + await testEntryExists({ storageType, host: "example.org" }); + await testEntryExists({ + storageType, + host: "example.net", + topLevelBaseDomain: "example.com", + }); + + // Clear entries from principal with custom OA. + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + getPrincipal("example.net", null, { userContextId: 2 }), + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + // Test that we only deleted entries for the exact origin. + await testEntryExists({ storageType, host: "example.net" }); + await testEntryExists({ + expected: false, + storageType, + host: "example.net", + originAttributes: { userContextId: 2 }, + }); + await testEntryExists({ + storageType, + host: "example.net", + originAttributes: { privateBrowsingId: 1 }, + }); + await testEntryExists({ storageType, host: "test.example.net" }); + await testEntryExists({ storageType, host: "example.org" }); + await testEntryExists({ + storageType, + host: "example.net", + topLevelBaseDomain: "example.com", + }); + + // Clear entries of from partitioned principal. + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + getPrincipal("example.net", "example.com"), + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + // Test that we only deleted entries for the partition. + await testEntryExists({ storageType, host: "example.net" }); + await testEntryExists({ + expected: false, + storageType, + host: "example.net", + originAttributes: { userContextId: 2 }, + }); + await testEntryExists({ + storageType, + host: "example.net", + originAttributes: { privateBrowsingId: 1 }, + }); + await testEntryExists({ storageType, host: "test.example.net" }); + await testEntryExists({ storageType, host: "example.org" }); + await testEntryExists({ + expected: false, + storageType, + host: "example.net", + topLevelBaseDomain: "example.com", + }); + + // Clear entries of from principal without suffix. + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + getPrincipal("example.net", null), + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + // Test that we only deleted entries for the given principal, and not entries + // for principals with the same host, but different OriginAttributes or + // subdomains. + await testEntryExists({ expected: false, storageType, host: "example.net" }); + await testEntryExists({ + expected: false, + storageType, + host: "example.net", + originAttributes: { userContextId: 2 }, + }); + await testEntryExists({ + storageType, + host: "example.net", + originAttributes: { privateBrowsingId: 1 }, + }); + + await testEntryExists({ storageType, host: "test.example.net" }); + await testEntryExists({ storageType, host: "example.org" }); + await testEntryExists({ + expected: false, + storageType, + host: "example.net", + topLevelBaseDomain: "example.com", + }); + + // Cleanup + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); +} + +// Tests + +add_task(function setup() { + // Allow setting local storage in xpcshell tests. + Services.prefs.setBoolPref("dom.storage.client_validation", false); +}); + +/** + * Tests deleting localStorage entries by host. + */ +add_task(async function test_host_localStorage() { + await runTestHost("localStorage"); +}); + +/** + * Tests deleting indexedDB entries by host. + */ +add_task(async function test_host_indexedDB() { + await runTestHost("indexedDB"); +}); + +/** + * Tests deleting (partitioned) localStorage entries by base domain. + */ +add_task(async function test_baseDomain_localStorage() { + await runTestBaseDomain("localStorage"); +}); + +/** + * Tests deleting (partitioned) indexedDB entries by base domain. + */ +add_task(async function test_baseDomain_indexedDB() { + await runTestBaseDomain("indexedDB"); +}); + +/** + * Tests deleting localStorage entries by principal. + */ +add_task(async function test_principal_localStorage() { + // Bug 1688221, Bug 1688665. + if (skipLocalStorageTests) { + info("Skipping test"); + return; + } + await runTestPrincipal("localStorage"); +}); + +function getRelativeFile(...components) { + const profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + const file = profileDir.clone(); + for (const component of components) { + file.append(component); + } + + return file; +} + +function countSubitems(file) { + const entriesIterator = file.directoryEntries; + let count = 0; + while (entriesIterator.hasMoreElements()) { + ++count; + entriesIterator.nextFile; + } + return count; +} + +add_task(async function test_deleteAllAtShutdown() { + const storageType = "indexedDB"; + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + const toBeRemovedDir = getRelativeFile("storage", "to-be-removed"); + if (toBeRemovedDir.exists()) { + toBeRemovedDir.remove(true); + } + + await setTestEntries(storageType); + + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNTEARDOWN + ); + + // Clear entries from principal with custom OA. + for (const origin of TEST_ORIGINS) { + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + getPrincipal( + origin.host, + origin.topLevelBaseDomain, + origin.originAttributes + ), + false, + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + await testEntryExists({ expected: false, storageType, ...origin }); + } + + Assert.ok( + toBeRemovedDir.exists(), + "to-be-removed directory should exist now" + ); + + Assert.equal( + countSubitems(toBeRemovedDir), + TEST_ORIGINS.length, + `storage/to-be-removed has ${TEST_ORIGINS.length} subdirectories` + ); + + if (skipCleanupAfterDeletionAtShutdownTests) { + // XXX(krosylight): xpcshell does not support background tasks + return; + } + + info("Verifying cleanupAfterDeletionAtShutdown"); + await new Promise(aResolve => { + Services.clearData.cleanupAfterDeletionAtShutdown( + Ci.nsIClearDataService.CLEAR_DOM_QUOTA, + aResolve + ); + }); + + Assert.ok( + !toBeRemovedDir.exists(), + "to-be-removed directory should disappear" + ); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_security_settings.js b/toolkit/components/cleardata/tests/unit/test_security_settings.js new file mode 100644 index 0000000000..b14f567bab --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_security_settings.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test for SecuritySettingsCleaner. + * This tests both, the SiteSecurityService and the ClientAuthRememberService. + */ + +"use strict"; + +let gSSService = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService +); + +let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService( + Ci.nsIClientAuthRememberService +); + +let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +// These are not actual server and client certs. The ClientAuthRememberService +// does not care which certs we store decisions for, as long as they're valid. +let [clientCert] = certDB.getCerts(); + +function addSecurityInfo({ host, topLevelBaseDomain, originAttributes = {} }) { + let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes); + + let uri = Services.io.newURI(`https://${host}`); + + gSSService.processHeader(uri, "max-age=1000;", attrs); + + cars.rememberDecisionScriptable(host, attrs, clientCert); +} + +function addTestSecurityInfo() { + // First party + addSecurityInfo({ host: "example.net" }); + addSecurityInfo({ host: "test.example.net" }); + addSecurityInfo({ host: "example.org" }); + + // Third-party partitioned + addSecurityInfo({ host: "example.com", topLevelBaseDomain: "example.net" }); + addSecurityInfo({ host: "example.net", topLevelBaseDomain: "example.org" }); + addSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + }); + + // Ensure we have the correct state initially. + testSecurityInfo({ host: "example.net" }); + testSecurityInfo({ host: "test.example.net" }); + testSecurityInfo({ host: "example.org" }); + testSecurityInfo({ host: "example.com", topLevelBaseDomain: "example.net" }); + testSecurityInfo({ host: "example.net", topLevelBaseDomain: "example.org" }); + testSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + }); +} + +function testSecurityInfo({ + host, + topLevelBaseDomain, + expectedHSTS = true, + expectedCARS = true, + originAttributes = {}, +}) { + let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes); + + let messageSuffix = `for ${host}`; + if (topLevelBaseDomain) { + messageSuffix += ` partitioned under ${topLevelBaseDomain}`; + } + + let uri = Services.io.newURI(`https://${host}`); + let isSecure = gSSService.isSecureURI(uri, attrs); + Assert.equal( + isSecure, + expectedHSTS, + `HSTS ${expectedHSTS ? "is set" : "is not set"} ${messageSuffix}` + ); + + let hasRemembered = cars.hasRememberedDecisionScriptable(host, attrs, {}); + // CARS deleteDecisionsByHost does not include subdomains. That means for some + // test cases we expect a different remembered state. + Assert.equal( + hasRemembered, + expectedCARS, + `CAR ${expectedCARS ? "is set" : "is not set"} ${messageSuffix}` + ); +} + +add_task(async function test_baseDomain() { + gSSService.clearAll(); + + // ---- hsts cleaner ---- + addTestSecurityInfo(); + + // Clear hsts data of example.net including partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_HSTS, + aResolve + ); + }); + + testSecurityInfo({ + host: "example.net", + expectedHSTS: false, + expectedCARS: true, + }); + // HSTSCleaner also removes subdomain settings. + testSecurityInfo({ + host: "test.example.net", + expectedHSTS: false, + expectedCARS: true, + }); + testSecurityInfo({ host: "example.org" }); + + testSecurityInfo({ + host: "example.com", + topLevelBaseDomain: "example.net", + expectedHSTS: false, + expectedCARS: true, + }); + testSecurityInfo({ + host: "example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: false, + expectedCARS: true, + }); + testSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: false, + expectedCARS: true, + }); + + // ---- client auth remember cleaner ----- + addTestSecurityInfo(); + + // Clear security settings of example.net including partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE, + aResolve + ); + }); + + testSecurityInfo({ + host: "example.net", + expectedHSTS: true, + expectedCARS: false, + }); + // ClientAuthRememberCleaner also removes subdomain settings. + testSecurityInfo({ + host: "test.example.net", + expectedHSTS: true, + expectedCARS: false, + }); + testSecurityInfo({ host: "example.org" }); + + testSecurityInfo({ + host: "example.com", + topLevelBaseDomain: "example.net", + expectedHSTS: true, + expectedCARS: false, + }); + testSecurityInfo({ + host: "example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: true, + expectedCARS: false, + }); + testSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: true, + expectedCARS: false, + }); + + // Cleanup + gSSService.clearAll(); +}); + +add_task(async function test_host() { + gSSService.clearAll(); + + // ---- HSTS cleaer ---- + addTestSecurityInfo(); + + // Clear security settings of example.net without partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_HSTS, + aResolve + ); + }); + + testSecurityInfo({ + host: "example.net", + expectedHSTS: false, + expectedCARS: true, + }); + testSecurityInfo({ + host: "test.example.net", + expectedHSTS: false, + expectedCARS: true, + }); + testSecurityInfo({ host: "example.org" }); + + testSecurityInfo({ host: "example.com", topLevelBaseDomain: "example.net" }); + testSecurityInfo({ + host: "example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: false, + expectedCARS: true, + }); + testSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: false, + expectedCARS: true, + }); + + // Cleanup + gSSService.clearAll(); + + // --- clientAuthRemember cleaner --- + + addTestSecurityInfo(); + + // Clear security settings of example.net without partitions. + await new Promise(aResolve => { + Services.clearData.deleteDataFromHost( + "example.net", + false, + Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE, + aResolve + ); + }); + + testSecurityInfo({ + host: "example.net", + expectedHSTS: true, + expectedCARS: false, + }); + testSecurityInfo({ + host: "test.example.net", + expectedHSTS: true, + expectedCARS: true, + }); + testSecurityInfo({ host: "example.org" }); + + testSecurityInfo({ host: "example.com", topLevelBaseDomain: "example.net" }); + testSecurityInfo({ + host: "example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: true, + expectedCARS: false, + }); + testSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.org", + expectedHSTS: true, + expectedCARS: true, + }); + + // Cleanup + gSSService.clearAll(); +}); diff --git a/toolkit/components/cleardata/tests/unit/test_storage_permission.js b/toolkit/components/cleardata/tests/unit/test_storage_permission.js new file mode 100644 index 0000000000..a44e9f2c6a --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/test_storage_permission.js @@ -0,0 +1,398 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests for permissions + */ + +"use strict"; + +// Test that only the storageAccessAPI gets removed. +add_task(async function test_removing_storage_permission() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + Services.perms.addFromPrincipal( + principal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + principal, + "cookie", + Services.perms.ALLOW_ACTION + ); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "There is a storageAccessAPI permission set" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION, + "the storageAccessAPI permission has been removed" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal(principal, "cookie"), + Services.perms.ALLOW_ACTION, + "the cookie permission has not been removed" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); + +// Test that the storageAccessAPI gets removed from a particular principal +add_task(async function test_removing_storage_permission_from_principal() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + const anotherUri = Services.io.newURI("https://example.com"); + const anotherPrincipal = + Services.scriptSecurityManager.createContentPrincipal(anotherUri, {}); + + Services.perms.addFromPrincipal( + principal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has been added to the first principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + anotherPrincipal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has been added to the second principal" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + true /* user request */, + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION, + "storageAccessAPI permission has been removed from the first principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + anotherPrincipal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has not been removed from the second principal" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); + +// Test that the storageAccessAPI gets removed from a base domain. +add_task(async function test_removing_storage_permission_from_base_domainl() { + const uri = Services.io.newURI("https://example.net"); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + const uriSub = Services.io.newURI("http://test.example.net"); + const principalSub = Services.scriptSecurityManager.createContentPrincipal( + uriSub, + {} + ); + + const anotherUri = Services.io.newURI("https://example.com"); + const anotherPrincipal = + Services.scriptSecurityManager.createContentPrincipal(anotherUri, {}); + + Services.perms.addFromPrincipal( + principal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + principalSub, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Services.perms.addFromPrincipal( + anotherPrincipal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has been added to the first principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principalSub, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has been added to the subdomain principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + anotherPrincipal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has been added to the second principal" + ); + + await new Promise(aResolve => { + Services.clearData.deleteDataFromBaseDomain( + "example.net", + true /* user request */, + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS, + value => { + Assert.equal(value, 0); + aResolve(); + } + ); + }); + + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principal, + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION, + "storageAccessAPI permission has been removed from the first principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + principalSub, + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION, + "storageAccessAPI permission has been removed from the sub domain principal" + ); + Assert.equal( + Services.perms.testExactPermissionFromPrincipal( + anotherPrincipal, + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION, + "storageAccessAPI permission has not been removed from the second principal" + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); + +// Tests the deleteUserInteractionForClearingHistory function. +add_task(async function test_deleteUserInteractionForClearingHistory() { + // These should be retained. + PermissionTestUtils.add( + "https://example.com", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.com", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.com^userContextId=3", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + // These should be removed. + PermissionTestUtils.add( + "https://example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://sub.example.org^userContextId=3", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + let principalWithStorage = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "https://sub.example.com" + ); + + await new Promise(resolve => { + return Services.clearData.deleteUserInteractionForClearingHistory( + [principalWithStorage], + 0, + resolve + ); + }); + + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.org", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.org", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.org^userContextId=3", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.com", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.com", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://sub.example.com^userContextId=3", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + + // This permission is set earlier than the timestamp and should be retained. + PermissionTestUtils.add( + "https://example.net", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + // Add some time in between taking the snapshot of the timestamp + // to avoid flakyness. + await new Promise(c => do_timeout(100, c)); + let timestamp = Date.now(); + await new Promise(c => do_timeout(100, c)); + + // This permission is set later than the timestamp and should be removed. + PermissionTestUtils.add( + "https://example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + await new Promise(resolve => { + return Services.clearData.deleteUserInteractionForClearingHistory( + [principalWithStorage], + // ClearDataService takes PRTime (microseconds) + timestamp * 1000, + resolve + ); + }); + + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.org", + "storageAccessAPI" + ), + Services.perms.UNKNOWN_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.net", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + Assert.equal( + PermissionTestUtils.testExactPermission( + "https://example.com", + "storageAccessAPI" + ), + Services.perms.ALLOW_ACTION + ); + + await new Promise(aResolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => aResolve() + ); + }); +}); diff --git a/toolkit/components/cleardata/tests/unit/xpcshell.ini b/toolkit/components/cleardata/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..49c8fcf38c --- /dev/null +++ b/toolkit/components/cleardata/tests/unit/xpcshell.ini @@ -0,0 +1,19 @@ +[DEFAULT] +tags = condprof +firefox-appdir = browser +head = head.js +skip-if = toolkit == 'android' +support-files = + +[test_basic.js] +[test_certs.js] +[test_cookies.js] +[test_identity_credential_storage.js] +[test_downloads.js] +[test_network_cache.js] +skip-if = condprof # Bug 1769154 - expected fail w/condprof +[test_passwords.js] +[test_permissions.js] +[test_security_settings.js] +[test_storage_permission.js] +[test_quota.js] |