summaryrefslogtreecommitdiffstats
path: root/toolkit/components/cleardata
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/components/cleardata
parentInitial commit. (diff)
downloadthunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz
thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/cleardata')
-rw-r--r--toolkit/components/cleardata/ClearDataService.sys.mjs1794
-rw-r--r--toolkit/components/cleardata/PrincipalsCollector.sys.mjs178
-rw-r--r--toolkit/components/cleardata/ServiceWorkerCleanUp.sys.mjs87
-rw-r--r--toolkit/components/cleardata/SiteDataTestUtils.sys.mjs409
-rw-r--r--toolkit/components/cleardata/components.conf16
-rw-r--r--toolkit/components/cleardata/moz.build36
-rw-r--r--toolkit/components/cleardata/nsIClearDataService.idl336
-rw-r--r--toolkit/components/cleardata/tests/browser/browser.ini20
-rw-r--r--toolkit/components/cleardata/tests/browser/browser_auth_tokens.js108
-rw-r--r--toolkit/components/cleardata/tests/browser/browser_css_cache.js129
-rw-r--r--toolkit/components/cleardata/tests/browser/browser_image_cache.js219
-rw-r--r--toolkit/components/cleardata/tests/browser/browser_preflight_cache.js166
-rw-r--r--toolkit/components/cleardata/tests/browser/browser_quota.js318
-rw-r--r--toolkit/components/cleardata/tests/browser/browser_serviceworkers.js287
-rw-r--r--toolkit/components/cleardata/tests/browser/browser_sessionStorage.js235
-rw-r--r--toolkit/components/cleardata/tests/browser/file_cors_preflight.sjs40
-rw-r--r--toolkit/components/cleardata/tests/browser/file_css_cache.css3
-rw-r--r--toolkit/components/cleardata/tests/browser/file_css_cache.html6
-rw-r--r--toolkit/components/cleardata/tests/browser/file_image_cache.html7
-rw-r--r--toolkit/components/cleardata/tests/browser/file_image_cache.jpgbin0 -> 361 bytes
-rw-r--r--toolkit/components/cleardata/tests/browser/worker.js1
-rw-r--r--toolkit/components/cleardata/tests/marionette/manifest.ini2
-rw-r--r--toolkit/components/cleardata/tests/marionette/test_moved_origin_directory_cleanup.py134
-rw-r--r--toolkit/components/cleardata/tests/marionette/test_service_worker_at_shutdown.py62
-rw-r--r--toolkit/components/cleardata/tests/unit/head.js27
-rw-r--r--toolkit/components/cleardata/tests/unit/test_basic.js19
-rw-r--r--toolkit/components/cleardata/tests/unit/test_certs.js233
-rw-r--r--toolkit/components/cleardata/tests/unit/test_cookies.js393
-rw-r--r--toolkit/components/cleardata/tests/unit/test_downloads.js310
-rw-r--r--toolkit/components/cleardata/tests/unit/test_identity_credential_storage.js121
-rw-r--r--toolkit/components/cleardata/tests/unit/test_network_cache.js316
-rw-r--r--toolkit/components/cleardata/tests/unit/test_passwords.js89
-rw-r--r--toolkit/components/cleardata/tests/unit/test_permissions.js424
-rw-r--r--toolkit/components/cleardata/tests/unit/test_quota.js560
-rw-r--r--toolkit/components/cleardata/tests/unit/test_security_settings.js279
-rw-r--r--toolkit/components/cleardata/tests/unit/test_storage_permission.js398
-rw-r--r--toolkit/components/cleardata/tests/unit/xpcshell.ini19
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
new file mode 100644
index 0000000000..48c454d27c
--- /dev/null
+++ b/toolkit/components/cleardata/tests/browser/file_image_cache.jpg
Binary files differ
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]