summaryrefslogtreecommitdiffstats
path: root/toolkit/components/antitracking/bouncetrackingprotection
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/antitracking/bouncetrackingprotection
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/antitracking/bouncetrackingprotection')
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp596
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h84
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp841
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h222
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp76
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h72
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp612
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h156
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp190
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h110
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp107
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h28
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/components.conf19
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/moz.build51
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl46
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml20
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js73
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js121
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js89
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js63
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html59
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs19
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html11
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js275
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml7
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py133
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js307
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml8
28 files changed, 4395 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp
new file mode 100644
index 0000000000..e5d9ccfea9
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp
@@ -0,0 +1,596 @@
+/* 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 "BounceTrackingProtection.h"
+
+#include "BounceTrackingProtectionStorage.h"
+#include "BounceTrackingState.h"
+#include "BounceTrackingRecord.h"
+
+#include "BounceTrackingStateGlobal.h"
+#include "ErrorList.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/dom/Promise.h"
+#include "nsDebug.h"
+#include "nsHashPropertyBag.h"
+#include "nsIClearDataService.h"
+#include "nsIObserverService.h"
+#include "nsIPrincipal.h"
+#include "nsISupports.h"
+#include "nsServiceManagerUtils.h"
+#include "nscore.h"
+#include "prtime.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "xpcpublic.h"
+
+#define TEST_OBSERVER_MSG_RECORD_BOUNCES_FINISHED "test-record-bounces-finished"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(BounceTrackingProtection, nsIBounceTrackingProtection);
+
+LazyLogModule gBounceTrackingProtectionLog("BounceTrackingProtection");
+
+static StaticRefPtr<BounceTrackingProtection> sBounceTrackingProtection;
+
+static constexpr uint32_t TRACKER_PURGE_FLAGS =
+ nsIClearDataService::CLEAR_ALL_CACHES | nsIClearDataService::CLEAR_COOKIES |
+ nsIClearDataService::CLEAR_DOM_STORAGES |
+ nsIClearDataService::CLEAR_CLIENT_AUTH_REMEMBER_SERVICE |
+ nsIClearDataService::CLEAR_EME | nsIClearDataService::CLEAR_MEDIA_DEVICES |
+ nsIClearDataService::CLEAR_STORAGE_ACCESS |
+ nsIClearDataService::CLEAR_AUTH_TOKENS |
+ nsIClearDataService::CLEAR_AUTH_CACHE;
+
+// static
+already_AddRefed<BounceTrackingProtection>
+BounceTrackingProtection::GetSingleton() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) {
+ return nullptr;
+ }
+
+ if (!sBounceTrackingProtection) {
+ sBounceTrackingProtection = new BounceTrackingProtection();
+
+ RunOnShutdown([] { sBounceTrackingProtection = nullptr; });
+ }
+
+ return do_AddRef(sBounceTrackingProtection);
+}
+
+BounceTrackingProtection::BounceTrackingProtection() {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("constructor"));
+
+ mStorage = new BounceTrackingProtectionStorage();
+
+ nsresult rv = mStorage->Init();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Error,
+ ("storage init failed"));
+ return;
+ }
+
+ // Schedule timer for tracker purging. The timer interval is determined by
+ // pref.
+ uint32_t purgeTimerPeriod = StaticPrefs::
+ privacy_bounceTrackingProtection_bounceTrackingPurgeTimerPeriodSec();
+
+ // The pref can be set to 0 to disable interval purging.
+ if (purgeTimerPeriod == 0) {
+ return;
+ }
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("Scheduling mBounceTrackingPurgeTimer. Interval: %d seconds.",
+ purgeTimerPeriod));
+
+ rv = NS_NewTimerWithCallback(
+ getter_AddRefs(mBounceTrackingPurgeTimer),
+ [](auto) {
+ if (!sBounceTrackingProtection) {
+ return;
+ }
+ sBounceTrackingProtection->PurgeBounceTrackers()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [] {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: PurgeBounceTrackers finished after timer call.",
+ __FUNCTION__));
+ },
+ [] { NS_WARNING("RunPurgeBounceTrackers failed"); });
+ },
+ purgeTimerPeriod * PR_MSEC_PER_SEC, nsITimer::TYPE_REPEATING_SLACK,
+ "mBounceTrackingPurgeTimer");
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to schedule timer for RunPurgeBounceTrackers.");
+}
+
+nsresult BounceTrackingProtection::RecordStatefulBounces(
+ BounceTrackingState* aBounceTrackingState) {
+ NS_ENSURE_ARG_POINTER(aBounceTrackingState);
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: aBounceTrackingState: %s", __FUNCTION__,
+ aBounceTrackingState->Describe().get()));
+
+ // Assert: navigable’s bounce tracking record is not null.
+ BounceTrackingRecord* record =
+ aBounceTrackingState->GetBounceTrackingRecord();
+ NS_ENSURE_TRUE(record, NS_ERROR_FAILURE);
+
+ // Get the bounce tracker map and the user activation map.
+ RefPtr<BounceTrackingStateGlobal> globalState =
+ mStorage->GetOrCreateStateGlobal(aBounceTrackingState);
+ MOZ_ASSERT(globalState);
+
+ // For each host in navigable’s bounce tracking record's bounce set:
+ for (const nsACString& host : record->GetBounceHosts()) {
+ // If host equals navigable’s bounce tracking record's initial host,
+ // continue.
+ if (host == record->GetInitialHost()) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip host == initialHost: %s", __FUNCTION__,
+ PromiseFlatCString(host).get()));
+ continue;
+ }
+ // If host equals navigable’s bounce tracking record's final host, continue.
+ if (host == record->GetFinalHost()) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip host == finalHost: %s", __FUNCTION__,
+ PromiseFlatCString(host).get()));
+ continue;
+ }
+
+ // If user activation map contains host, continue.
+ if (globalState->HasUserActivation(host)) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip host with recent user activation: %s", __FUNCTION__,
+ PromiseFlatCString(host).get()));
+ continue;
+ }
+
+ // If stateful bounce tracking map contains host, continue.
+ if (globalState->HasBounceTracker(host)) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip already existing host: %s", __FUNCTION__,
+ PromiseFlatCString(host).get()));
+ continue;
+ }
+
+ // If navigable’s bounce tracking record's storage access set does not
+ // contain host, continue.
+ if (StaticPrefs::
+ privacy_bounceTrackingProtection_requireStatefulBounces() &&
+ !record->GetStorageAccessHosts().Contains(host)) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip host without storage access: %s", __FUNCTION__,
+ PromiseFlatCString(host).get()));
+ continue;
+ }
+
+ // Set stateful bounce tracking map[host] to topDocument’s relevant settings
+ // object's current wall time.
+ PRTime now = PR_Now();
+ MOZ_ASSERT(!globalState->HasBounceTracker(host));
+ nsresult rv = globalState->RecordBounceTracker(host, now);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ ("%s: Added candidate to mBounceTrackers: %s, Time: %" PRIu64,
+ __FUNCTION__, PromiseFlatCString(host).get(),
+ static_cast<uint64_t>(now)));
+ }
+
+ // Set navigable’s bounce tracking record to null.
+ aBounceTrackingState->ResetBounceTrackingRecord();
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Done, reset aBounceTrackingState: %s", __FUNCTION__,
+ aBounceTrackingState->Describe().get()));
+
+ // If running in test automation, dispatch an observer message indicating
+ // we're finished recording bounces.
+ if (StaticPrefs::privacy_bounceTrackingProtection_enableTestMode()) {
+ nsCOMPtr<nsIObserverService> obsSvc =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
+
+ RefPtr<nsHashPropertyBag> props = new nsHashPropertyBag();
+
+ nsresult rv = props->SetPropertyAsUint64(
+ u"browserId"_ns, aBounceTrackingState->GetBrowserId());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obsSvc->NotifyObservers(
+ ToSupports(props), TEST_OBSERVER_MSG_RECORD_BOUNCES_FINISHED, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult BounceTrackingProtection::RecordUserActivation(
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE);
+
+ nsAutoCString siteHost;
+ nsresult rv = aPrincipal->GetBaseDomain(siteHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ ("%s: siteHost: %s", __FUNCTION__, siteHost.get()));
+
+ RefPtr<BounceTrackingStateGlobal> globalState =
+ mStorage->GetOrCreateStateGlobal(aPrincipal);
+ MOZ_ASSERT(globalState);
+
+ return globalState->RecordUserActivation(siteHost, PR_Now());
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::TestGetBounceTrackerCandidateHosts(
+ JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx,
+ nsTArray<nsCString>& aCandidates) {
+ MOZ_ASSERT(aCx);
+
+ OriginAttributes oa;
+ if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa);
+ MOZ_ASSERT(globalState);
+
+ for (const nsACString& host : globalState->BounceTrackersMapRef().Keys()) {
+ aCandidates.AppendElement(host);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::TestGetUserActivationHosts(
+ JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx,
+ nsTArray<nsCString>& aHosts) {
+ MOZ_ASSERT(aCx);
+
+ OriginAttributes oa;
+ if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa);
+ MOZ_ASSERT(globalState);
+
+ for (const nsACString& host : globalState->UserActivationMapRef().Keys()) {
+ aHosts.AppendElement(host);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::ClearAll() {
+ BounceTrackingState::ResetAll();
+ return mStorage->Clear();
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::ClearBySiteHostAndOA(
+ const nsACString& aSiteHost, JS::Handle<JS::Value> aOriginAttributes,
+ JSContext* aCx) {
+ NS_ENSURE_ARG_POINTER(aCx);
+
+ OriginAttributes originAttributes;
+ if (!aOriginAttributes.isObject() ||
+ !originAttributes.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Reset per tab state for tabs matching the given OriginAttributes.
+ BounceTrackingState::ResetAllForOriginAttributes(originAttributes);
+
+ return mStorage->ClearBySiteHost(aSiteHost, &originAttributes);
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::ClearBySiteHost(const nsACString& aSiteHost) {
+ BounceTrackingState::ResetAll();
+
+ return mStorage->ClearBySiteHost(aSiteHost, nullptr);
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::ClearByTimeRange(PRTime aFrom, PRTime aTo) {
+ NS_ENSURE_TRUE(aFrom >= 0, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aFrom < aTo, NS_ERROR_INVALID_ARG);
+
+ // Clear all BounceTrackingState, we don't keep track of time ranges.
+ BounceTrackingState::ResetAll();
+
+ return mStorage->ClearByTimeRange(aFrom, aTo);
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::ClearByOriginAttributesPattern(
+ const nsAString& aPattern) {
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Reset all per-tab state matching the given OriginAttributesPattern.
+ BounceTrackingState::ResetAllForOriginAttributesPattern(pattern);
+
+ return mStorage->ClearByOriginAttributesPattern(pattern);
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::TestRunPurgeBounceTrackers(
+ JSContext* aCx, mozilla::dom::Promise** aPromise) {
+ NS_ENSURE_ARG_POINTER(aCx);
+ NS_ENSURE_ARG_POINTER(aPromise);
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (!globalObject) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ ErrorResult result;
+ RefPtr<dom::Promise> promise = dom::Promise::Create(globalObject, result);
+ if (result.Failed()) {
+ return result.StealNSResult();
+ }
+
+ // PurgeBounceTrackers returns a MozPromise, wrap it in a dom::Promise
+ // required for XPCOM.
+ PurgeBounceTrackers()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](const PurgeBounceTrackersMozPromise::ResolveValueType&
+ purgedSiteHosts) {
+ promise->MaybeResolve(purgedSiteHosts);
+ },
+ [promise] { promise->MaybeRejectWithUndefined(); });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::TestAddBounceTrackerCandidate(
+ JS::Handle<JS::Value> aOriginAttributes, const nsACString& aHost,
+ const PRTime aBounceTime, JSContext* aCx) {
+ MOZ_ASSERT(aCx);
+
+ OriginAttributes oa;
+ if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ BounceTrackingStateGlobal* stateGlobal = mStorage->GetOrCreateStateGlobal(oa);
+ MOZ_ASSERT(stateGlobal);
+
+ // Ensure aHost is lowercase to match nsIURI and nsIPrincipal.
+ nsAutoCString host(aHost);
+ ToLowerCase(host);
+
+ // Can not have a host in both maps.
+ nsresult rv = stateGlobal->TestRemoveUserActivation(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return stateGlobal->RecordBounceTracker(host, aBounceTime);
+}
+
+NS_IMETHODIMP
+BounceTrackingProtection::TestAddUserActivation(
+ JS::Handle<JS::Value> aOriginAttributes, const nsACString& aHost,
+ const PRTime aActivationTime, JSContext* aCx) {
+ MOZ_ASSERT(aCx);
+
+ OriginAttributes oa;
+ if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ BounceTrackingStateGlobal* stateGlobal = mStorage->GetOrCreateStateGlobal(oa);
+ MOZ_ASSERT(stateGlobal);
+
+ // Ensure aHost is lowercase to match nsIURI and nsIPrincipal.
+ nsAutoCString host(aHost);
+ ToLowerCase(host);
+
+ return stateGlobal->RecordUserActivation(host, aActivationTime);
+}
+
+RefPtr<BounceTrackingProtection::PurgeBounceTrackersMozPromise>
+BounceTrackingProtection::PurgeBounceTrackers() {
+ // Run the purging algorithm for all global state objects.
+ for (const auto& entry : mStorage->StateGlobalMapRef()) {
+ const OriginAttributes& originAttributes = entry.GetKey();
+ BounceTrackingStateGlobal* stateGlobal = entry.GetData();
+ MOZ_ASSERT(stateGlobal);
+
+ if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString oaSuffix;
+ originAttributes.CreateSuffix(oaSuffix);
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Running purge algorithm for OA: '%s'", __FUNCTION__,
+ oaSuffix.get()));
+ }
+
+ PurgeBounceTrackersForStateGlobal(stateGlobal, originAttributes);
+ }
+
+ // Wait for all data clearing operations to complete. mClearPromises contains
+ // one promise per host / clear task.
+ return ClearDataMozPromise::AllSettled(GetCurrentSerialEventTarget(),
+ mClearPromises)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&](ClearDataMozPromise::AllSettledPromiseType::ResolveOrRejectValue&&
+ aResults) {
+ MOZ_ASSERT(aResults.IsResolve(), "AllSettled never rejects");
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ ("%s: Done. Cleared %zu hosts.", __FUNCTION__,
+ aResults.ResolveValue().Length()));
+
+ nsTArray<nsCString> purgedSiteHosts;
+ // If any clear call failed reject.
+ for (auto& result : aResults.ResolveValue()) {
+ if (result.IsReject()) {
+ mClearPromises.Clear();
+ return PurgeBounceTrackersMozPromise::CreateAndReject(
+ NS_ERROR_FAILURE, __func__);
+ }
+ purgedSiteHosts.AppendElement(result.ResolveValue());
+ }
+
+ // No clearing errors, resolve.
+ mClearPromises.Clear();
+ return PurgeBounceTrackersMozPromise::CreateAndResolve(
+ std::move(purgedSiteHosts), __func__);
+ });
+}
+
+nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal(
+ BounceTrackingStateGlobal* aStateGlobal,
+ const OriginAttributes& aOriginAttributes) {
+ MOZ_ASSERT(aStateGlobal);
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: #mUserActivation: %d, #mBounceTrackers: %d", __FUNCTION__,
+ aStateGlobal->UserActivationMapRef().Count(),
+ aStateGlobal->BounceTrackersMapRef().Count()));
+
+ // Purge already in progress.
+ if (!mClearPromises.IsEmpty()) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip: Purge already in progress.", __FUNCTION__));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ const PRTime now = PR_Now();
+ // Convert the user activation lifetime into microseconds for calculation with
+ // PRTime values. The pref is a 32-bit value. Cast into 64-bit before
+ // multiplying so we get the correct result.
+ int64_t activationLifetimeUsec =
+ static_cast<int64_t>(
+ StaticPrefs::
+ privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec()) *
+ PR_USEC_PER_SEC;
+
+ // 1. Remove hosts from the user activation map whose user activation flag has
+ // expired.
+ nsresult rv =
+ aStateGlobal->ClearUserActivationBefore(now - activationLifetimeUsec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // 2. Go over bounce tracker candidate map and purge state.
+ rv = NS_OK;
+ nsCOMPtr<nsIClearDataService> clearDataService =
+ do_GetService("@mozilla.org/clear-data-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mClearPromises.Clear();
+ nsTArray<nsCString> purgedSiteHosts;
+
+ // Collect hosts to remove from the bounce trackers map. We can not remove
+ // them while iterating over the map.
+ nsTArray<nsCString> bounceTrackerCandidatesToRemove;
+
+ for (auto hostIter = aStateGlobal->BounceTrackersMapRef().ConstIter();
+ !hostIter.Done(); hostIter.Next()) {
+ const nsACString& host = hostIter.Key();
+ const PRTime& bounceTime = hostIter.Data();
+
+ // If bounceTime + bounce tracking grace period is after now, then continue.
+ // The host is still within the grace period and must not be purged.
+ if (bounceTime +
+ StaticPrefs::
+ privacy_bounceTrackingProtection_bounceTrackingGracePeriodSec() *
+ PR_USEC_PER_SEC >
+ now) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip host within bounce tracking grace period %s",
+ __FUNCTION__, PromiseFlatCString(host).get()));
+
+ continue;
+ }
+
+ // If there is a top-level traversable whose active document's origin's
+ // site's host equals host, then continue.
+ // TODO: Bug 1842047: Implement a more accurate check that calls into the
+ // browser implementations to determine whether the site is currently open
+ // on the top level.
+ bool hostIsActive;
+ rv = BounceTrackingState::HasBounceTrackingStateForSite(host, hostIsActive);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ hostIsActive = false;
+ }
+ if (hostIsActive) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip host which is active %s", __FUNCTION__,
+ PromiseFlatCString(host).get()));
+ continue;
+ }
+
+ // No exception above applies, clear state for the given host.
+
+ RefPtr<ClearDataMozPromise::Private> clearPromise =
+ new ClearDataMozPromise::Private(__func__);
+ RefPtr<ClearDataCallback> cb = new ClearDataCallback(clearPromise, host);
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Purge state for host: %s", __FUNCTION__,
+ PromiseFlatCString(host).get()));
+
+ // TODO: Bug 1842067: Clear by site + OA.
+ rv = clearDataService->DeleteDataFromBaseDomain(host, false,
+ TRACKER_PURGE_FLAGS, cb);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ clearPromise->Reject(0, __func__);
+ }
+
+ mClearPromises.AppendElement(clearPromise);
+
+ // Remove it from the bounce trackers map, it's about to be purged. If the
+ // clear call fails still remove it. We want to avoid an ever growing list
+ // of hosts in case of repeated failures.
+ bounceTrackerCandidatesToRemove.AppendElement(host);
+ }
+
+ // Remove hosts from the bounce trackers map which we executed purge calls
+ // for.
+ return aStateGlobal->RemoveBounceTrackers(bounceTrackerCandidatesToRemove);
+}
+
+// ClearDataCallback
+
+NS_IMPL_ISUPPORTS(BounceTrackingProtection::ClearDataCallback,
+ nsIClearDataCallback);
+
+// nsIClearDataCallback implementation
+NS_IMETHODIMP BounceTrackingProtection::ClearDataCallback::OnDataDeleted(
+ uint32_t aFailedFlags) {
+ if (aFailedFlags) {
+ mPromise->Reject(aFailedFlags, __func__);
+ } else {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ ("%s: Cleared %s", __FUNCTION__, mHost.get()));
+ mPromise->Resolve(std::move(mHost), __func__);
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h
new file mode 100644
index 0000000000..98c61504c0
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h
@@ -0,0 +1,84 @@
+/* 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/. */
+#ifndef mozilla_BounceTrackingProtection_h__
+#define mozilla_BounceTrackingProtection_h__
+
+#include "mozilla/Logging.h"
+#include "mozilla/MozPromise.h"
+#include "nsIBounceTrackingProtection.h"
+#include "nsIClearDataService.h"
+
+class nsIPrincipal;
+class nsITimer;
+
+namespace mozilla {
+
+class BounceTrackingState;
+class BounceTrackingStateGlobal;
+class BounceTrackingProtectionStorage;
+class OriginAttributes;
+
+extern LazyLogModule gBounceTrackingProtectionLog;
+
+class BounceTrackingProtection final : public nsIBounceTrackingProtection {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIBOUNCETRACKINGPROTECTION
+
+ public:
+ static already_AddRefed<BounceTrackingProtection> GetSingleton();
+
+ // This algorithm is called when detecting the end of an extended navigation.
+ // This could happen if a user-initiated navigation is detected in process
+ // navigation start for bounce tracking, or if the client bounce detection
+ // timer expires after process response received for bounce tracking without
+ // observing a client redirect.
+ nsresult RecordStatefulBounces(BounceTrackingState* aBounceTrackingState);
+
+ // Stores a user activation flag with a timestamp for the given principal.
+ nsresult RecordUserActivation(nsIPrincipal* aPrincipal);
+
+ private:
+ BounceTrackingProtection();
+ ~BounceTrackingProtection() = default;
+
+ // Timer which periodically runs PurgeBounceTrackers.
+ nsCOMPtr<nsITimer> mBounceTrackingPurgeTimer;
+
+ // Storage for user agent globals.
+ RefPtr<BounceTrackingProtectionStorage> mStorage;
+
+ // Clear state for classified bounce trackers. To be called on an interval.
+ using PurgeBounceTrackersMozPromise =
+ MozPromise<nsTArray<nsCString>, nsresult, true>;
+ RefPtr<PurgeBounceTrackersMozPromise> PurgeBounceTrackers();
+
+ nsresult PurgeBounceTrackersForStateGlobal(
+ BounceTrackingStateGlobal* aStateGlobal,
+ const OriginAttributes& aOriginAttributes);
+
+ // Pending clear operations are stored as ClearDataMozPromise, one per host.
+ using ClearDataMozPromise = MozPromise<nsCString, uint32_t, true>;
+ nsTArray<RefPtr<ClearDataMozPromise>> mClearPromises;
+
+ // Wraps nsIClearDataCallback in MozPromise.
+ class ClearDataCallback final : public nsIClearDataCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICLEARDATACALLBACK
+
+ explicit ClearDataCallback(ClearDataMozPromise::Private* aPromise,
+ const nsACString& aHost)
+ : mHost(aHost), mPromise(aPromise){};
+
+ private:
+ virtual ~ClearDataCallback() { mPromise->Reject(0, __func__); }
+
+ nsCString mHost;
+ RefPtr<ClearDataMozPromise::Private> mPromise;
+ };
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp
new file mode 100644
index 0000000000..bdd2c7dc18
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp
@@ -0,0 +1,841 @@
+/* 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 "BounceTrackingProtectionStorage.h"
+#include <cstdint>
+
+#include "BounceTrackingState.h"
+#include "BounceTrackingStateGlobal.h"
+#include "ErrorList.h"
+#include "MainThreadUtils.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageService.h"
+#include "mozIStorageStatement.h"
+#include "mozStorageCID.h"
+#include "mozilla/Components.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Services.h"
+#include "mozilla/ShutdownPhase.h"
+#include "nsCOMPtr.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIObserverService.h"
+#include "nsIPrincipal.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsStringFwd.h"
+#include "nsVariant.h"
+#include "nscore.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsCRT.h"
+
+#define BOUNCE_TRACKING_PROTECTION_DB_FILENAME \
+ "bounce-tracking-protection.sqlite"_ns
+#define SCHEMA_VERSION 1
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(BounceTrackingProtectionStorage, nsIAsyncShutdownBlocker,
+ nsIObserver);
+
+BounceTrackingStateGlobal*
+BounceTrackingProtectionStorage::GetOrCreateStateGlobal(
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+ return GetOrCreateStateGlobal(aPrincipal->OriginAttributesRef());
+}
+
+BounceTrackingStateGlobal*
+BounceTrackingProtectionStorage::GetOrCreateStateGlobal(
+ BounceTrackingState* aBounceTrackingState) {
+ MOZ_ASSERT(aBounceTrackingState);
+ return GetOrCreateStateGlobal(aBounceTrackingState->OriginAttributesRef());
+}
+
+BounceTrackingStateGlobal*
+BounceTrackingProtectionStorage::GetOrCreateStateGlobal(
+ const OriginAttributes& aOriginAttributes) {
+ return mStateGlobal.GetOrInsertNew(aOriginAttributes, this,
+ aOriginAttributes);
+}
+
+nsresult BounceTrackingProtectionStorage::ClearBySiteHost(
+ const nsACString& aSiteHost, OriginAttributes* aOriginAttributes) {
+ NS_ENSURE_TRUE(!aSiteHost.IsEmpty(), NS_ERROR_INVALID_ARG);
+
+ // If OriginAttributes are passed only clear the matching state global.
+ if (aOriginAttributes) {
+ RefPtr<BounceTrackingStateGlobal> stateGlobal =
+ mStateGlobal.Get(*aOriginAttributes);
+ if (stateGlobal) {
+ nsresult rv = stateGlobal->ClearSiteHost(aSiteHost, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ // Otherwise we need to clear the host across all state globals.
+ for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) {
+ BounceTrackingStateGlobal* stateGlobal = iter.Data();
+ MOZ_ASSERT(stateGlobal);
+ // Update in memory state. Skip storage so we can batch the writes later.
+ nsresult rv = stateGlobal->ClearSiteHost(aSiteHost, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Update the database.
+ // Private browsing data is not written to disk.
+ if (aOriginAttributes &&
+ aOriginAttributes->mPrivateBrowsingId !=
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) {
+ return NS_OK;
+ }
+ return DeleteDBEntries(aOriginAttributes, aSiteHost);
+}
+
+nsresult BounceTrackingProtectionStorage::ClearByTimeRange(PRTime aFrom,
+ PRTime aTo) {
+ for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) {
+ BounceTrackingStateGlobal* stateGlobal = iter.Data();
+ MOZ_ASSERT(stateGlobal);
+ // Update in memory state. Skip storage so we can batch the writes later.
+ nsresult rv =
+ stateGlobal->ClearByTimeRange(aFrom, Some(aTo), Nothing(), true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Update the database.
+ return DeleteDBEntriesInTimeRange(nullptr, aFrom, Some(aTo));
+}
+
+nsresult BounceTrackingProtectionStorage::ClearByOriginAttributesPattern(
+ const OriginAttributesPattern& aOriginAttributesPattern) {
+ // Clear in memory state.
+ for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) {
+ if (aOriginAttributesPattern.Matches(iter.Key())) {
+ iter.Remove();
+ }
+ }
+
+ // Update the database.
+ // Private browsing data is not written to disk.
+ if (aOriginAttributesPattern.mPrivateBrowsingId.WasPassed() &&
+ aOriginAttributesPattern.mPrivateBrowsingId.Value() !=
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) {
+ return NS_OK;
+ }
+ return DeleteDBEntriesByOriginAttributesPattern(aOriginAttributesPattern);
+}
+
+nsresult BounceTrackingProtectionStorage::UpdateDBEntry(
+ const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost,
+ EntryType aEntryType, PRTime aTimeStamp) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSiteHost.IsEmpty());
+ MOZ_ASSERT(aTimeStamp);
+
+ nsresult rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString originAttributeSuffix;
+ aOriginAttributes.CreateSuffix(originAttributeSuffix);
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: originAttributes: %s, siteHost=%s, entryType=%d, "
+ "timeStamp=%" PRId64,
+ __FUNCTION__, originAttributeSuffix.get(),
+ PromiseFlatCString(aSiteHost).get(),
+ static_cast<uint8_t>(aEntryType), aTimeStamp));
+ }
+
+ IncrementPendingWrites();
+
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+ nsCString siteHost(aSiteHost);
+
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction(
+ "BounceTrackingProtectionStorage::UpdateEntry",
+ [self, aOriginAttributes, siteHost, aEntryType, aTimeStamp]() {
+ nsresult rv =
+ UpsertData(self->mDatabaseConnection, aOriginAttributes,
+ siteHost, aEntryType, aTimeStamp);
+ self->DecrementPendingWrites();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+nsresult BounceTrackingProtectionStorage::DeleteDBEntries(
+ OriginAttributes* aOriginAttributes, const nsACString& aSiteHost) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aSiteHost.IsEmpty());
+ MOZ_ASSERT(!aOriginAttributes ||
+ aOriginAttributes->mPrivateBrowsingId ==
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID,
+ "Must not write private browsing data to the table.");
+
+ nsresult rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString originAttributeSuffix("*");
+ if (aOriginAttributes) {
+ aOriginAttributes->CreateSuffix(originAttributeSuffix);
+ }
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: originAttributes: %s, siteHost=%s", __FUNCTION__,
+ originAttributeSuffix.get(), PromiseFlatCString(aSiteHost).get()));
+ }
+
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+ nsCString siteHost(aSiteHost);
+ Maybe<OriginAttributes> originAttributes;
+ if (aOriginAttributes) {
+ originAttributes.emplace(*aOriginAttributes);
+ }
+
+ IncrementPendingWrites();
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("BounceTrackingProtectionStorage::DeleteEntry",
+ [self, originAttributes, siteHost]() {
+ nsresult rv =
+ DeleteData(self->mDatabaseConnection,
+ originAttributes, siteHost);
+ self->DecrementPendingWrites();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+nsresult BounceTrackingProtectionStorage::Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Clear in memory data.
+ mStateGlobal.Clear();
+
+ // Clear on disk data.
+ nsresult rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ IncrementPendingWrites();
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("BounceTrackingProtectionStorage::Clear",
+ [self]() {
+ nsresult rv =
+ ClearData(self->mDatabaseConnection);
+ self->DecrementPendingWrites();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return NS_OK;
+}
+
+nsresult BounceTrackingProtectionStorage::DeleteDBEntriesInTimeRange(
+ OriginAttributes* aOriginAttributes, PRTime aFrom, Maybe<PRTime> aTo,
+ Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG_MIN(aFrom, 0);
+ NS_ENSURE_TRUE(aTo.isNothing() || aTo.value() > aFrom, NS_ERROR_INVALID_ARG);
+
+ nsresult rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+ Maybe<OriginAttributes> originAttributes;
+ if (aOriginAttributes) {
+ originAttributes.emplace(*aOriginAttributes);
+ }
+
+ IncrementPendingWrites();
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction(
+ "BounceTrackingProtectionStorage::DeleteDBEntriesInTimeRange",
+ [self, originAttributes, aFrom, aTo, aEntryType]() {
+ nsresult rv =
+ DeleteDataInTimeRange(self->mDatabaseConnection,
+ originAttributes, aFrom, aTo, aEntryType);
+ self->DecrementPendingWrites();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return NS_OK;
+}
+
+nsresult
+BounceTrackingProtectionStorage::DeleteDBEntriesByOriginAttributesPattern(
+ const OriginAttributesPattern& aOriginAttributesPattern) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aOriginAttributesPattern.mPrivateBrowsingId.WasPassed() ||
+ aOriginAttributesPattern.mPrivateBrowsingId.Value() ==
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID,
+ "Must not clear private browsing data from the table.");
+
+ nsresult rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ IncrementPendingWrites();
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction(
+ "BounceTrackingProtectionStorage::"
+ "DeleteEntriesByOriginAttributesPattern",
+ [self, aOriginAttributesPattern]() {
+ nsresult rv = DeleteDataByOriginAttributesPattern(
+ self->mDatabaseConnection, aOriginAttributesPattern);
+ self->DecrementPendingWrites();
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ return NS_OK;
+}
+
+// nsIAsyncShutdownBlocker
+
+NS_IMETHODIMP BounceTrackingProtectionStorage::BlockShutdown(
+ nsIAsyncShutdownClient* aClient) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsresult rv = WaitForInitialization();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MonitorAutoLock lock(mMonitor);
+ mShuttingDown.Flip();
+
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction(
+ "BounceTrackingProtectionStorage::BlockShutdown",
+ [self]() {
+ MonitorAutoLock lock(self->mMonitor);
+
+ MOZ_ASSERT(self->mPendingWrites == 0);
+
+ if (self->mDatabaseConnection) {
+ Unused << self->mDatabaseConnection->Close();
+ self->mDatabaseConnection = nullptr;
+ }
+
+ self->mFinalized.Flip();
+ self->mMonitor.NotifyAll();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "BounceTrackingProtectionStorage::BlockShutdown "
+ "- mainthread callback",
+ [self]() { self->Finalize(); }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+nsresult BounceTrackingProtectionStorage::WaitForInitialization() {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "Must only wait for initialization in the main thread.");
+ MonitorAutoLock lock(mMonitor);
+ while (!mInitialized && !mErrored && !mShuttingDown) {
+ mMonitor.Wait();
+ }
+ if (mErrored) {
+ return NS_ERROR_FAILURE;
+ }
+ if (mShuttingDown) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return NS_OK;
+}
+
+void BounceTrackingProtectionStorage::Finalize() {
+ nsCOMPtr<nsIAsyncShutdownClient> asc = GetAsyncShutdownBarrier();
+ MOZ_ASSERT(asc);
+ DebugOnly<nsresult> rv = asc->RemoveBlocker(this);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+// nsIObserver
+
+NS_IMETHODIMP
+BounceTrackingProtectionStorage::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ AssertIsOnMainThread();
+ if (nsCRT::strcmp(aTopic, "last-pb-context-exited") != 0) {
+ return nsresult::NS_ERROR_FAILURE;
+ }
+
+ uint32_t removedCount = 0;
+ // Clear in-memory private browsing entries.
+ for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) {
+ BounceTrackingStateGlobal* stateGlobal = iter.Data();
+ MOZ_ASSERT(stateGlobal);
+ if (stateGlobal->IsPrivateBrowsing()) {
+ iter.Remove();
+ removedCount++;
+ }
+ }
+ MOZ_LOG(
+ gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: last-pb-context-exited: Removed %d private browsing state globals",
+ __FUNCTION__, removedCount));
+
+ return NS_OK;
+}
+
+// nsIAsyncShutdownBlocker
+
+already_AddRefed<nsIAsyncShutdownClient>
+BounceTrackingProtectionStorage::GetAsyncShutdownBarrier() const {
+ nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
+ MOZ_RELEASE_ASSERT(svc);
+
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(client));
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_RELEASE_ASSERT(client);
+
+ return client.forget();
+}
+
+NS_IMETHODIMP BounceTrackingProtectionStorage::GetState(nsIPropertyBag**) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP BounceTrackingProtectionStorage::GetName(nsAString& aName) {
+ aName.AssignLiteral("BounceTrackingProtectionStorage: Flushing to disk");
+ return NS_OK;
+}
+
+nsresult BounceTrackingProtectionStorage::Init() {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__));
+
+ // Init shouldn't be called if the feature is disabled.
+ NS_ENSURE_TRUE(
+ StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(),
+ NS_ERROR_FAILURE);
+
+ // Register a shutdown blocker so we can flush pending changes to disk before
+ // shutdown.
+ // Init may also be called during shutdown, e.g. because of clearing data
+ // during shutdown.
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier = GetAsyncShutdownBarrier();
+ NS_ENSURE_TRUE(shutdownBarrier, NS_ERROR_FAILURE);
+
+ bool closed;
+ nsresult rv = shutdownBarrier->GetIsClosed(&closed);
+ if (closed || NS_WARN_IF(NS_FAILED(rv))) {
+ MonitorAutoLock lock(mMonitor);
+ mShuttingDown.Flip();
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ rv = shutdownBarrier->AddBlocker(
+ this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Listen for last private browsing context exited message so we can clean up
+ // in memory state when the PBM session ends.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE);
+ rv = observerService->AddObserver(this, "last-pb-context-exited", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create the database file.
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mDatabaseFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mDatabaseFile->AppendNative(BOUNCE_TRACKING_PROTECTION_DB_FILENAME);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Init the database and import data.
+ NS_ENSURE_SUCCESS(
+ NS_CreateBackgroundTaskQueue("BounceTrackingProtectionStorage",
+ getter_AddRefs(mBackgroundThread)),
+ NS_ERROR_FAILURE);
+
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+
+ mBackgroundThread->Dispatch(
+ NS_NewRunnableFunction("BounceTrackingProtectionStorage::Init",
+ [self]() {
+ MonitorAutoLock lock(self->mMonitor);
+ nsresult rv = self->CreateDatabaseConnection();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ self->mErrored.Flip();
+ self->mMonitor.Notify();
+ return;
+ }
+
+ rv = self->LoadMemoryStateFromDisk();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ self->mErrored.Flip();
+ self->mMonitor.Notify();
+ return;
+ }
+
+ self->mInitialized.Flip();
+ self->mMonitor.Notify();
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ return NS_OK;
+}
+
+nsresult BounceTrackingProtectionStorage::CreateDatabaseConnection() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ NS_ENSURE_TRUE(mDatabaseFile, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<mozIStorageService> storage =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(storage, NS_ERROR_UNEXPECTED);
+
+ nsresult rv = storage->OpenDatabase(mDatabaseFile,
+ mozIStorageService::CONNECTION_DEFAULT,
+ getter_AddRefs(mDatabaseConnection));
+ if (rv == NS_ERROR_FILE_CORRUPTED) {
+ rv = mDatabaseFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = storage->OpenDatabase(mDatabaseFile,
+ mozIStorageService::CONNECTION_DEFAULT,
+ getter_AddRefs(mDatabaseConnection));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(mDatabaseConnection, NS_ERROR_UNEXPECTED);
+ bool ready = false;
+ mDatabaseConnection->GetConnectionReady(&ready);
+ NS_ENSURE_TRUE(ready, NS_ERROR_UNEXPECTED);
+
+ return EnsureTable();
+}
+
+nsresult BounceTrackingProtectionStorage::EnsureTable() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ NS_ENSURE_TRUE(mDatabaseConnection, NS_ERROR_UNEXPECTED);
+
+ nsresult rv = mDatabaseConnection->SetSchemaVersion(SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const constexpr auto createTableQuery =
+ "CREATE TABLE IF NOT EXISTS sites ("
+ "originAttributeSuffix TEXT NOT NULL,"
+ "siteHost TEXT NOT NULL, "
+ "entryType INTEGER NOT NULL, "
+ "timeStamp INTEGER NOT NULL, "
+ "PRIMARY KEY (originAttributeSuffix, siteHost)"
+ ");"_ns;
+
+ return mDatabaseConnection->ExecuteSimpleSQL(createTableQuery);
+}
+
+nsresult BounceTrackingProtectionStorage::LoadMemoryStateFromDisk() {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "Must not load the table from disk in the main thread.");
+
+ const constexpr auto selectAllQuery(
+ "SELECT originAttributeSuffix, siteHost, entryType, timeStamp FROM sites;"_ns);
+
+ nsCOMPtr<mozIStorageStatement> readStmt;
+ nsresult rv = mDatabaseConnection->CreateStatement(selectAllQuery,
+ getter_AddRefs(readStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult;
+ // Collect DB entries into an array to hand to the main thread later.
+ nsTArray<ImportEntry> importEntries;
+ while (NS_SUCCEEDED(readStmt->ExecuteStep(&hasResult)) && hasResult) {
+ nsAutoCString originAttributeSuffix, siteHost;
+ int64_t timeStamp;
+ int32_t typeInt;
+
+ rv = readStmt->GetUTF8String(0, originAttributeSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetUTF8String(1, siteHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetInt32(2, &typeInt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = readStmt->GetInt64(3, &timeStamp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Convert entryType field to enum.
+ BounceTrackingProtectionStorage::EntryType entryType =
+ static_cast<BounceTrackingProtectionStorage::EntryType>(typeInt);
+ // Check that the enum value is valid.
+ if (NS_WARN_IF(
+ entryType !=
+ BounceTrackingProtectionStorage::EntryType::BounceTracker &&
+ entryType !=
+ BounceTrackingProtectionStorage::EntryType::UserActivation)) {
+ continue;
+ }
+
+ OriginAttributes oa;
+ bool success = oa.PopulateFromSuffix(originAttributeSuffix);
+ if (NS_WARN_IF(!success)) {
+ continue;
+ }
+
+ // Collect entries to dispatch to main thread later.
+ importEntries.AppendElement(
+ ImportEntry{oa, siteHost, entryType, timeStamp});
+ }
+
+ // We can only access the state map on the main thread.
+ RefPtr<BounceTrackingProtectionStorage> self = this;
+ return NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "BounceTrackingProtectionStorage::LoadMemoryStateFromDisk",
+ [self, importEntries = std::move(importEntries)]() {
+ // For each entry get or create BounceTrackingStateGlobal and insert it
+ // into global state map.
+ for (const ImportEntry& entry : importEntries) {
+ RefPtr<BounceTrackingStateGlobal> stateGlobal =
+ self->GetOrCreateStateGlobal(entry.mOriginAttributes);
+ MOZ_ASSERT(stateGlobal);
+
+ nsresult rv;
+ if (entry.mEntryType ==
+ BounceTrackingProtectionStorage::EntryType::BounceTracker) {
+ rv = stateGlobal->RecordBounceTracker(entry.mSiteHost,
+ entry.mTimeStamp, true);
+ } else {
+ rv = stateGlobal->RecordUserActivation(entry.mSiteHost,
+ entry.mTimeStamp, true);
+ }
+ if (NS_WARN_IF(NS_FAILED(rv)) &&
+ MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString originAttributeSuffix;
+ entry.mOriginAttributes.CreateSuffix(originAttributeSuffix);
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Failed to load entry from disk: "
+ "originAttributeSuffix=%s, siteHost=%s, entryType=%d, "
+ "timeStamp=%" PRId64,
+ __FUNCTION__, originAttributeSuffix.get(),
+ PromiseFlatCString(entry.mSiteHost).get(),
+ static_cast<uint8_t>(entry.mEntryType), entry.mTimeStamp));
+ }
+ }
+ }));
+}
+
+void BounceTrackingProtectionStorage::IncrementPendingWrites() {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mPendingWrites < std::numeric_limits<uint32_t>::max());
+ mPendingWrites++;
+}
+
+void BounceTrackingProtectionStorage::DecrementPendingWrites() {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mPendingWrites > 0);
+ mPendingWrites--;
+}
+
+// static
+nsresult BounceTrackingProtectionStorage::UpsertData(
+ mozIStorageConnection* aDatabaseConnection,
+ const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost,
+ BounceTrackingProtectionStorage::EntryType aEntryType, PRTime aTimeStamp) {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "Must not write to the table from the main thread.");
+ MOZ_ASSERT(aDatabaseConnection);
+ MOZ_ASSERT(!aSiteHost.IsEmpty());
+ MOZ_ASSERT(aTimeStamp > 0);
+ MOZ_ASSERT(aOriginAttributes.mPrivateBrowsingId ==
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID,
+ "Must not write private browsing data to the table.");
+
+ auto constexpr upsertQuery =
+ "INSERT INTO sites (originAttributeSuffix, siteHost, entryType, "
+ "timeStamp)"
+ "VALUES (:originAttributeSuffix, :siteHost, :entryType, :timeStamp)"
+ "ON CONFLICT (originAttributeSuffix, siteHost)"
+ "DO UPDATE SET entryType = :entryType, timeStamp = :timeStamp;"_ns;
+
+ nsCOMPtr<mozIStorageStatement> upsertStmt;
+ nsresult rv = aDatabaseConnection->CreateStatement(
+ upsertQuery, getter_AddRefs(upsertStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Serialize OriginAttributes.
+ nsAutoCString originAttributeSuffix;
+ aOriginAttributes.CreateSuffix(originAttributeSuffix);
+
+ rv = upsertStmt->BindUTF8StringByName("originAttributeSuffix"_ns,
+ originAttributeSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = upsertStmt->BindUTF8StringByName("siteHost"_ns, aSiteHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = upsertStmt->BindInt32ByName("entryType"_ns,
+ static_cast<int32_t>(aEntryType));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = upsertStmt->BindInt64ByName("timeStamp"_ns, aTimeStamp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return upsertStmt->Execute();
+}
+
+// static
+nsresult BounceTrackingProtectionStorage::DeleteData(
+ mozIStorageConnection* aDatabaseConnection,
+ Maybe<OriginAttributes> aOriginAttributes, const nsACString& aSiteHost) {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "Must not write to the table from the main thread.");
+ MOZ_ASSERT(aDatabaseConnection);
+ MOZ_ASSERT(!aSiteHost.IsEmpty());
+ MOZ_ASSERT(aOriginAttributes.isNothing() ||
+ aOriginAttributes->mPrivateBrowsingId ==
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID);
+
+ nsAutoCString deleteQuery("DELETE FROM sites WHERE siteHost = :siteHost");
+
+ if (aOriginAttributes) {
+ deleteQuery.AppendLiteral(
+ " AND originAttributeSuffix = :originAttributeSuffix");
+ }
+
+ nsCOMPtr<mozIStorageStatement> upsertStmt;
+ nsresult rv = aDatabaseConnection->CreateStatement(
+ deleteQuery, getter_AddRefs(upsertStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = upsertStmt->BindUTF8StringByName("siteHost"_ns, aSiteHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aOriginAttributes) {
+ nsAutoCString originAttributeSuffix;
+ aOriginAttributes->CreateSuffix(originAttributeSuffix);
+ rv = upsertStmt->BindUTF8StringByName("originAttributeSuffix"_ns,
+ originAttributeSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return upsertStmt->Execute();
+}
+
+// static
+nsresult BounceTrackingProtectionStorage::DeleteDataInTimeRange(
+ mozIStorageConnection* aDatabaseConnection,
+ Maybe<OriginAttributes> aOriginAttributes, PRTime aFrom, Maybe<PRTime> aTo,
+ Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType) {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "Must not write to the table from the main thread.");
+ MOZ_ASSERT(aDatabaseConnection);
+ MOZ_ASSERT(aOriginAttributes.isNothing() ||
+ aOriginAttributes->mPrivateBrowsingId ==
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID);
+ MOZ_ASSERT(aFrom >= 0);
+ MOZ_ASSERT(aTo.isNothing() || aTo.value() > aFrom);
+
+ nsAutoCString deleteQuery(
+ "DELETE FROM sites "
+ "WHERE timeStamp >= :aFrom"_ns);
+
+ if (aTo.isSome()) {
+ deleteQuery.AppendLiteral(" AND timeStamp <= :aTo");
+ }
+
+ if (aOriginAttributes) {
+ deleteQuery.AppendLiteral(
+ " AND originAttributeSuffix = :originAttributeSuffix");
+ }
+
+ if (aEntryType.isSome()) {
+ deleteQuery.AppendLiteral(" AND entryType = :entryType");
+ }
+ deleteQuery.AppendLiteral(";");
+
+ nsCOMPtr<mozIStorageStatement> deleteStmt;
+ nsresult rv = aDatabaseConnection->CreateStatement(
+ deleteQuery, getter_AddRefs(deleteStmt));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = deleteStmt->BindInt64ByName("aFrom"_ns, aFrom);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aTo.isSome()) {
+ rv = deleteStmt->BindInt64ByName("aTo"_ns, aTo.value());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (aOriginAttributes) {
+ nsAutoCString originAttributeSuffix;
+ aOriginAttributes->CreateSuffix(originAttributeSuffix);
+ rv = deleteStmt->BindUTF8StringByName("originAttributeSuffix"_ns,
+ originAttributeSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (aEntryType.isSome()) {
+ rv = deleteStmt->BindInt32ByName("entryType"_ns,
+ static_cast<int32_t>(*aEntryType));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return deleteStmt->Execute();
+}
+
+nsresult BounceTrackingProtectionStorage::DeleteDataByOriginAttributesPattern(
+ mozIStorageConnection* aDatabaseConnection,
+ const OriginAttributesPattern& aOriginAttributesPattern) {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "Must not write to the table from the main thread.");
+ MOZ_ASSERT(aDatabaseConnection);
+
+ nsCOMPtr<mozIStorageFunction> patternMatchFunction(
+ new OriginAttrsPatternMatchOASuffixSQLFunction(aOriginAttributesPattern));
+
+ nsresult rv = aDatabaseConnection->CreateFunction(
+ "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX"_ns, 1, patternMatchFunction);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aDatabaseConnection->ExecuteSimpleSQL(
+ "DELETE FROM sites WHERE "
+ "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX(originAttributeSuffix);"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return aDatabaseConnection->RemoveFunction(
+ "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX"_ns);
+}
+
+// static
+nsresult BounceTrackingProtectionStorage::ClearData(
+ mozIStorageConnection* aDatabaseConnection) {
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "Must not write to the table from the main thread.");
+ NS_ENSURE_ARG_POINTER(aDatabaseConnection);
+ return aDatabaseConnection->ExecuteSimpleSQL("DELETE FROM sites;"_ns);
+}
+
+NS_IMPL_ISUPPORTS(OriginAttrsPatternMatchOASuffixSQLFunction,
+ mozIStorageFunction)
+
+NS_IMETHODIMP
+OriginAttrsPatternMatchOASuffixSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ nsresult rv;
+
+ nsAutoCString originAttributeSuffix;
+ rv = aFunctionArguments->GetUTF8String(0, originAttributeSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ OriginAttributes originAttributes;
+ bool parsedSuccessfully =
+ originAttributes.PopulateFromSuffix(originAttributeSuffix);
+ NS_ENSURE_TRUE(parsedSuccessfully, NS_ERROR_FAILURE);
+
+ bool result = mPattern.Matches(originAttributes);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsBool(result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h
new file mode 100644
index 0000000000..8d1d4e0417
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h
@@ -0,0 +1,222 @@
+/* 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/. */
+#ifndef mozilla_BounceTrackingProtectionStorage_h__
+#define mozilla_BounceTrackingProtectionStorage_h__
+
+#include "mozIStorageFunction.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/ThreadSafety.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIFile.h"
+#include "nsIObserver.h"
+#include "nsISupports.h"
+#include "nsTHashMap.h"
+#include "mozIStorageConnection.h"
+#include "mozilla/OriginAttributesHashKey.h"
+
+class nsIPrincipal;
+class mozIStorageConnection;
+
+namespace mozilla {
+
+class BounceTrackingStateGlobal;
+class BounceTrackingState;
+class OriginAttributes;
+
+extern LazyLogModule gBounceTrackingProtectionLog;
+
+class BounceTrackingProtectionStorage final : public nsIObserver,
+ public nsIAsyncShutdownBlocker,
+ public SupportsWeakPtr {
+ friend class BounceTrackingStateGlobal;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ public:
+ BounceTrackingProtectionStorage()
+ : mMonitor("mozilla::BounceTrackingProtectionStorage::mMonitor"),
+ mPendingWrites(0){};
+
+ // Initialises the storage including the on-disk database.
+ [[nodiscard]] nsresult Init();
+
+ // Getters for mStateGlobal.
+ BounceTrackingStateGlobal* GetOrCreateStateGlobal(
+ const OriginAttributes& aOriginAttributes);
+
+ BounceTrackingStateGlobal* GetOrCreateStateGlobal(nsIPrincipal* aPrincipal);
+
+ BounceTrackingStateGlobal* GetOrCreateStateGlobal(
+ BounceTrackingState* aBounceTrackingState);
+
+ using StateGlobalMap =
+ nsTHashMap<OriginAttributesHashKey, RefPtr<BounceTrackingStateGlobal>>;
+ // Provides a read-only reference to the state global map.
+ const StateGlobalMap& StateGlobalMapRef() { return mStateGlobal; }
+
+ // The enum values match the database type field. Updating them requires a DB
+ // migration.
+ enum class EntryType : uint8_t { BounceTracker = 0, UserActivation = 1 };
+
+ // Clear all state for a given site host. If aOriginAttributes is passed, only
+ // entries for that OA will be deleted.
+ [[nodiscard]] nsresult ClearBySiteHost(const nsACString& aSiteHost,
+ OriginAttributes* aOriginAttributes);
+
+ // Clear all state within a given time range.
+ [[nodiscard]] nsresult ClearByTimeRange(PRTime aFrom, PRTime aTo);
+
+ // Clear all state for a given OriginAttributesPattern.
+ [[nodiscard]] nsresult ClearByOriginAttributesPattern(
+ const OriginAttributesPattern& aOriginAttributesPattern);
+
+ // Clear all state.
+ [[nodiscard]] nsresult Clear();
+
+ private:
+ ~BounceTrackingProtectionStorage() = default;
+
+ // Worker thread. This should be a valid thread after Init() returns and be
+ // destroyed when we finalize
+ nsCOMPtr<nsISerialEventTarget> mBackgroundThread; // main thread only
+
+ // Database connections. Guaranteed to be non-null and working once
+ // initialized and not-yet finalized
+ RefPtr<mozIStorageConnection> mDatabaseConnection; // Worker thread only
+
+ // Wait (non-blocking) until the service is fully initialized. We may be
+ // waiting for that async work started by Init().
+ [[nodiscard]] nsresult WaitForInitialization();
+
+ // Called to indicate to the async shutdown service that we are all wrapped
+ // up. This also spins down the worker thread, since it is called after all
+ // disk database connections are closed.
+ void Finalize();
+
+ // Utility function to grab the correct barrier this service needs to shut
+ // down by
+ already_AddRefed<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() const;
+
+ // Initialises the DB connection on the worker thread.
+ [[nodiscard]] nsresult CreateDatabaseConnection();
+
+ // Creates amd initialises the database table if needed. Worker thread only.
+ [[nodiscard]] nsresult EnsureTable();
+
+ // Temporary data structure used to import db data into memory.
+ struct ImportEntry {
+ OriginAttributes mOriginAttributes;
+ nsCString mSiteHost;
+ EntryType mEntryType;
+ PRTime mTimeStamp;
+ };
+
+ // Imports state from the database on disk into memory.
+ [[nodiscard]] nsresult LoadMemoryStateFromDisk();
+
+ // Used to (thread-safely) track how many operations have been launched to the
+ // worker thread so that we can wait for it to hit zero before close the disk
+ // database connection
+ void IncrementPendingWrites();
+ void DecrementPendingWrites();
+
+ // Update or create database entry. Worker thread only.
+ [[nodiscard]] static nsresult UpsertData(
+ mozIStorageConnection* aDatabaseConnection,
+ const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost,
+ EntryType aEntryType, PRTime aTimeStamp);
+
+ // Delete database entries. Worker thread only.
+ [[nodiscard]] static nsresult DeleteData(
+ mozIStorageConnection* aDatabaseConnection,
+ Maybe<OriginAttributes> aOriginAttributes, const nsACString& aSiteHost);
+
+ // Delete all entries before a given time. Worker thread only.
+ // If aEntryType is passed only entries of that type will be deleted.
+ [[nodiscard]] static nsresult DeleteDataInTimeRange(
+ mozIStorageConnection* aDatabaseConnection,
+ Maybe<OriginAttributes> aOriginAttributes, PRTime aFrom,
+ Maybe<PRTime> aTo,
+ Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType = Nothing{});
+
+ // Delete all entries matching the given OriginAttributesPattern. Worker
+ // thread only.
+ [[nodiscard]] static nsresult DeleteDataByOriginAttributesPattern(
+ mozIStorageConnection* aDatabaseConnection,
+ const OriginAttributesPattern& aOriginAttributesPattern);
+
+ // Clear all entries from the database.
+ [[nodiscard]] static nsresult ClearData(
+ mozIStorageConnection* aDatabaseConnection);
+
+ // Service state management. We protect these variables with a monitor. This
+ // monitor is also used to signal the completion of initialization and
+ // finalization performed in the worker thread.
+ Monitor mMonitor;
+
+ FlippedOnce<false> mInitialized MOZ_GUARDED_BY(mMonitor);
+ FlippedOnce<false> mErrored MOZ_GUARDED_BY(mMonitor);
+ FlippedOnce<false> mShuttingDown MOZ_GUARDED_BY(mMonitor);
+ FlippedOnce<false> mFinalized MOZ_GUARDED_BY(mMonitor);
+ uint32_t mPendingWrites MOZ_GUARDED_BY(mMonitor);
+
+ // The database file handle. We can only create this in the main thread and
+ // need it in the worker to perform blocking disk IO. So we put it on this,
+ // since we pass this to the worker anyway
+ nsCOMPtr<nsIFile> mDatabaseFile;
+
+ // Map of origin attributes to global state object. This enables us to track
+ // bounce tracking state per OA, e.g. to separate private browsing from normal
+ // browsing.
+ StateGlobalMap mStateGlobal{};
+
+ // Helpers used to sync updates to BounceTrackingStateGlobal with the
+ // database.
+
+ // Updates or inserts a DB entry keyed by OA + site host.
+ [[nodiscard]] nsresult UpdateDBEntry(
+ const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost,
+ EntryType aEntryType, PRTime aTimeStamp);
+
+ // Deletes a DB entry keyed by OA + site host. If only aSiteHost is passed,
+ // all entries for that host will be deleted across OriginAttributes.
+ [[nodiscard]] nsresult DeleteDBEntries(OriginAttributes* aOriginAttributes,
+ const nsACString& aSiteHost);
+
+ // Delete all DB entries before a given time.
+ // If aEntryType is passed only entries of that type will be deleted.
+ [[nodiscard]] nsresult DeleteDBEntriesInTimeRange(
+ OriginAttributes* aOriginAttributes, PRTime aFrom,
+ Maybe<PRTime> aTo = Nothing{}, Maybe<EntryType> aEntryType = Nothing{});
+
+ // Deletes all DB entries matching the given OriginAttributesPattern.
+ [[nodiscard]] nsresult DeleteDBEntriesByOriginAttributesPattern(
+ const OriginAttributesPattern& aOriginAttributesPattern);
+};
+
+// A SQL function to match DB entries by OriginAttributesPattern.
+class OriginAttrsPatternMatchOASuffixSQLFunction final
+ : public mozIStorageFunction {
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+
+ explicit OriginAttrsPatternMatchOASuffixSQLFunction(
+ OriginAttributesPattern const& aPattern)
+ : mPattern(aPattern) {}
+ OriginAttrsPatternMatchOASuffixSQLFunction() = delete;
+
+ private:
+ ~OriginAttrsPatternMatchOASuffixSQLFunction() = default;
+
+ OriginAttributesPattern mPattern;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_BounceTrackingProtectionStorage_h__
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp
new file mode 100644
index 0000000000..14ee178ae2
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "BounceTrackingRecord.h"
+#include "mozilla/Logging.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+extern LazyLogModule gBounceTrackingProtectionLog;
+
+NS_IMPL_CYCLE_COLLECTION(BounceTrackingRecord);
+
+void BounceTrackingRecord::SetInitialHost(const nsACString& aHost) {
+ mInitialHost = aHost;
+}
+
+const nsACString& BounceTrackingRecord::GetInitialHost() {
+ return mInitialHost;
+}
+
+void BounceTrackingRecord::SetFinalHost(const nsACString& aHost) {
+ mFinalHost = aHost;
+}
+
+const nsACString& BounceTrackingRecord::GetFinalHost() { return mFinalHost; }
+
+void BounceTrackingRecord::AddBounceHost(const nsACString& aHost) {
+ mBounceHosts.Insert(aHost);
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: %s", __FUNCTION__, Describe().get()));
+}
+
+// static
+nsCString BounceTrackingRecord::DescribeSet(const nsTHashSet<nsCString>& set) {
+ nsAutoCString setStr;
+
+ setStr.AppendLiteral("[");
+
+ if (!set.IsEmpty()) {
+ for (const nsACString& host : set) {
+ setStr.Append(host);
+ setStr.AppendLiteral(",");
+ }
+ setStr.Truncate(setStr.Length() - 1);
+ }
+
+ setStr.AppendLiteral("]");
+
+ return std::move(setStr);
+}
+
+void BounceTrackingRecord::AddStorageAccessHost(const nsACString& aHost) {
+ mStorageAccessHosts.Insert(aHost);
+}
+
+const nsTHashSet<nsCString>& BounceTrackingRecord::GetBounceHosts() {
+ return mBounceHosts;
+}
+
+const nsTHashSet<nsCString>& BounceTrackingRecord::GetStorageAccessHosts() {
+ return mStorageAccessHosts;
+}
+
+nsCString BounceTrackingRecord::Describe() {
+ return nsPrintfCString(
+ "{mInitialHost:%s, mFinalHost:%s, mBounceHosts:%s, "
+ "mStorageAccessHosts:%s}",
+ mInitialHost.get(), mFinalHost.get(), DescribeSet(mBounceHosts).get(),
+ DescribeSet(mStorageAccessHosts).get());
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h
new file mode 100644
index 0000000000..d3e980d00b
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_BounceTrackingRecord_h
+#define mozilla_BounceTrackingRecord_h
+
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+namespace dom {
+class CanonicalBrowsingContext;
+}
+
+// Stores per-tab data relevant to bounce tracking protection for every extended
+// navigation.
+class BounceTrackingRecord final {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BounceTrackingRecord);
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BounceTrackingRecord);
+
+ void SetInitialHost(const nsACString& aHost);
+
+ const nsACString& GetInitialHost();
+
+ void SetFinalHost(const nsACString& aHost);
+
+ const nsACString& GetFinalHost();
+
+ void AddBounceHost(const nsACString& aHost);
+
+ void AddStorageAccessHost(const nsACString& aHost);
+
+ const nsTHashSet<nsCString>& GetBounceHosts();
+
+ const nsTHashSet<nsCString>& GetStorageAccessHosts();
+
+ // Create a string that describes this record. Used for logging.
+ nsCString Describe();
+
+ private:
+ ~BounceTrackingRecord() = default;
+
+ // A site's host. The initiator site of the current extended navigation.
+ nsAutoCString mInitialHost;
+
+ // A site's host or null. The destination of the current extended navigation.
+ // Updated after every document load.
+ nsAutoCString mFinalHost;
+
+ // A set of sites' hosts. All server-side and client-side redirects hit during
+ // this extended navigation.
+ nsTHashSet<nsCString> mBounceHosts;
+
+ // A set of sites' hosts. All sites which accessed storage during this
+ // extended navigation.
+ nsTHashSet<nsCString> mStorageAccessHosts;
+
+ // Create a comma-delimited string that describes a string set. Used for
+ // logging.
+ static nsCString DescribeSet(const nsTHashSet<nsCString>& set);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp
new file mode 100644
index 0000000000..c5abb8b8d7
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp
@@ -0,0 +1,612 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "BounceTrackingProtection.h"
+#include "BounceTrackingState.h"
+#include "BounceTrackingRecord.h"
+
+#include "BounceTrackingStorageObserver.h"
+#include "ErrorList.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/BrowsingContextWebProgress.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIBrowser.h"
+#include "nsIChannel.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIURI.h"
+#include "nsIWebProgressListener.h"
+#include "nsIPrincipal.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "nsTHashMap.h"
+#include "mozilla/dom/Element.h"
+
+namespace mozilla {
+
+// Global map: browserId -> BounceTrackingState
+static StaticAutoPtr<nsTHashMap<uint64_t, RefPtr<BounceTrackingState>>>
+ sBounceTrackingStates;
+
+static StaticRefPtr<BounceTrackingStorageObserver> sStorageObserver;
+
+NS_IMPL_ISUPPORTS(BounceTrackingState, nsIWebProgressListener,
+ nsISupportsWeakReference);
+
+BounceTrackingState::BounceTrackingState() {
+ MOZ_ASSERT(StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup());
+ mBounceTrackingProtection = BounceTrackingProtection::GetSingleton();
+};
+
+BounceTrackingState::~BounceTrackingState() {
+ if (sBounceTrackingStates) {
+ sBounceTrackingStates->Remove(mBrowserId);
+ }
+}
+
+// static
+already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate(
+ dom::BrowsingContextWebProgress* aWebProgress) {
+ MOZ_ASSERT(aWebProgress);
+
+ if (!ShouldCreateBounceTrackingStateForWebProgress(aWebProgress)) {
+ return nullptr;
+ }
+
+ // Create BounceTrackingState instance and populate the global
+ // BounceTrackingState map.
+ if (!sBounceTrackingStates) {
+ sBounceTrackingStates =
+ new nsTHashMap<nsUint64HashKey, RefPtr<BounceTrackingState>>();
+ ClearOnShutdown(&sBounceTrackingStates);
+ }
+
+ if (!sStorageObserver) {
+ sStorageObserver = new BounceTrackingStorageObserver();
+ ClearOnShutdown(&sStorageObserver);
+
+ DebugOnly<nsresult> rv = sStorageObserver->Init();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to init storage observer");
+ }
+
+ dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext();
+ if (!browsingContext) {
+ return nullptr;
+ }
+ uint64_t browserId = browsingContext->BrowserId();
+ bool createdNew;
+ RefPtr<BounceTrackingState> bounceTrackingState =
+ do_AddRef(sBounceTrackingStates->LookupOrInsertWith(browserId, [&] {
+ createdNew = true;
+ return do_AddRef(new BounceTrackingState());
+ }));
+
+ if (createdNew) {
+ nsresult rv = bounceTrackingState->Init(aWebProgress);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ }
+
+ return bounceTrackingState.forget();
+};
+
+// static
+void BounceTrackingState::ResetAll() { Reset(nullptr, nullptr); }
+
+// static
+void BounceTrackingState::ResetAllForOriginAttributes(
+ const OriginAttributes& aOriginAttributes) {
+ Reset(&aOriginAttributes, nullptr);
+}
+
+// static
+void BounceTrackingState::ResetAllForOriginAttributesPattern(
+ const OriginAttributesPattern& aPattern) {
+ Reset(nullptr, &aPattern);
+}
+
+nsresult BounceTrackingState::Init(
+ dom::BrowsingContextWebProgress* aWebProgress) {
+ NS_ENSURE_ARG_POINTER(aWebProgress);
+ NS_ENSURE_TRUE(
+ StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(),
+ NS_ERROR_NOT_AVAILABLE);
+ NS_ENSURE_TRUE(mBounceTrackingProtection, NS_ERROR_FAILURE);
+
+ // Store the browser ID so we can get the associated BC later without having
+ // to hold a reference to aWebProgress.
+ dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext();
+ NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE);
+ mBrowserId = browsingContext->BrowserId();
+ // Create a copy of the BC's OriginAttributes so we can use it later without
+ // having to hold a reference to the BC.
+ mOriginAttributes = browsingContext->OriginAttributesRef();
+ MOZ_ASSERT(mOriginAttributes.mPartitionKey.IsEmpty(),
+ "Top level BCs mus not have a partition key.");
+
+ // Add a listener for window load. See BounceTrackingState::OnStateChange for
+ // the listener code.
+ nsresult rv = aWebProgress->AddProgressListener(
+ this, nsIWebProgress::NOTIFY_STATE_WINDOW);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+void BounceTrackingState::ResetBounceTrackingRecord() {
+ mBounceTrackingRecord = nullptr;
+}
+
+BounceTrackingRecord* BounceTrackingState::GetBounceTrackingRecord() {
+ return mBounceTrackingRecord;
+}
+
+nsCString BounceTrackingState::Describe() {
+ nsAutoCString oaSuffix;
+ OriginAttributesRef().CreateSuffix(oaSuffix);
+
+ return nsPrintfCString(
+ "{ mBounceTrackingRecord: %s, mOriginAttributes: %s }",
+ mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() : "null",
+ oaSuffix.get());
+}
+
+// static
+void BounceTrackingState::Reset(const OriginAttributes* aOriginAttributes,
+ const OriginAttributesPattern* aPattern) {
+ if (aOriginAttributes || aPattern) {
+ MOZ_ASSERT((aOriginAttributes != nullptr) != (aPattern != nullptr),
+ "Must not pass both aOriginAttributes and aPattern.");
+ }
+
+ if (!sBounceTrackingStates) {
+ return;
+ }
+ for (const RefPtr<BounceTrackingState>& bounceTrackingState :
+ sBounceTrackingStates->Values()) {
+ if ((aOriginAttributes &&
+ *aOriginAttributes != bounceTrackingState->OriginAttributesRef()) ||
+ (aPattern &&
+ !aPattern->Matches(bounceTrackingState->OriginAttributesRef()))) {
+ continue;
+ }
+ if (bounceTrackingState->mClientBounceDetectionTimeout) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__));
+ bounceTrackingState->mClientBounceDetectionTimeout->Cancel();
+ bounceTrackingState->mClientBounceDetectionTimeout = nullptr;
+ }
+ bounceTrackingState->ResetBounceTrackingRecord();
+ }
+}
+
+// static
+bool BounceTrackingState::ShouldCreateBounceTrackingStateForWebProgress(
+ dom::BrowsingContextWebProgress* aWebProgress) {
+ NS_ENSURE_TRUE(aWebProgress, false);
+
+ // Feature is globally disabled.
+ if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) {
+ return false;
+ }
+
+ // Only keep track of top level content browsing contexts.
+ dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext();
+ if (!browsingContext || !browsingContext->IsTopContent()) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("%s: Skip non top-content.", __FUNCTION__));
+ return false;
+ }
+
+ return true;
+}
+
+// static
+nsresult BounceTrackingState::HasBounceTrackingStateForSite(
+ const nsACString& aSiteHost, bool& aResult) {
+ aResult = false;
+ NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_FAILURE);
+
+ if (!sBounceTrackingStates) {
+ return NS_OK;
+ }
+
+ // Iterate over all browsing contexts which have a bounce tracking state. Use
+ // the content principal base domain field to determine whether a BC has an
+ // active site that matches aSiteHost.
+ for (const RefPtr<BounceTrackingState>& state :
+ sBounceTrackingStates->Values()) {
+ RefPtr<dom::BrowsingContext> browsingContext =
+ state->CurrentBrowsingContext();
+
+ if (!browsingContext || browsingContext->IsDiscarded() ||
+ browsingContext->IsInBFCache()) {
+ continue;
+ }
+
+ RefPtr<dom::Element> embedderElement =
+ browsingContext->GetEmbedderElement();
+ if (!embedderElement) {
+ continue;
+ }
+
+ nsCOMPtr<nsIBrowser> browser = embedderElement->AsBrowser();
+ if (!browser) {
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> contentPrincipal;
+ nsresult rv =
+ browser->GetContentPrincipal(getter_AddRefs(contentPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ nsAutoCString baseDomain;
+ rv = contentPrincipal->GetBaseDomain(baseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ if (aSiteHost.Equals(baseDomain)) {
+ aResult = true;
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<dom::BrowsingContext>
+BounceTrackingState::CurrentBrowsingContext() {
+ MOZ_ASSERT(mBrowserId != 0);
+ return dom::BrowsingContext::GetCurrentTopByBrowserId(mBrowserId);
+}
+
+const OriginAttributes& BounceTrackingState::OriginAttributesRef() {
+ return mOriginAttributes;
+}
+
+nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) {
+ NS_ENSURE_ARG_POINTER(aChannel);
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__));
+
+ nsCOMPtr<nsILoadInfo> loadInfo;
+ nsresult rv = aChannel->GetLoadInfo(getter_AddRefs(loadInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Collect uri list including any redirects.
+ nsTArray<nsCString> siteList;
+
+ for (const nsCOMPtr<nsIRedirectHistoryEntry>& redirectHistoryEntry :
+ loadInfo->RedirectChain()) {
+ nsCOMPtr<nsIPrincipal> principal;
+ rv = redirectHistoryEntry->GetPrincipal(getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Filter out non-content principals.
+ if (!principal->GetIsContentPrincipal()) {
+ continue;
+ }
+
+ nsAutoCString baseDomain;
+ rv = principal->GetBaseDomain(baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ siteList.AppendElement(baseDomain);
+ }
+
+ // Add site via the current URI which is the end of the chain.
+ nsCOMPtr<nsIURI> channelURI;
+ rv = aChannel->GetURI(getter_AddRefs(channelURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIEffectiveTLDService> tldService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString siteHost;
+ rv = tldService->GetSchemelessSite(channelURI, siteHost);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to retrieve site for final channel URI.");
+ }
+
+ siteList.AppendElement(siteHost);
+
+ return OnResponseReceived(siteList);
+}
+
+// nsIWebProgressListener
+
+NS_IMETHODIMP
+BounceTrackingState::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aStateFlags,
+ nsresult aStatus) {
+ NS_ENSURE_ARG_POINTER(aWebProgress);
+ NS_ENSURE_ARG_POINTER(aRequest);
+
+ bool isTopLevel = false;
+ nsresult rv = aWebProgress->GetIsTopLevel(&isTopLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Filter for top level loads.
+ if (!isTopLevel) {
+ return NS_OK;
+ }
+
+ // Filter for window loads.
+ if (!(aStateFlags & nsIWebProgressListener::STATE_STOP) ||
+ !(aStateFlags & nsIWebProgressListener::STATE_IS_WINDOW)) {
+ return NS_OK;
+ }
+
+ // Get the document principal via the current window global.
+ dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext();
+ NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE);
+
+ dom::WindowGlobalParent* windowGlobalParent =
+ browsingContext->Canonical()->GetCurrentWindowGlobal();
+ NS_ENSURE_TRUE(windowGlobalParent, NS_ERROR_FAILURE);
+
+ return OnDocumentLoaded(windowGlobalParent->DocumentPrincipal());
+}
+
+NS_IMETHODIMP
+BounceTrackingState::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BounceTrackingState::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* aLocation,
+ uint32_t aFlags) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BounceTrackingState::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BounceTrackingState::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+BounceTrackingState::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+nsresult BounceTrackingState::OnStartNavigation(
+ nsIPrincipal* aTriggeringPrincipal,
+ const bool aHasValidUserGestureActivation) {
+ NS_ENSURE_ARG_POINTER(aTriggeringPrincipal);
+
+ // Logging
+ if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString origin;
+ nsresult rv = aTriggeringPrincipal->GetOrigin(origin);
+ if (NS_FAILED(rv)) {
+ origin = "err";
+ }
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__,
+ origin.get(),
+ mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get()
+ : "null"));
+ }
+
+ // Remove any queued global tasks to record stateful bounces for bounce
+ // tracking from the networking task source.
+ if (mClientBounceDetectionTimeout) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__));
+ mClientBounceDetectionTimeout->Cancel();
+ mClientBounceDetectionTimeout = nullptr;
+ }
+
+ // Obtain the (schemeless) site to keep track of bounces.
+ nsAutoCString siteHost;
+
+ // If origin is an opaque origin, set initialHost to empty host. Strictly
+ // speaking we only need to check IsNullPrincipal, but we're generally only
+ // interested in content principals. Other principal types are not considered
+ // to be trackers.
+ if (!aTriggeringPrincipal->GetIsContentPrincipal()) {
+ siteHost = "";
+ }
+
+ // obtain site
+ nsresult rv = aTriggeringPrincipal->GetBaseDomain(siteHost);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ siteHost = "";
+ }
+
+ // If navigable’s bounce tracking record is null: Set navigable’s bounce
+ // tracking record to a new bounce tracking record with initial host set to
+ // initialHost.
+ if (!mBounceTrackingRecord) {
+ mBounceTrackingRecord = new BounceTrackingRecord();
+ mBounceTrackingRecord->SetInitialHost(siteHost);
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: new BounceTrackingRecord(): %s", __FUNCTION__,
+ mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get()
+ : "null"));
+
+ return NS_OK;
+ }
+
+ // If sourceSnapshotParams’s has transient activation is true: The user
+ // activation ends the extended navigation. Process the bounce candidates.
+ // Also treat system principal navigation as having user interaction
+ bool hasUserActivation = aHasValidUserGestureActivation ||
+ aTriggeringPrincipal->IsSystemPrincipal();
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: site: %s, hasUserActivation? %d", __FUNCTION__, siteHost.get(),
+ hasUserActivation));
+ if (hasUserActivation) {
+ rv = mBounceTrackingProtection->RecordStatefulBounces(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_ASSERT(!mBounceTrackingRecord);
+ mBounceTrackingRecord = new BounceTrackingRecord();
+ mBounceTrackingRecord->SetInitialHost(siteHost);
+
+ return NS_OK;
+ }
+
+ // There is no transient user activation. Add host as a bounce candidate.
+ mBounceTrackingRecord->AddBounceHost(siteHost);
+
+ return NS_OK;
+}
+
+// Private
+
+nsresult BounceTrackingState::OnResponseReceived(
+ const nsTArray<nsCString>& aSiteList) {
+ NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE);
+
+ // Logging
+ if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString siteListStr;
+
+ for (const nsACString& site : aSiteList) {
+ siteListStr.Append(site);
+ siteListStr.AppendLiteral(", ");
+ }
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: #%zu siteList: %s", __FUNCTION__, siteListStr.Length(),
+ siteListStr.get()));
+ }
+
+ // Check if there is still an active timeout. This shouldn't happen since
+ // OnStartNavigation already cancels it.
+ if (NS_WARN_IF(mClientBounceDetectionTimeout)) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__));
+ mClientBounceDetectionTimeout->Cancel();
+ mClientBounceDetectionTimeout = nullptr;
+ }
+
+ // Run steps after a timeout: queue a global task on the networking task
+ // source with global to record stateful bounces for bounce.
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Scheduling mClientBounceDetectionTimeout", __FUNCTION__));
+
+ // Use a weak reference to this to avoid keeping the object alive if the tab
+ // is closed during the timeout.
+ WeakPtr<BounceTrackingState> thisWeak = this;
+ nsresult rv = NS_NewTimerWithCallback(
+ getter_AddRefs(mClientBounceDetectionTimeout),
+ [thisWeak](auto) {
+ if (!thisWeak) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: !thisWeak", __FUNCTION__));
+ return;
+ }
+ MOZ_LOG(
+ gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Calling RecordStatefulBounces after timeout.", __FUNCTION__));
+
+ BounceTrackingState* bounceTrackingState = thisWeak;
+ bounceTrackingState->mBounceTrackingProtection->RecordStatefulBounces(
+ bounceTrackingState);
+
+ bounceTrackingState->mClientBounceDetectionTimeout = nullptr;
+ },
+ StaticPrefs::
+ privacy_bounceTrackingProtection_clientBounceDetectionTimerPeriodMS(),
+ nsITimer::TYPE_ONE_SHOT, "mClientBounceDetectionTimeout");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // For each URL in URLs: Insert host to the navigable’s bounce tracking
+ // record's bounce set.
+ for (const nsACString& site : aSiteList) {
+ mBounceTrackingRecord->AddBounceHost(site);
+ }
+
+ return NS_OK;
+}
+
+nsresult BounceTrackingState::OnDocumentLoaded(
+ nsIPrincipal* aDocumentPrincipal) {
+ NS_ENSURE_ARG_POINTER(aDocumentPrincipal);
+
+ // Assert: navigable’s bounce tracking record is not null.
+ NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE);
+
+ // Logging
+ if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString origin;
+ nsresult rv = aDocumentPrincipal->GetOrigin(origin);
+ if (NS_FAILED(rv)) {
+ origin = "err";
+ }
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__,
+ origin.get(),
+ mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get()
+ : "null"));
+ }
+
+ nsAutoCString siteHost;
+ if (!aDocumentPrincipal->GetIsContentPrincipal()) {
+ siteHost = "";
+ } else {
+ nsresult rv = aDocumentPrincipal->GetBaseDomain(siteHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Set the navigable’s bounce tracking record's final host to the host of
+ // finalSite.
+ mBounceTrackingRecord->SetFinalHost(siteHost);
+
+ return NS_OK;
+}
+
+nsresult BounceTrackingState::OnCookieWrite(const nsACString& aSiteHost) {
+ NS_ENSURE_TRUE(!aSiteHost.IsEmpty(), NS_ERROR_FAILURE);
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("%s: OnCookieWrite: %s.", __FUNCTION__,
+ PromiseFlatCString(aSiteHost).get()));
+
+ if (!mBounceTrackingRecord) {
+ return NS_OK;
+ }
+
+ mBounceTrackingRecord->AddStorageAccessHost(aSiteHost);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h
new file mode 100644
index 0000000000..70deee5abe
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_BounceTrackingState_h
+#define mozilla_BounceTrackingState_h
+
+#include "mozilla/WeakPtr.h"
+#include "mozilla/OriginAttributes.h"
+#include "nsIPrincipal.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsStringFwd.h"
+#include "nsIWebProgressListener.h"
+#include "nsWeakReference.h"
+
+class nsIChannel;
+class nsITimer;
+class nsIPrincipal;
+
+namespace mozilla {
+
+class BounceTrackingProtection;
+class BounceTrackingRecord;
+
+namespace dom {
+class CanonicalBrowsingContext;
+class BrowsingContext;
+class BrowsingContextWebProgress;
+} // namespace dom
+
+/**
+ * This class manages the bounce tracking state for a given tab. It is attached
+ * to top-level CanonicalBrowsingContexts.
+ */
+class BounceTrackingState : public nsIWebProgressListener,
+ public nsSupportsWeakReference,
+ public SupportsWeakPtr {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ // Gets or creates an existing BrowsingContextState keyed by browserId. May
+ // return nullptr if the given web progress / browsing context is not suitable
+ // (see ShouldCreateBounceTrackingStateForWebProgress).
+ static already_AddRefed<BounceTrackingState> GetOrCreate(
+ dom::BrowsingContextWebProgress* aWebProgress);
+
+ // Reset state for all BounceTrackingState instances this includes resetting
+ // BounceTrackingRecords and cancelling any running timers.
+ static void ResetAll();
+ static void ResetAllForOriginAttributes(
+ const OriginAttributes& aOriginAttributes);
+ static void ResetAllForOriginAttributesPattern(
+ const OriginAttributesPattern& aPattern);
+
+ BounceTrackingRecord* GetBounceTrackingRecord();
+
+ void ResetBounceTrackingRecord();
+
+ // Callback for when we received a response from the server and are about to
+ // create a document for the response. Calls into
+ // BounceTrackingState::OnResponseReceived.
+ nsresult OnDocumentStartRequest(nsIChannel* aChannel);
+
+ // At the start of a navigation, either initialize a new bounce tracking
+ // record, or append a client-side redirect to the current bounce tracking
+ // record.
+ // Should only be called for top level content navigations.
+ nsresult OnStartNavigation(nsIPrincipal* aTriggeringPrincipal,
+ const bool aHasValidUserGestureActivation);
+
+ // Record sites which have written cookies in the current extended
+ // navigation.
+ nsresult OnCookieWrite(const nsACString& aSiteHost);
+
+ // Whether the given BrowsingContext should hold a BounceTrackingState
+ // instance to monitor bounce tracking navigations.
+ static bool ShouldCreateBounceTrackingStateForBC(
+ dom::CanonicalBrowsingContext* aBrowsingContext);
+
+ // Check if there is a BounceTrackingState which current browsing context is
+ // associated with aSiteHost.
+ // This is an approximation for checking if a given site is currently loaded
+ // in the top level context, e.g. in a tab. See Bug 1842047 for adding a more
+ // accurate check that calls into the browser implementations.
+ static nsresult HasBounceTrackingStateForSite(const nsACString& aSiteHost,
+ bool& aResult);
+
+ // Get the currently associated BrowsingContext. Returns nullptr if it has not
+ // been attached yet.
+ already_AddRefed<dom::BrowsingContext> CurrentBrowsingContext();
+
+ uint64_t GetBrowserId() { return mBrowserId; }
+
+ const OriginAttributes& OriginAttributesRef();
+
+ // Create a string that describes this object. Used for logging.
+ nsCString Describe();
+
+ private:
+ explicit BounceTrackingState();
+ virtual ~BounceTrackingState();
+
+ uint64_t mBrowserId{};
+
+ // OriginAttributes associated with the browser this state is attached to.
+ OriginAttributes mOriginAttributes;
+
+ // Reference to the BounceTrackingProtection singleton.
+ RefPtr<BounceTrackingProtection> mBounceTrackingProtection;
+
+ // Record to keep track of extended navigation data. Reset on extended
+ // navigation end.
+ RefPtr<BounceTrackingRecord> mBounceTrackingRecord;
+
+ // Timer to wait to wait for a client redirect after a navigation ends.
+ RefPtr<nsITimer> mClientBounceDetectionTimeout;
+
+ // Reset state for all BounceTrackingState instances this includes resetting
+ // BounceTrackingRecords and cancelling any running timers.
+ // Optionally filter by OriginAttributes or OriginAttributesPattern.
+ static void Reset(const OriginAttributes* aOriginAttributes,
+ const OriginAttributesPattern* aPattern);
+
+ // Whether the given web progress should hold a BounceTrackingState
+ // instance to monitor bounce tracking navigations.
+ static bool ShouldCreateBounceTrackingStateForWebProgress(
+ dom::BrowsingContextWebProgress* aWebProgress);
+
+ // Init to be called after creation, attaches nsIWebProgressListener.
+ nsresult Init(dom::BrowsingContextWebProgress* aWebProgress);
+
+ // When the response is received at the end of a navigation, fill the
+ // bounce set.
+ nsresult OnResponseReceived(const nsTArray<nsCString>& aSiteList);
+
+ // When the document is loaded at the end of a navigation, update the
+ // final host.
+ nsresult OnDocumentLoaded(nsIPrincipal* aDocumentPrincipal);
+
+ // TODO: Bug 1839918: Detection of stateful bounces.
+
+ // Record sites which have accessed storage in the current extended
+ // navigation.
+ nsresult OnStorageAccess();
+
+ // Record sites which have activated service workers in the current
+ // extended navigation.
+ nsresult OnServiceWorkerActivation();
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp
new file mode 100644
index 0000000000..3481753431
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp
@@ -0,0 +1,190 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "BounceTrackingStateGlobal.h"
+#include "BounceTrackingProtectionStorage.h"
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Logging.h"
+#include "nsIPrincipal.h"
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTION(BounceTrackingStateGlobal);
+
+extern LazyLogModule gBounceTrackingProtectionLog;
+
+BounceTrackingStateGlobal::BounceTrackingStateGlobal(
+ BounceTrackingProtectionStorage* aStorage, const OriginAttributes& aAttrs)
+ : mStorage(aStorage), mOriginAttributes(aAttrs) {
+ MOZ_ASSERT(aStorage);
+}
+
+bool BounceTrackingStateGlobal::HasUserActivation(
+ const nsACString& aSiteHost) const {
+ return mUserActivation.Contains(aSiteHost);
+}
+
+nsresult BounceTrackingStateGlobal::RecordUserActivation(
+ const nsACString& aSiteHost, PRTime aTime, bool aSkipStorage) {
+ NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aTime > 0, NS_ERROR_INVALID_ARG);
+
+ // A site must only be in one of the maps at a time.
+ bool hasRemoved = mBounceTrackers.Remove(aSiteHost);
+
+ if (hasRemoved) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Removed bounce tracking candidate due to user activation: %s",
+ __FUNCTION__, PromiseFlatCString(aSiteHost).get()));
+ }
+
+ mUserActivation.InsertOrUpdate(aSiteHost, aTime);
+
+ if (aSkipStorage || !ShouldPersistToDisk()) {
+ return NS_OK;
+ }
+
+ // Write the change to storage.
+ NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE);
+ return mStorage->UpdateDBEntry(
+ mOriginAttributes, aSiteHost,
+ BounceTrackingProtectionStorage::EntryType::UserActivation, aTime);
+}
+
+nsresult BounceTrackingStateGlobal::TestRemoveUserActivation(
+ const nsACString& aSiteHost) {
+ bool hasRemoved = mUserActivation.Remove(aSiteHost);
+
+ // Avoid potentially removing a bounce tracking entry if there is no user
+ // activation entry.
+ if (!hasRemoved) {
+ return NS_OK;
+ }
+
+ if (!ShouldPersistToDisk()) {
+ return NS_OK;
+ }
+
+ // Write the change to storage.
+ NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE);
+ return mStorage->DeleteDBEntries(&mOriginAttributes, aSiteHost);
+}
+
+nsresult BounceTrackingStateGlobal::ClearUserActivationBefore(PRTime aTime) {
+ return ClearByTimeRange(
+ 0, Some(aTime),
+ Some(BounceTrackingProtectionStorage::EntryType::UserActivation));
+}
+
+nsresult BounceTrackingStateGlobal::ClearSiteHost(const nsACString& aSiteHost,
+ bool aSkipStorage) {
+ NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG);
+
+ bool removedUserActivation = mUserActivation.Remove(aSiteHost);
+ bool removedBounceTracker = mBounceTrackers.Remove(aSiteHost);
+ if (removedUserActivation || removedBounceTracker) {
+ MOZ_ASSERT(removedUserActivation != removedBounceTracker,
+ "A site must only be in one of the maps at a time.");
+ }
+
+ if (aSkipStorage || !ShouldPersistToDisk()) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE);
+ return mStorage->DeleteDBEntries(&mOriginAttributes, aSiteHost);
+}
+
+nsresult BounceTrackingStateGlobal::ClearByTimeRange(
+ PRTime aFrom, Maybe<PRTime> aTo,
+ Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType,
+ bool aSkipStorage) {
+ NS_ENSURE_ARG_MIN(aFrom, 0);
+ NS_ENSURE_TRUE(!aTo || aTo.value() > aFrom, NS_ERROR_INVALID_ARG);
+
+ // Clear in memory user activation data.
+ if (aEntryType.isNothing() ||
+ aEntryType.value() ==
+ BounceTrackingProtectionStorage::EntryType::UserActivation) {
+ for (auto iter = mUserActivation.Iter(); !iter.Done(); iter.Next()) {
+ if (iter.Data() >= aFrom &&
+ (aTo.isNothing() || iter.Data() <= aTo.value())) {
+ iter.Remove();
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Remove user activation for %s", __FUNCTION__,
+ PromiseFlatCString(iter.Key()).get()));
+ }
+ }
+ }
+
+ // Clear in memory bounce tracker data.
+ if (aEntryType.isNothing() ||
+ aEntryType.value() ==
+ BounceTrackingProtectionStorage::EntryType::BounceTracker) {
+ for (auto iter = mBounceTrackers.Iter(); !iter.Done(); iter.Next()) {
+ if (iter.Data() >= aFrom &&
+ (aTo.isNothing() || iter.Data() <= aTo.value())) {
+ iter.Remove();
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Remove bouncer tracker for %s", __FUNCTION__,
+ PromiseFlatCString(iter.Key()).get()));
+ }
+ }
+ }
+
+ if (aSkipStorage || !ShouldPersistToDisk()) {
+ return NS_OK;
+ }
+
+ // Write the change to storage.
+ NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE);
+ return mStorage->DeleteDBEntriesInTimeRange(&mOriginAttributes, aFrom, aTo,
+ aEntryType);
+}
+
+bool BounceTrackingStateGlobal::HasBounceTracker(
+ const nsACString& aSiteHost) const {
+ return mBounceTrackers.Contains(aSiteHost);
+}
+
+nsresult BounceTrackingStateGlobal::RecordBounceTracker(
+ const nsACString& aSiteHost, PRTime aTime, bool aSkipStorage) {
+ NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(aTime > 0, NS_ERROR_INVALID_ARG);
+
+ // Can not record a bounce tracker if the site has a user activation.
+ NS_ENSURE_TRUE(!mUserActivation.Contains(aSiteHost), NS_ERROR_FAILURE);
+ mBounceTrackers.InsertOrUpdate(aSiteHost, aTime);
+
+ if (aSkipStorage || !ShouldPersistToDisk()) {
+ return NS_OK;
+ }
+
+ // Write the change to storage.
+ NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE);
+ return mStorage->UpdateDBEntry(
+ mOriginAttributes, aSiteHost,
+ BounceTrackingProtectionStorage::EntryType::BounceTracker, aTime);
+}
+
+nsresult BounceTrackingStateGlobal::RemoveBounceTrackers(
+ const nsTArray<nsCString>& aSiteHosts) {
+ for (const nsCString& siteHost : aSiteHosts) {
+ mBounceTrackers.Remove(siteHost);
+
+ // TODO: Create a bulk delete query.
+ if (ShouldPersistToDisk()) {
+ NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE);
+ nsresult rv = mStorage->DeleteDBEntries(&mOriginAttributes, siteHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h
new file mode 100644
index 0000000000..6680ceae6f
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_BounceTrackingStateGlobal_h
+#define mozilla_BounceTrackingStateGlobal_h
+
+#include "BounceTrackingProtectionStorage.h"
+#include "mozilla/WeakPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashMap.h"
+#include "nsISupports.h"
+
+namespace mozilla {
+
+/**
+ * This class holds the global state maps which are used to keep track of
+ * potential bounce trackers and user activations.
+ * @see BounceTrackingState for the per browser / tab state.
+ *
+ * Updates to the state maps are persisted to storage.
+ */
+class BounceTrackingStateGlobal final {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BounceTrackingStateGlobal);
+ NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BounceTrackingStateGlobal);
+
+ BounceTrackingStateGlobal(BounceTrackingProtectionStorage* aStorage,
+ const OriginAttributes& aAttrs);
+
+ bool IsPrivateBrowsing() const {
+ return mOriginAttributes.mPrivateBrowsingId !=
+ nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID;
+ }
+
+ bool ShouldPersistToDisk() const { return !IsPrivateBrowsing(); }
+
+ bool HasUserActivation(const nsACString& aSiteHost) const;
+
+ // Store a user interaction flag for the given host. This will remove the
+ // host from the bounce tracker map if it exists.
+ [[nodiscard]] nsresult RecordUserActivation(const nsACString& aSiteHost,
+ PRTime aTime,
+ bool aSkipStorage = false);
+
+ // Test-only method to clear a user activation flag.
+ [[nodiscard]] nsresult TestRemoveUserActivation(const nsACString& aSiteHost);
+
+ // Clear any user interactions that happened before aTime.
+ [[nodiscard]] nsresult ClearUserActivationBefore(PRTime aTime);
+
+ bool HasBounceTracker(const nsACString& aSiteHost) const;
+
+ // Store a bounce tracker flag for the given host. A host which received user
+ // interaction recently can not be recorded as a bounce tracker.
+ [[nodiscard]] nsresult RecordBounceTracker(const nsACString& aSiteHost,
+ PRTime aTime,
+ bool aSkipStorage = false);
+
+ // Remove one or many bounce trackers identified by site host.
+ [[nodiscard]] nsresult RemoveBounceTrackers(
+ const nsTArray<nsCString>& aSiteHosts);
+
+ [[nodiscard]] nsresult ClearSiteHost(const nsACString& aSiteHost,
+ bool aSkipStorage = false);
+
+ [[nodiscard]] nsresult ClearByTimeRange(
+ PRTime aFrom, Maybe<PRTime> aTo = Nothing(),
+ Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType = Nothing(),
+ bool aSkipStorage = false);
+
+ const nsTHashMap<nsCStringHashKey, PRTime>& UserActivationMapRef() {
+ return mUserActivation;
+ }
+
+ const nsTHashMap<nsCStringHashKey, PRTime>& BounceTrackersMapRef() {
+ return mBounceTrackers;
+ }
+
+ private:
+ ~BounceTrackingStateGlobal() = default;
+
+ // The storage which manages this state global. Used to persist changes to
+ // this state global in storage.
+ // This needs to be a weak pointer to avoid BounceTrackingProtectionStorage
+ // and BounceTrackingStateGlobal holding strong references to each other
+ // leading to memory leaks.
+ WeakPtr<BounceTrackingProtectionStorage> mStorage;
+
+ // Origin attributes this state global is associated with. e.g. if the state
+ // was associated with a PBM window this would set privateBrowsingId: 1.
+ OriginAttributes mOriginAttributes;
+
+ // Map of site hosts to moments. The moments represent the most recent wall
+ // clock time at which the user activated a top-level document on the
+ // associated site host.
+ nsTHashMap<nsCStringHashKey, PRTime> mUserActivation{};
+
+ // Map of site hosts to moments. The moments represent the first wall clock
+ // time since the last execution of the bounce tracking timer at which a page
+ // on the given site host performed an action that could indicate stateful
+ // bounce tracking took place.
+ nsTHashMap<nsCStringHashKey, PRTime> mBounceTrackers{};
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp
new file mode 100644
index 0000000000..cc9c3ce971
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp
@@ -0,0 +1,107 @@
+/* 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 "BounceTrackingStorageObserver.h"
+
+#include "BounceTrackingState.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "nsCOMPtr.h"
+#include "nsICookieNotification.h"
+#include "nsIObserverService.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "nsICookie.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(BounceTrackingStorageObserver, nsIObserver);
+
+nsresult BounceTrackingStorageObserver::Init() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__));
+
+ // Add observers to listen for cookie changes.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE);
+
+ nsresult rv = observerService->AddObserver(this, "cookie-changed", false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return observerService->AddObserver(this, "private-cookie-changed", false);
+}
+
+// nsIObserver
+NS_IMETHODIMP
+BounceTrackingStorageObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("Observe topic %s", aTopic));
+
+ NS_ENSURE_TRUE(aSubject, NS_ERROR_FAILURE);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsICookieNotification> notification =
+ do_QueryInterface(aSubject, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsICookieNotification::Action action = notification->GetAction();
+ // Filter for cookies added, changed or deleted. We don't care about other
+ // actions such as clearing the entire cookie store.
+ if (action != nsICookieNotification::COOKIE_ADDED &&
+ action != nsICookieNotification::COOKIE_CHANGED &&
+ action != nsICookieNotification::COOKIE_DELETED) {
+ return NS_OK;
+ }
+
+ // Ensure the notification is associated with a BrowsingContext. It's only set
+ // for cases where a website updated a cookie.
+ RefPtr<dom::BrowsingContext> browsingContext;
+ rv = notification->GetBrowsingContext(getter_AddRefs(browsingContext));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!browsingContext) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("Could not get BC for CookieNotification."));
+ return NS_OK;
+ }
+
+ // Check if the cookie is partitioned. Partitioned cookies can not be used for
+ // bounce tracking.
+ nsCOMPtr<nsICookie> cookie;
+ rv = notification->GetCookie(getter_AddRefs(cookie));
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(cookie);
+
+ if (!cookie->OriginAttributesNative().mPartitionKey.IsEmpty()) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("Skipping partitioned cookie."));
+ return NS_OK;
+ }
+
+ dom::BrowsingContext* topBC = browsingContext->Top();
+ dom::BrowsingContextWebProgress* webProgress =
+ topBC->Canonical()->GetWebProgress();
+ if (!webProgress) {
+ return NS_OK;
+ }
+
+ RefPtr<BounceTrackingState> bounceTrackingState =
+ webProgress->GetBounceTrackingState();
+ if (!bounceTrackingState) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("BC does not have BounceTrackingState."));
+ return NS_OK;
+ }
+
+ nsAutoCString baseDomain;
+ rv = notification->GetBaseDomain(baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return bounceTrackingState->OnCookieWrite(baseDomain);
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h
new file mode 100644
index 0000000000..1e76c85a3c
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h
@@ -0,0 +1,28 @@
+/* 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/. */
+#ifndef mozilla_BounceTrackingStorageObserver_h__
+#define mozilla_BounceTrackingStorageObserver_h__
+
+#include "mozilla/Logging.h"
+#include "nsIObserver.h"
+
+namespace mozilla {
+
+extern LazyLogModule gBounceTrackingProtectionLog;
+
+class BounceTrackingStorageObserver final : public nsIObserver {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ public:
+ BounceTrackingStorageObserver() = default;
+ nsresult Init();
+
+ private:
+ ~BounceTrackingStorageObserver() = default;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/components.conf b/toolkit/components/antitracking/bouncetrackingprotection/components.conf
new file mode 100644
index 0000000000..c0b202bb18
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/components.conf
@@ -0,0 +1,19 @@
+# -*- 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 = [
+ {
+ 'name': 'BounceTrackingProtection',
+ 'cid': '{4866F748-29DA-4C10-8EAA-ED2F7851E6B1}',
+ 'interfaces': ['nsIBounceTrackingProtection'],
+ 'contract_ids': ['@mozilla.org/bounce-tracking-protection;1'],
+ 'type': 'mozilla::BounceTrackingProtection',
+ 'headers': ['/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h'],
+ 'singleton': True,
+ 'constructor': 'mozilla::BounceTrackingProtection::GetSingleton',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/moz.build b/toolkit/components/antitracking/bouncetrackingprotection/moz.build
new file mode 100644
index 0000000000..09107bb782
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/moz.build
@@ -0,0 +1,51 @@
+# -*- 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking")
+
+XPIDL_SOURCES += [
+ "nsIBounceTrackingProtection.idl",
+]
+
+XPIDL_MODULE = "toolkit_antitracking"
+
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXPORTS.mozilla += [
+ "BounceTrackingProtection.h",
+ "BounceTrackingProtectionStorage.h",
+ "BounceTrackingRecord.h",
+ "BounceTrackingState.h",
+ "BounceTrackingStateGlobal.h",
+ "BounceTrackingStorageObserver.h",
+]
+
+UNIFIED_SOURCES += [
+ "BounceTrackingProtection.cpp",
+ "BounceTrackingProtectionStorage.cpp",
+ "BounceTrackingRecord.cpp",
+ "BounceTrackingState.cpp",
+ "BounceTrackingStateGlobal.cpp",
+ "BounceTrackingStorageObserver.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+]
+
+FINAL_LIBRARY = "xul"
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.toml",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl
new file mode 100644
index 0000000000..9ade9cb0ea
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl
@@ -0,0 +1,46 @@
+/* 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"
+
+[scriptable, uuid(4866F748-29DA-4C10-8EAA-ED2F7851E6B1)]
+interface nsIBounceTrackingProtection : nsISupports {
+ // Reset the global bounce tracking state, including the maps for tracking
+ // bounce tracker candidates and user activation.
+ void clearAll();
+
+ // Clear bounce tracking state for a specific site host and OriginAttributes pair.
+ [implicit_jscontext]
+ void clearBySiteHostAndOA(in ACString aSiteHost, in jsval originAttributes);
+
+ // Clear bounce tracking state for a specific site host for all OriginAttributes.
+ void clearBySiteHost(in ACString aSiteHost);
+
+ // Clear bounce tracking state for a specific time range.
+ void clearByTimeRange(in PRTime aFrom, in PRTime aTo);
+
+ // Clear bounce tracking state for the given origin attributes.
+ void clearByOriginAttributesPattern(in AString aPattern);
+
+ // Trigger the bounce tracking timer algorithm that clears state for
+ // classified bounce trackers.
+ [implicit_jscontext]
+ Promise testRunPurgeBounceTrackers();
+
+ // Getters and setters for user activation and bounce tracker state.
+ // These are used for testing purposes only.
+ // State is keyed by OriginAttributes.
+
+ [implicit_jscontext]
+ Array<ACString> testGetBounceTrackerCandidateHosts(in jsval originAttributes);
+
+ [implicit_jscontext]
+ Array<ACString> testGetUserActivationHosts(in jsval originAttributes);
+
+ [implicit_jscontext]
+ void testAddBounceTrackerCandidate(in jsval originAttributes, in ACString aSiteHost, in PRTime aBounceTime);
+
+ [implicit_jscontext]
+ void testAddUserActivation(in jsval originAttributes, in ACString aSiteHost, in PRTime aActivationTime);
+};
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml
new file mode 100644
index 0000000000..1c44d7804e
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = "head.js"
+prefs = [
+ "privacy.bounceTrackingProtection.enabled=true",
+ "privacy.bounceTrackingProtection.enableTestMode=true",
+ "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0",
+]
+support-files = [
+ "file_start.html",
+ "file_bounce.sjs",
+ "file_bounce.html",
+]
+
+["browser_bouncetracking_oa_isolation.js"]
+
+["browser_bouncetracking_purge.js"]
+
+["browser_bouncetracking_simple.js"]
+
+["browser_bouncetracking_stateful.js"]
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js
new file mode 100644
index 0000000000..12c2c943dd
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", true],
+ ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
+ ],
+ });
+});
+
+// Tests that bounces in PBM don't affect state in normal browsing.
+add_task(async function test_pbm_data_isolated() {
+ await runTestBounce({
+ bounceType: "client",
+ setState: "cookie-client",
+ originAttributes: { privateBrowsingId: 1 },
+ postBounceCallback: () => {
+ // After the PBM bounce assert that we haven't recorded any data for normal browsing.
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No bounce tracker candidates for normal browsing."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activations for normal browsing."
+ );
+ },
+ });
+});
+
+// Tests that bounces in PBM don't affect state in normal browsing.
+add_task(async function test_containers_isolated() {
+ await runTestBounce({
+ bounceType: "server",
+ setState: "cookie-server",
+ originAttributes: { userContextId: 2 },
+ postBounceCallback: () => {
+ // After the bounce in the container tab assert that we haven't recorded any data for normal browsing.
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No bounce tracker candidates for normal browsing."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activations for normal browsing."
+ );
+
+ // Or in another container tab.
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({
+ userContextId: 1,
+ }).length,
+ 0,
+ "No bounce tracker candidates for container tab 1."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({
+ userContextId: 1,
+ }).length,
+ 0,
+ "No user activations for container tab 1."
+ );
+ },
+ });
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js
new file mode 100644
index 0000000000..a8e98b80f0
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BOUNCE_TRACKING_GRACE_PERIOD_SEC = 30;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec",
+ BOUNCE_TRACKING_GRACE_PERIOD_SEC,
+ ],
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", false],
+ ],
+ });
+});
+
+/**
+ * The following tests ensure that sites that have open tabs are exempt from purging.
+ */
+
+function initBounceTrackerState() {
+ bounceTrackingProtection.clearAll();
+
+ // Bounce time of 1 is out of the grace period which means we should purge.
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1);
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.net", 1);
+
+ // Should not purge because within grace period.
+ let timestampWithinGracePeriod =
+ Date.now() - (BOUNCE_TRACKING_GRACE_PERIOD_SEC * 1000) / 2;
+ bounceTrackingProtection.testAddBounceTrackerCandidate(
+ {},
+ "example.org",
+ timestampWithinGracePeriod * 1000
+ );
+}
+
+add_task(async function test_purging_skip_open_foreground_tab() {
+ initBounceTrackerState();
+
+ // Foreground tab
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ ["example.net"],
+ "Should only purge example.net. example.org is within the grace period, example.com has an open tab."
+ );
+
+ info("Close the tab for example.com and test that it gets purged now.");
+ initBounceTrackerState();
+
+ BrowserTestUtils.removeTab(tab);
+ Assert.deepEqual(
+ (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(),
+ ["example.net", "example.com"].sort(),
+ "example.com should have been purged now that it no longer has an open tab."
+ );
+
+ bounceTrackingProtection.clearAll();
+});
+
+add_task(async function test_purging_skip_open_background_tab() {
+ initBounceTrackerState();
+
+ // Background tab
+ let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ ["example.net"],
+ "Should only purge example.net. example.org is within the grace period, example.com has an open tab."
+ );
+
+ info("Close the tab for example.com and test that it gets purged now.");
+ initBounceTrackerState();
+
+ BrowserTestUtils.removeTab(tab);
+ Assert.deepEqual(
+ (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(),
+ ["example.net", "example.com"].sort(),
+ "example.com should have been purged now that it no longer has an open tab."
+ );
+
+ bounceTrackingProtection.clearAll();
+});
+
+add_task(async function test_purging_skip_open_tab_extra_window() {
+ initBounceTrackerState();
+
+ // Foreground tab in new window.
+ let win = await BrowserTestUtils.openNewBrowserWindow({});
+ await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "https://example.com"
+ );
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ ["example.net"],
+ "Should only purge example.net. example.org is within the grace period, example.com has an open tab."
+ );
+
+ info(
+ "Close the window with the tab for example.com and test that it gets purged now."
+ );
+ initBounceTrackerState();
+
+ await BrowserTestUtils.closeWindow(win);
+ Assert.deepEqual(
+ (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(),
+ ["example.net", "example.com"].sort(),
+ "example.com should have been purged now that it no longer has an open tab."
+ );
+
+ bounceTrackingProtection.clearAll();
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js
new file mode 100644
index 0000000000..dfbd4d0fc0
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", false],
+ ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
+ ],
+ });
+});
+
+// Tests a stateless bounce via client redirect.
+add_task(async function test_client_bounce_simple() {
+ await runTestBounce({ bounceType: "client" });
+});
+
+// Tests a stateless bounce via server redirect.
+add_task(async function test_server_bounce_simple() {
+ await runTestBounce({ bounceType: "server" });
+});
+
+// Tests a chained redirect consisting of a server and a client redirect.
+add_task(async function test_bounce_chain() {
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No bounce tracker hosts initially."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activation hosts initially."
+ );
+
+ await BrowserTestUtils.withNewTab(
+ getBaseUrl(ORIGIN_A) + "file_start.html",
+ async browser => {
+ let promiseRecordBounces = waitForRecordBounces(browser);
+
+ // The final destination after the bounces.
+ let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html");
+
+ // Construct last hop.
+ let bounceChainUrlEnd = getBounceURL({ bounceType: "server", targetURL });
+ // Construct first hop, nesting last hop.
+ let bounceChainUrlFull = getBounceURL({
+ bounceType: "client",
+ redirectDelayMS: 100,
+ bounceOrigin: ORIGIN_TRACKER_B,
+ targetURL: bounceChainUrlEnd,
+ });
+
+ info("bounceChainUrl: " + bounceChainUrlFull.href);
+
+ // Navigate through the bounce chain.
+ await navigateLinkClick(browser, bounceChainUrlFull);
+
+ // Wait for the final site to be loaded which complete the BounceTrackingRecord.
+ await BrowserTestUtils.browserLoaded(browser, false, targetURL);
+
+ // Navigate again with user gesture which triggers
+ // BounceTrackingProtection::RecordStatefulBounces. We could rely on the
+ // timeout (mClientBounceDetectionTimeout) here but that can cause races
+ // in debug where the load is quite slow.
+ await navigateLinkClick(
+ browser,
+ new URL(getBaseUrl(ORIGIN_C) + "file_start.html")
+ );
+
+ await promiseRecordBounces;
+
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(),
+ [SITE_TRACKER_B, SITE_TRACKER].sort(),
+ `Identified all bounce trackers in the redirect chain.`
+ );
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetUserActivationHosts({}).sort(),
+ [SITE_A, SITE_B].sort(),
+ "Should only have user activation for sites where we clicked links."
+ );
+
+ bounceTrackingProtection.clearAll();
+ }
+ );
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js
new file mode 100644
index 0000000000..e7fb4521a7
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let bounceTrackingProtection;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", true],
+ ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
+ ],
+ });
+ bounceTrackingProtection = Cc[
+ "@mozilla.org/bounce-tracking-protection;1"
+ ].getService(Ci.nsIBounceTrackingProtection);
+});
+
+// Cookie tests.
+
+add_task(async function test_bounce_stateful_cookies_client() {
+ info("Test client bounce with cookie.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "cookie-client",
+ });
+ info("Test client bounce without cookie.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: null,
+ expectCandidate: false,
+ expectPurge: false,
+ });
+});
+
+add_task(async function test_bounce_stateful_cookies_server() {
+ info("Test server bounce with cookie.");
+ await runTestBounce({
+ bounceType: "server",
+ setState: "cookie-server",
+ });
+ info("Test server bounce without cookie.");
+ await runTestBounce({
+ bounceType: "server",
+ setState: null,
+ expectCandidate: false,
+ expectPurge: false,
+ });
+});
+
+// Storage tests.
+
+// TODO: Bug 1848406: Implement stateful bounce detection for localStorage.
+add_task(async function test_bounce_stateful_localStorage() {
+ info("TODO: client bounce with localStorage.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "localStorage",
+ expectCandidate: false,
+ expectPurge: false,
+ });
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html
new file mode 100644
index 0000000000..2756555fa5
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>Bounce!</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ </head>
+ <body>
+ <p>Nothing to see here...</p>
+ <script>
+ // Wrap the entire block so we can run async code.
+ (async () => {
+ let url = new URL(location.href);
+
+ let redirectDelay = url.searchParams.get("redirectDelay");
+ if(redirectDelay != null) {
+ redirectDelay = Number.parseInt(redirectDelay);
+ } else {
+ redirectDelay = 50;
+ }
+
+ let setState = url.searchParams.get("setState");
+ if (setState) {
+ let id = Math.random().toString();
+
+ if (setState == "cookie-client") {
+ let cookie = document.cookie;
+
+ if (cookie) {
+ console.info("Received cookie", cookie);
+ } else {
+ let newCookie = `id=${id}`;
+ console.info("Setting new cookie", newCookie);
+ document.cookie = newCookie;
+ }
+ } else if (setState == "localStorage") {
+ let entry = localStorage.getItem("id");
+
+ if (entry) {
+ console.info("Found localStorage entry. id", entry);
+ } else {
+ console.info("Setting new localStorage entry. id", id);
+ localStorage.setItem(id, id);
+ }
+ }
+ }
+
+ let target = url.searchParams.get("target");
+ if (target) {
+ console.info("redirecting to", target);
+ setTimeout(() => {
+ location.href = target;
+ }, redirectDelay);
+ }
+ })();
+ </script>
+ </body>
+</html>
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs
new file mode 100644
index 0000000000..5e948a899b
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs
@@ -0,0 +1,19 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let query = new URLSearchParams(request.queryString);
+
+ let setState = query.get("setState");
+ if (setState == "cookie-server") {
+ response.setHeader("Set-Cookie", "foo=bar");
+ }
+
+ let statusCode = 302;
+ let statusCodeQuery = query.get("statusCode");
+ if (statusCodeQuery) {
+ statusCode = Number.parseInt(statusCodeQuery);
+ }
+
+ response.setStatusLine("1.1", statusCode, "Found");
+ response.setHeader("Location", query.get("target"), false);
+}
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html
new file mode 100644
index 0000000000..ded691023b
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <title>Blank</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js
new file mode 100644
index 0000000000..f5857b6919
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js
@@ -0,0 +1,275 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const SITE_A = "example.com";
+const ORIGIN_A = `https://${SITE_A}`;
+
+const SITE_B = "example.org";
+const ORIGIN_B = `https://${SITE_B}`;
+
+const SITE_C = "example.net";
+const ORIGIN_C = `https://${SITE_C}`;
+
+const SITE_TRACKER = "itisatracker.org";
+const ORIGIN_TRACKER = `https://${SITE_TRACKER}`;
+
+const SITE_TRACKER_B = "trackertest.org";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const ORIGIN_TRACKER_B = `http://${SITE_TRACKER_B}`;
+
+// Test message used for observing when the record-bounces method in
+// BounceTrackingProtection.cpp has finished.
+const OBSERVER_MSG_RECORD_BOUNCES_FINISHED = "test-record-bounces-finished";
+
+const ROOT_DIR = getRootDirectory(gTestPath);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "bounceTrackingProtection",
+ "@mozilla.org/bounce-tracking-protection;1",
+ "nsIBounceTrackingProtection"
+);
+
+/**
+ * Get the base url for the current test directory using the given origin.
+ * @param {string} origin - Origin to use in URL.
+ * @returns {string} - Generated URL as a string.
+ */
+function getBaseUrl(origin) {
+ return ROOT_DIR.replace("chrome://mochitests/content", origin);
+}
+
+/**
+ * Constructs a url for an intermediate "bounce" hop which represents a tracker.
+ * @param {*} options - URL generation options.
+ * @param {('server'|'client')} options.bounceType - Redirect type to use for
+ * the bounce.
+ * @param {string} [options.bounceOrigin] - The origin of the bounce URL.
+ * @param {string} [options.targetURL] - URL to redirect to after the bounce.
+ * @param {("cookie"|null)} [options.setState] - What type of state should be set during
+ * the bounce. No state by default.
+ * @param {number} [options.statusCode] - HTTP status code to use for server
+ * side redirect. Only applies to bounceType == "server".
+ * @param {number} [options.redirectDelayMS] - How long to wait before
+ * redirecting. Only applies to bounceType == "client".
+ * @returns {URL} Generated URL which points to an endpoint performing the
+ * redirect.
+ */
+function getBounceURL({
+ bounceType,
+ bounceOrigin = ORIGIN_TRACKER,
+ targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"),
+ setState = null,
+ statusCode = 302,
+ redirectDelayMS = 50,
+}) {
+ if (!["server", "client"].includes(bounceType)) {
+ throw new Error("Invalid bounceType");
+ }
+
+ let bounceFile =
+ bounceType == "client" ? "file_bounce.html" : "file_bounce.sjs";
+
+ let bounceUrl = new URL(getBaseUrl(bounceOrigin) + bounceFile);
+
+ let { searchParams } = bounceUrl;
+ searchParams.set("target", targetURL.href);
+ if (setState) {
+ searchParams.set("setState", setState);
+ }
+
+ if (bounceType == "server") {
+ searchParams.set("statusCode", statusCode);
+ } else if (bounceType == "client") {
+ searchParams.set("redirectDelay", redirectDelayMS);
+ }
+
+ return bounceUrl;
+}
+
+/**
+ * Insert an <a href/> element with the given target and perform a synthesized
+ * click on it.
+ * @param {MozBrowser} browser - Browser to insert the link in.
+ * @param {URL} targetURL - Destination for navigation.
+ * @returns {Promise} Resolves once the click is done. Does not wait for
+ * navigation or load.
+ */
+async function navigateLinkClick(browser, targetURL) {
+ await SpecialPowers.spawn(browser, [targetURL.href], targetURL => {
+ let link = content.document.createElement("a");
+
+ link.href = targetURL;
+ link.textContent = targetURL;
+ // The link needs display: block, otherwise synthesizeMouseAtCenter doesn't
+ // hit it.
+ link.style.display = "block";
+
+ content.document.body.appendChild(link);
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser);
+}
+
+/**
+ * Wait for the record-bounces method to run for the given tab / browser.
+ * @param {browser} browser - Browser element which represents the tab we want
+ * to observe.
+ * @returns {Promise} Promise which resolves once the record-bounces method has
+ * run for the given browser.
+ */
+async function waitForRecordBounces(browser) {
+ return TestUtils.topicObserved(
+ OBSERVER_MSG_RECORD_BOUNCES_FINISHED,
+ subject => {
+ // Ensure the message was dispatched for the browser we're interested in.
+ let propBag = subject.QueryInterface(Ci.nsIPropertyBag2);
+ let browserId = propBag.getProperty("browserId");
+ return browser.browsingContext.browserId == browserId;
+ }
+ );
+}
+
+/**
+ * Test helper which loads an initial blank page, then navigates to a url which
+ * performs a bounce. Checks that the bounce hosts are properly identified as
+ * trackers.
+ * @param {object} options - Test Options.
+ * @param {('server'|'client')} options.bounceType - Whether to perform a client
+ * or server side redirect.
+ * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState]
+ * Type of state to set during the redirect. Defaults to non stateful redirect.
+ * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site to be
+ * identified as a bounce tracker (candidate).
+ * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to have
+ * its storage purged.
+ * @param {OriginAttributes} [options.originAttributes={}] - Origin attributes
+ * to use for the test. This determines whether the test is run in normal
+ * browsing, a private window or a container tab. By default the test is run
+ * in normal browsing.
+ * @param {function} [options.postBounceCallback] - Optional function to run after the
+ * bounce has completed.
+ */
+async function runTestBounce(options = {}) {
+ let {
+ bounceType,
+ setState = null,
+ expectCandidate = true,
+ expectPurge = true,
+ originAttributes = {},
+ postBounceCallback = () => {},
+ } = options;
+ info(`runTestBounce ${JSON.stringify(options)}`);
+
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts(
+ originAttributes
+ ).length,
+ 0,
+ "No bounce tracker hosts initially."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts(originAttributes)
+ .length,
+ 0,
+ "No user activation hosts initially."
+ );
+
+ let win = window;
+ let { privateBrowsingId, userContextId } = originAttributes;
+ let usePrivateWindow =
+ privateBrowsingId != null &&
+ privateBrowsingId !=
+ Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID;
+ if (userContextId != null && userContextId > 0 && usePrivateWindow) {
+ throw new Error("userContextId is not supported in private windows");
+ }
+
+ if (usePrivateWindow) {
+ win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ }
+
+ let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ userContextId,
+ });
+ win.gBrowser.selectedTab = tab;
+
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let promiseRecordBounces = waitForRecordBounces(browser);
+
+ // The final destination after the bounce.
+ let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html");
+
+ // Navigate through the bounce chain.
+ await navigateLinkClick(
+ browser,
+ getBounceURL({ bounceType, targetURL, setState })
+ );
+
+ // Wait for the final site to be loaded which complete the BounceTrackingRecord.
+ await BrowserTestUtils.browserLoaded(browser, false, targetURL);
+
+ // Navigate again with user gesture which triggers
+ // BounceTrackingProtection::RecordStatefulBounces. We could rely on the
+ // timeout (mClientBounceDetectionTimeout) here but that can cause races
+ // in debug where the load is quite slow.
+ await navigateLinkClick(
+ browser,
+ new URL(getBaseUrl(ORIGIN_C) + "file_start.html")
+ );
+
+ await promiseRecordBounces;
+
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts(
+ originAttributes
+ ),
+ expectCandidate ? [SITE_TRACKER] : [],
+ `Should ${
+ expectCandidate ? "" : "not "
+ }have identified ${SITE_TRACKER} as a bounce tracker.`
+ );
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetUserActivationHosts(originAttributes)
+ .sort(),
+ [SITE_A, SITE_B].sort(),
+ "Should only have user activation for sites where we clicked links."
+ );
+
+ // If the caller specified a function to run after the bounce, run it now.
+ await postBounceCallback();
+
+ Assert.deepEqual(
+ await bounceTrackingProtection.testRunPurgeBounceTrackers(),
+ expectPurge ? [SITE_TRACKER] : [],
+ `Should ${expectPurge ? "" : "not "}purge state for ${SITE_TRACKER}.`
+ );
+
+ // Clean up
+ BrowserTestUtils.removeTab(tab);
+ if (usePrivateWindow) {
+ await BrowserTestUtils.closeWindow(win);
+
+ info(
+ "Closing the last PBM window should trigger a purge of all PBM state."
+ );
+ Assert.ok(
+ !bounceTrackingProtection.testGetBounceTrackerCandidateHosts(
+ originAttributes
+ ).length,
+ "No bounce tracker hosts after closing private window."
+ );
+ Assert.ok(
+ !bounceTrackingProtection.testGetUserActivationHosts(originAttributes)
+ .length,
+ "No user activation hosts after closing private window."
+ );
+ }
+ bounceTrackingProtection.clearAll();
+}
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml
new file mode 100644
index 0000000000..7caad6eb15
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+prefs = [
+ "privacy.bounceTrackingProtection.enabled=true",
+ "privacy.bounceTrackingProtection.enableTestMode=true",
+]
+
+["test_bouncetracking_storage_persistence.py"]
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py
new file mode 100644
index 0000000000..afc3239839
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py
@@ -0,0 +1,133 @@
+# 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_harness import MarionetteTestCase
+
+# Tests the persistence of the bounce tracking protection storage across
+# restarts.
+
+
+class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase):
+ def setUp(self):
+ super(BounceTrackingStoragePersistenceTestCase, self).setUp()
+ self.marionette.enforce_gecko_prefs(
+ {
+ "privacy.bounceTrackingProtection.enabled": True,
+ "privacy.bounceTrackingProtection.enableTestMode": True,
+ }
+ )
+
+ self.marionette.set_context("chrome")
+ self.populate_state()
+
+ def populate_state(self):
+ # Add some data to test persistence.
+ self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.net", Date.now() * 10000);
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.org", Date.now() * 10000);
+ bounceTrackingProtection.testAddBounceTrackerCandidate({ userContextId: 3 }, "tracker.com", Date.now() * 10000);
+ // A private browsing entry which must not be persisted across restarts.
+ bounceTrackingProtection.testAddBounceTrackerCandidate({ privateBrowsingId: 1 }, "tracker.net", Date.now() * 10000);
+
+ bounceTrackingProtection.testAddUserActivation({}, "example.com", (Date.now() + 5000) * 10000);
+ // A private browsing entry which must not be persisted across restarts.
+ bounceTrackingProtection.testAddUserActivation({ privateBrowsingId: 1 }, "example.org", (Date.now() + 2000) * 10000);
+ """
+ )
+
+ def test_state_after_restart(self):
+ self.marionette.restart(clean=False, in_app=True)
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort();
+ """,
+ )
+ self.assertEqual(
+ len(bounceTrackerCandidates),
+ 2,
+ msg="There should be two entries for default OA",
+ )
+ self.assertEqual(bounceTrackerCandidates[0], "bouncetracker.net")
+ self.assertEqual(bounceTrackerCandidates[1], "bouncetracker.org")
+
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort();
+ """,
+ )
+ self.assertEqual(
+ len(bounceTrackerCandidates),
+ 1,
+ msg="There should be only one entry for user context 3",
+ )
+ self.assertEqual(bounceTrackerCandidates[0], "tracker.com")
+
+ # Unrelated user context should not have any entries.
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 4 }).length;
+ """,
+ )
+ self.assertEqual(
+ bounceTrackerCandidates,
+ 0,
+ msg="There should be no entries for user context 4",
+ )
+
+ # Private browsing entries should not be persisted across restarts.
+ bounceTrackerCandidates = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ privateBrowsingId: 1 }).length;
+ """,
+ )
+ self.assertEqual(
+ bounceTrackerCandidates,
+ 0,
+ msg="There should be no entries for private browsing",
+ )
+
+ userActivations = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetUserActivationHosts({}).sort();
+ """,
+ )
+ self.assertEqual(
+ len(userActivations),
+ 1,
+ msg="There should be only one entry for user activation",
+ )
+ self.assertEqual(userActivations[0], "example.com")
+
+ # Private browsing entries should not be persisted across restarts.
+ userActivations = self.marionette.execute_script(
+ """
+ let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+ return bounceTrackingProtection.testGetUserActivationHosts({ privateBrowsingId: 1 }).length;
+ """,
+ )
+ self.assertEqual(
+ userActivations, 0, msg="There should be no entries for private browsing"
+ )
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
new file mode 100644
index 0000000000..5ede57a08b
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
@@ -0,0 +1,307 @@
+/* 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"
+);
+
+let btp;
+let bounceTrackingGracePeriodSec;
+let bounceTrackingActivationLifetimeSec;
+
+/**
+ * Adds brackets to a host if it's an IPv6 address.
+ * @param {string} host - Host which may be an IPv6.
+ * @returns {string} bracketed IPv6 or host if host is not an IPv6.
+ */
+function maybeFixupIpv6(host) {
+ if (!host.includes(":")) {
+ return host;
+ }
+ return `[${host}]`;
+}
+
+/**
+ * Adds cookies and indexedDB test data for the given host.
+ * @param {string} host
+ */
+async function addStateForHost(host) {
+ info(`adding state for host ${host}`);
+ SiteDataTestUtils.addToCookies({ host });
+ await SiteDataTestUtils.addToIndexedDB(`https://${maybeFixupIpv6(host)}`);
+}
+
+/**
+ * Checks if the given host as cookies or indexedDB data.
+ * @param {string} host
+ * @returns {boolean}
+ */
+async function hasStateForHost(host) {
+ let origin = `https://${maybeFixupIpv6(host)}`;
+ if (SiteDataTestUtils.hasCookies(origin)) {
+ return true;
+ }
+ return SiteDataTestUtils.hasIndexedDB(origin);
+}
+
+/**
+ * Assert that there are no bounce tracker candidates or user activations
+ * recorded.
+ */
+function assertEmpty() {
+ Assert.equal(
+ btp.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No tracker candidates."
+ );
+ Assert.equal(
+ btp.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activation hosts."
+ );
+}
+
+add_setup(function () {
+ // Need a profile to data clearing calls.
+ do_get_profile();
+
+ btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+
+ // Reset global bounce tracking state.
+ btp.clearAll();
+
+ bounceTrackingGracePeriodSec = Services.prefs.getIntPref(
+ "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec"
+ );
+ bounceTrackingActivationLifetimeSec = Services.prefs.getIntPref(
+ "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec"
+ );
+});
+
+/**
+ * When both maps are empty running PurgeBounceTrackers should be a no-op.
+ */
+add_task(async function test_empty() {
+ assertEmpty();
+
+ info("Run PurgeBounceTrackers");
+ await btp.testRunPurgeBounceTrackers();
+
+ assertEmpty();
+});
+
+/**
+ * Tests that the PurgeBounceTrackers behaves as expected by adding site state
+ * and adding simulated bounce state and user activations.
+ */
+add_task(async function test_purge() {
+ let now = Date.now();
+
+ // Epoch in MS.
+ let timestampWithinGracePeriod =
+ now - (bounceTrackingGracePeriodSec * 1000) / 2;
+ let timestampWithinGracePeriod2 =
+ now - (bounceTrackingGracePeriodSec * 1000) / 4;
+ let timestampOutsideGracePeriodFiveSeconds =
+ now - (bounceTrackingGracePeriodSec + 5) * 1000;
+ let timestampOutsideGracePeriodThreeDays =
+ now - (bounceTrackingGracePeriodSec + 60 * 60 * 24 * 3) * 1000;
+ let timestampFuture = now + bounceTrackingGracePeriodSec * 1000 * 2;
+
+ let timestampValidUserActivation =
+ now - (bounceTrackingActivationLifetimeSec * 1000) / 2;
+ let timestampExpiredUserActivationFourSeconds =
+ now - (bounceTrackingActivationLifetimeSec + 4) * 1000;
+ let timestampExpiredUserActivationTenDays =
+ now - (bounceTrackingActivationLifetimeSec + 60 * 60 * 24 * 10) * 1000;
+
+ const TEST_TRACKERS = {
+ "example.com": {
+ bounceTime: timestampWithinGracePeriod,
+ userActivationTime: null,
+ message: "Should not purge within grace period.",
+ shouldPurge: bounceTrackingGracePeriodSec == 0,
+ },
+ "example2.com": {
+ bounceTime: timestampWithinGracePeriod2,
+ userActivationTime: null,
+ message: "Should not purge within grace period (2).",
+ shouldPurge: bounceTrackingGracePeriodSec == 0,
+ },
+ "example.net": {
+ bounceTime: timestampOutsideGracePeriodFiveSeconds,
+ userActivationTime: null,
+ message: "Should purge after grace period.",
+ shouldPurge: true,
+ },
+ // Also ensure that clear data calls with IP sites succeed.
+ "1.2.3.4": {
+ bounceTime: timestampOutsideGracePeriodThreeDays,
+ userActivationTime: null,
+ message: "Should purge after grace period (2).",
+ shouldPurge: true,
+ },
+ "2606:4700:4700::1111": {
+ bounceTime: timestampOutsideGracePeriodThreeDays,
+ userActivationTime: null,
+ message: "Should purge after grace period (3).",
+ shouldPurge: true,
+ },
+ "example.org": {
+ bounceTime: timestampWithinGracePeriod,
+ userActivationTime: null,
+ message: "Should not purge within grace period.",
+ shouldPurge: false,
+ },
+ "example2.org": {
+ bounceTime: timestampFuture,
+ userActivationTime: null,
+ message: "Should not purge for future bounce time (within grace period).",
+ shouldPurge: false,
+ },
+ "1.1.1.1": {
+ bounceTime: null,
+ userActivationTime: timestampValidUserActivation,
+ message: "Should not purge without bounce (valid user activation).",
+ shouldPurge: false,
+ },
+ // Also testing domains with trailing ".".
+ "mozilla.org.": {
+ bounceTime: null,
+ userActivationTime: timestampExpiredUserActivationFourSeconds,
+ message: "Should not purge without bounce (expired user activation).",
+ shouldPurge: false,
+ },
+ "firefox.com": {
+ bounceTime: null,
+ userActivationTime: timestampExpiredUserActivationTenDays,
+ message: "Should not purge without bounce (expired user activation) (2).",
+ shouldPurge: false,
+ },
+ };
+
+ info("Assert empty initially.");
+ assertEmpty();
+
+ info("Populate bounce and user activation sets.");
+
+ let expectedBounceTrackerHosts = [];
+ let expectedUserActivationHosts = [];
+
+ let expiredUserActivationHosts = [];
+ let expectedPurgedHosts = [];
+
+ // This would normally happen over time while browsing.
+ let initPromises = Object.entries(TEST_TRACKERS).map(
+ async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => {
+ // Add site state so we can later assert it has been purged.
+ await addStateForHost(siteHost);
+
+ if (bounceTime != null) {
+ if (userActivationTime != null) {
+ throw new Error(
+ "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint."
+ );
+ }
+
+ expectedBounceTrackerHosts.push(siteHost);
+
+ // Convert bounceTime timestamp to nanoseconds (PRTime).
+ info(
+ `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms`
+ );
+ btp.testAddBounceTrackerCandidate({}, siteHost, bounceTime * 1000);
+ }
+
+ if (userActivationTime != null) {
+ if (bounceTime != null) {
+ throw new Error(
+ "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint."
+ );
+ }
+
+ expectedUserActivationHosts.push(siteHost);
+ if (
+ userActivationTime + bounceTrackingActivationLifetimeSec * 1000 >
+ now
+ ) {
+ expiredUserActivationHosts.push(siteHost);
+ }
+
+ // Convert userActivationTime timestamp to nanoseconds (PRTime).
+ info(
+ `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms`
+ );
+ btp.testAddUserActivation({}, siteHost, userActivationTime * 1000);
+ }
+
+ if (shouldPurge) {
+ expectedPurgedHosts.push(siteHost);
+ }
+ }
+ );
+ await Promise.all(initPromises);
+
+ info(
+ "Check that bounce and user activation data has been correctly recorded."
+ );
+ Assert.deepEqual(
+ btp.testGetBounceTrackerCandidateHosts({}).sort(),
+ expectedBounceTrackerHosts.sort(),
+ "Has added bounce tracker hosts."
+ );
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts({}).sort(),
+ expectedUserActivationHosts.sort(),
+ "Has added user activation hosts."
+ );
+
+ info("Run PurgeBounceTrackers");
+ let actualPurgedHosts = await btp.testRunPurgeBounceTrackers();
+
+ Assert.deepEqual(
+ actualPurgedHosts.sort(),
+ expectedPurgedHosts.sort(),
+ "Should have purged all expected hosts."
+ );
+
+ let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts
+ .filter(host => !expectedPurgedHosts.includes(host))
+ .sort();
+ Assert.deepEqual(
+ btp.testGetBounceTrackerCandidateHosts({}).sort(),
+ expectedBounceTrackerHostsAfterPurge.sort(),
+ "After purge the bounce tracker candidate host set should be updated correctly."
+ );
+
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts({}).sort(),
+ expiredUserActivationHosts.sort(),
+ "After purge any expired user activation records should have been removed"
+ );
+
+ info("Test that we actually purged the correct sites.");
+ for (let siteHost of expectedPurgedHosts) {
+ Assert.ok(
+ !(await hasStateForHost(siteHost)),
+ `Site ${siteHost} should no longer have state.`
+ );
+ }
+ for (let siteHost of expectedBounceTrackerHostsAfterPurge) {
+ Assert.ok(
+ await hasStateForHost(siteHost),
+ `Site ${siteHost} should still have state.`
+ );
+ }
+
+ info("Reset bounce tracking state.");
+ btp.clearAll();
+ assertEmpty();
+
+ info("Clean up site data.");
+ await SiteDataTestUtils.clear();
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..16e270b85c
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+prefs = [
+ "privacy.bounceTrackingProtection.enabled=true",
+ "privacy.bounceTrackingProtection.enableTestMode=true",
+ "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0",
+]
+
+["test_bouncetracking_purge.js"]