summaryrefslogtreecommitdiffstats
path: root/toolkit/components/antitracking/bouncetrackingprotection
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:35:37 +0000
commita90a5cba08fdf6c0ceb95101c275108a152a3aed (patch)
tree532507288f3defd7f4dcf1af49698bcb76034855 /toolkit/components/antitracking/bouncetrackingprotection
parentAdding debian version 126.0.1-1. (diff)
downloadfirefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.tar.xz
firefox-a90a5cba08fdf6c0ceb95101c275108a152a3aed.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/antitracking/bouncetrackingprotection')
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.cpp22
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.h35
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp246
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h27
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp9
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp4
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp184
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h39
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp14
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp84
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h9
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml64
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/moz.build3
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingMapEntry.idl17
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl11
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml16
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js101
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js126
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js45
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js75
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js10
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js (renamed from toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js)23
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js54
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js38
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js66
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html187
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js40
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js174
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py6
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js10
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js153
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js62
-rw-r--r--toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml3
33 files changed, 1741 insertions, 216 deletions
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.cpp
new file mode 100644
index 0000000000..2c00306441
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.cpp
@@ -0,0 +1,22 @@
+/* -*- 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 "BounceTrackingMapEntry.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(BounceTrackingMapEntry, nsIBounceTrackingMapEntry);
+
+NS_IMETHODIMP BounceTrackingMapEntry::GetSiteHost(nsACString& aSiteHost) {
+ aSiteHost = mSiteHost;
+ return NS_OK;
+}
+NS_IMETHODIMP BounceTrackingMapEntry::GetTimeStamp(int64_t* aTimeStamp) {
+ *aTimeStamp = mTimeStamp;
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.h
new file mode 100644
index 0000000000..45d7237cff
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.h
@@ -0,0 +1,35 @@
+/* -*- 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_BounceTrackingMapEntry_h
+#define mozilla_BounceTrackingMapEntry_h
+
+#include "nsIBounceTrackingMapEntry.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+/**
+ * Represents an entry in the global bounce tracker or user activation map.
+ */
+class BounceTrackingMapEntry final : public nsIBounceTrackingMapEntry {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIBOUNCETRACKINGMAPENTRY
+
+ BounceTrackingMapEntry(const nsACString& aSiteHost, PRTime aTimeStamp)
+ : mSiteHost(aSiteHost), mTimeStamp(aTimeStamp) {}
+
+ private:
+ ~BounceTrackingMapEntry() = default;
+
+ nsAutoCString mSiteHost;
+ PRTime mTimeStamp;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp
index 2b0577d5c6..7882cd6d79 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp
@@ -7,6 +7,7 @@
#include "BounceTrackingProtectionStorage.h"
#include "BounceTrackingState.h"
#include "BounceTrackingRecord.h"
+#include "BounceTrackingMapEntry.h"
#include "BounceTrackingStateGlobal.h"
#include "ErrorList.h"
@@ -14,6 +15,7 @@
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ContentBlockingAllowList.h"
#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/dom/Promise.h"
@@ -21,6 +23,7 @@
#include "nsHashPropertyBag.h"
#include "nsIClearDataService.h"
#include "nsIObserverService.h"
+#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "nsISupports.h"
#include "nsServiceManagerUtils.h"
@@ -39,6 +42,10 @@ LazyLogModule gBounceTrackingProtectionLog("BounceTrackingProtection");
static StaticRefPtr<BounceTrackingProtection> sBounceTrackingProtection;
+// Keeps track of whether the feature is enabled based on pref state.
+// Initialized on first call of GetSingleton.
+Maybe<bool> BounceTrackingProtection::sFeatureIsEnabled;
+
static constexpr uint32_t TRACKER_PURGE_FLAGS =
nsIClearDataService::CLEAR_ALL_CACHES | nsIClearDataService::CLEAR_COOKIES |
nsIClearDataService::CLEAR_DOM_STORAGES |
@@ -53,10 +60,33 @@ already_AddRefed<BounceTrackingProtection>
BounceTrackingProtection::GetSingleton() {
MOZ_ASSERT(XRE_IsParentProcess());
- if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) {
+ // First call to GetSingleton, check main feature pref and record telemetry.
+ if (sFeatureIsEnabled.isNothing()) {
+ if (StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) {
+ sFeatureIsEnabled = Some(true);
+
+ glean::bounce_tracking_protection::enabled_at_startup.Set(true);
+ glean::bounce_tracking_protection::enabled_dry_run_mode_at_startup.Set(
+ StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode());
+ } else {
+ sFeatureIsEnabled = Some(false);
+
+ glean::bounce_tracking_protection::enabled_at_startup.Set(false);
+ glean::bounce_tracking_protection::enabled_dry_run_mode_at_startup.Set(
+ false);
+
+ // Feature is disabled.
+ return nullptr;
+ }
+ }
+ MOZ_ASSERT(sFeatureIsEnabled.isSome());
+
+ // Feature is disabled.
+ if (!sFeatureIsEnabled.value()) {
return nullptr;
}
+ // Feature is enabled, lazily create singleton instance.
if (!sBounceTrackingProtection) {
sBounceTrackingProtection = new BounceTrackingProtection();
@@ -67,7 +97,25 @@ BounceTrackingProtection::GetSingleton() {
}
BounceTrackingProtection::BounceTrackingProtection() {
- MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("constructor"));
+ MOZ_LOG(
+ gBounceTrackingProtectionLog, LogLevel::Info,
+ ("Init BounceTrackingProtection. Config: enableDryRunMode: %d, "
+ "bounceTrackingActivationLifetimeSec: %d, bounceTrackingGracePeriodSec: "
+ "%d, bounceTrackingPurgeTimerPeriodSec: %d, "
+ "clientBounceDetectionTimerPeriodMS: %d, requireStatefulBounces: %d, "
+ "HasMigratedUserActivationData: %d",
+ StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode(),
+ StaticPrefs::
+ privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec(),
+ StaticPrefs::
+ privacy_bounceTrackingProtection_bounceTrackingGracePeriodSec(),
+ StaticPrefs::
+ privacy_bounceTrackingProtection_bounceTrackingPurgeTimerPeriodSec(),
+ StaticPrefs::
+ privacy_bounceTrackingProtection_clientBounceDetectionTimerPeriodMS(),
+ StaticPrefs::privacy_bounceTrackingProtection_requireStatefulBounces(),
+ StaticPrefs::
+ privacy_bounceTrackingProtection_hasMigratedUserActivationData()));
mStorage = new BounceTrackingProtectionStorage();
@@ -78,6 +126,12 @@ BounceTrackingProtection::BounceTrackingProtection() {
return;
}
+ rv = MaybeMigrateUserInteractionPermissions();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Error,
+ ("user activation permission migration failed"));
+ }
+
// Schedule timer for tracker purging. The timer interval is determined by
// pref.
uint32_t purgeTimerPeriod = StaticPrefs::
@@ -134,6 +188,11 @@ nsresult BounceTrackingProtection::RecordStatefulBounces(
// For each host in navigable’s bounce tracking record's bounce set:
for (const nsACString& host : record->GetBounceHosts()) {
+ // Skip "null" entries, they are only used for logging purposes.
+ if (host.EqualsLiteral("null")) {
+ continue;
+ }
+
// If host equals navigable’s bounce tracking record's initial host,
// continue.
if (host == record->GetInitialHost()) {
@@ -187,9 +246,10 @@ nsresult BounceTrackingProtection::RecordStatefulBounces(
}
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
- ("%s: Added candidate to mBounceTrackers: %s, Time: %" PRIu64,
+ ("%s: Added bounce tracker candidate. siteHost: %s, "
+ "aBounceTrackingState: %s",
__FUNCTION__, PromiseFlatCString(host).get(),
- static_cast<uint64_t>(now)));
+ aBounceTrackingState->Describe().get()));
}
// Set navigable’s bounce tracking record to null.
@@ -220,30 +280,34 @@ nsresult BounceTrackingProtection::RecordStatefulBounces(
}
nsresult BounceTrackingProtection::RecordUserActivation(
- nsIPrincipal* aPrincipal) {
+ nsIPrincipal* aPrincipal, Maybe<PRTime> aActivationTime) {
MOZ_ASSERT(XRE_IsParentProcess());
-
NS_ENSURE_ARG_POINTER(aPrincipal);
- NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE);
+
+ if (!BounceTrackingState::ShouldTrackPrincipal(aPrincipal)) {
+ return NS_OK;
+ }
nsAutoCString siteHost;
nsresult rv = aPrincipal->GetBaseDomain(siteHost);
NS_ENSURE_SUCCESS(rv, rv);
- MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
("%s: siteHost: %s", __FUNCTION__, siteHost.get()));
RefPtr<BounceTrackingStateGlobal> globalState =
mStorage->GetOrCreateStateGlobal(aPrincipal);
MOZ_ASSERT(globalState);
- return globalState->RecordUserActivation(siteHost, PR_Now());
+ // aActivationTime defaults to current time if no value is provided.
+ return globalState->RecordUserActivation(siteHost,
+ aActivationTime.valueOr(PR_Now()));
}
NS_IMETHODIMP
BounceTrackingProtection::TestGetBounceTrackerCandidateHosts(
JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx,
- nsTArray<nsCString>& aCandidates) {
+ nsTArray<RefPtr<nsIBounceTrackingMapEntry>>& aCandidates) {
MOZ_ASSERT(aCx);
OriginAttributes oa;
@@ -254,8 +318,11 @@ BounceTrackingProtection::TestGetBounceTrackerCandidateHosts(
BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa);
MOZ_ASSERT(globalState);
- for (const nsACString& host : globalState->BounceTrackersMapRef().Keys()) {
- aCandidates.AppendElement(host);
+ for (auto iter = globalState->BounceTrackersMapRef().ConstIter();
+ !iter.Done(); iter.Next()) {
+ RefPtr<nsIBounceTrackingMapEntry> candidate =
+ new BounceTrackingMapEntry(iter.Key(), iter.Data());
+ aCandidates.AppendElement(candidate);
}
return NS_OK;
@@ -264,7 +331,7 @@ BounceTrackingProtection::TestGetBounceTrackerCandidateHosts(
NS_IMETHODIMP
BounceTrackingProtection::TestGetUserActivationHosts(
JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx,
- nsTArray<nsCString>& aHosts) {
+ nsTArray<RefPtr<nsIBounceTrackingMapEntry>>& aHosts) {
MOZ_ASSERT(aCx);
OriginAttributes oa;
@@ -275,8 +342,11 @@ BounceTrackingProtection::TestGetUserActivationHosts(
BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa);
MOZ_ASSERT(globalState);
- for (const nsACString& host : globalState->UserActivationMapRef().Keys()) {
- aHosts.AppendElement(host);
+ for (auto iter = globalState->UserActivationMapRef().ConstIter();
+ !iter.Done(); iter.Next()) {
+ RefPtr<nsIBounceTrackingMapEntry> candidate =
+ new BounceTrackingMapEntry(iter.Key(), iter.Data());
+ aHosts.AppendElement(candidate);
}
return NS_OK;
@@ -419,6 +489,11 @@ BounceTrackingProtection::TestAddUserActivation(
return stateGlobal->RecordUserActivation(host, aActivationTime);
}
+NS_IMETHODIMP
+BounceTrackingProtection::TestMaybeMigrateUserInteractionPermissions() {
+ return MaybeMigrateUserInteractionPermissions();
+}
+
RefPtr<BounceTrackingProtection::PurgeBounceTrackersMozPromise>
BounceTrackingProtection::PurgeBounceTrackers() {
// Prevent multiple purge operations from running at the same time.
@@ -468,7 +543,7 @@ BounceTrackingProtection::PurgeBounceTrackers() {
aResults) {
MOZ_ASSERT(aResults.IsResolve(), "AllSettled never rejects");
- MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
("%s: Done. Cleared %zu hosts.", __FUNCTION__,
aResults.ResolveValue().Length()));
@@ -572,6 +647,10 @@ nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal(
__FUNCTION__, PromiseFlatCString(host).get(),
originAttributeSuffix.get()));
}
+ // Remove allow-listed host so we don't need to check in again next purge
+ // run. If it gets classified again and the allow-list entry gets removed
+ // it will be purged in the next run.
+ bounceTrackerCandidatesToRemove.AppendElement(host);
continue;
}
@@ -581,15 +660,23 @@ nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal(
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__);
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ ("%s: Purging bounce tracker. siteHost: %s, bounceTime: %" PRIu64
+ " aStateGlobal: %s",
+ __FUNCTION__, PromiseFlatCString(host).get(), bounceTime,
+ aStateGlobal->Describe().get()));
+
+ if (StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode()) {
+ // In dry-run mode, we don't actually clear the data, but we still want to
+ // resolve the promise to indicate that the data would have been cleared.
+ clearPromise->Resolve(host, __func__);
+ } else {
+ // 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__);
+ }
}
aClearPromises.AppendElement(clearPromise);
@@ -642,22 +729,129 @@ nsresult BounceTrackingProtection::ClearExpiredUserInteractions(
return NS_OK;
}
+nsresult BounceTrackingProtection::MaybeMigrateUserInteractionPermissions() {
+ // Only run the migration once.
+ if (StaticPrefs::
+ privacy_bounceTrackingProtection_hasMigratedUserActivationData()) {
+ return NS_OK;
+ }
+
+ MOZ_LOG(
+ gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Importing user activation data from permissions", __FUNCTION__));
+
+ // Get all user activation permissions that are within our user activation
+ // lifetime. We don't care about the rest since they are considered expired
+ // for BTP.
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPermissionManager> permManager =
+ do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(permManager, NS_ERROR_FAILURE);
+
+ // Construct the since time param. The permission manager expects epoch in
+ // miliseconds.
+ int64_t nowMS = PR_Now() / PR_USEC_PER_MSEC;
+ int64_t activationLifetimeMS =
+ static_cast<int64_t>(
+ StaticPrefs::
+ privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec()) *
+ PR_MSEC_PER_SEC;
+ int64_t since = nowMS - activationLifetimeMS;
+ MOZ_ASSERT(since > 0);
+
+ // Get all user activation permissions last modified between "since" and now.
+ nsTArray<RefPtr<nsIPermission>> userActivationPermissions;
+ rv = permManager->GetAllByTypeSince("storageAccessAPI"_ns, since,
+ userActivationPermissions);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Found %zu (non-expired) user activation permissions",
+ __FUNCTION__, userActivationPermissions.Length()));
+
+ for (const auto& perm : userActivationPermissions) {
+ nsCOMPtr<nsIPrincipal> permPrincipal;
+
+ rv = perm->GetPrincipal(getter_AddRefs(permPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ MOZ_ASSERT(permPrincipal);
+
+ // The time the permission was last modified is the time of last user
+ // activation.
+ int64_t modificationTimeMS;
+ rv = perm->GetModificationTime(&modificationTimeMS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(modificationTimeMS >= since,
+ "Unexpected permission modification time");
+
+ // We may end up with duplicates here since user activation permissions are
+ // tracked by origin, while BTP tracks user activation by site host.
+ // RecordUserActivation is responsible for only keeping the most recent user
+ // activation flag for a given site host and needs to make sure existing
+ // activation flags are not overwritten by older timestamps.
+ // RecordUserActivation expects epoch in microseconds.
+ rv = RecordUserActivation(permPrincipal,
+ Some(modificationTimeMS * PR_USEC_PER_MSEC));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ }
+
+ // Migration successful, set the pref to indicate that we have migrated.
+ return mozilla::Preferences::SetBool(
+ "privacy.bounceTrackingProtection.hasMigratedUserActivationData", true);
+}
+
// ClearDataCallback
NS_IMPL_ISUPPORTS(BounceTrackingProtection::ClearDataCallback,
nsIClearDataCallback);
+BounceTrackingProtection::ClearDataCallback::ClearDataCallback(
+ ClearDataMozPromise::Private* aPromise, const nsACString& aHost)
+ : mHost(aHost), mClearDurationTimer(0), mPromise(aPromise) {
+ MOZ_ASSERT(!aHost.IsEmpty(), "Host must not be empty");
+ if (!StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode()) {
+ // Only collect timing information when actually performing the deletion
+ mClearDurationTimer =
+ glean::bounce_tracking_protection::purge_duration.Start();
+ MOZ_ASSERT(mClearDurationTimer);
+ }
+};
+
+BounceTrackingProtection::ClearDataCallback::~ClearDataCallback() {
+ mPromise->Reject(0, __func__);
+ if (mClearDurationTimer) {
+ glean::bounce_tracking_protection::purge_duration.Cancel(
+ std::move(mClearDurationTimer));
+ }
+}
+
// nsIClearDataCallback implementation
NS_IMETHODIMP BounceTrackingProtection::ClearDataCallback::OnDataDeleted(
uint32_t aFailedFlags) {
if (aFailedFlags) {
mPromise->Reject(aFailedFlags, __func__);
} else {
- MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
("%s: Cleared %s", __FUNCTION__, mHost.get()));
mPromise->Resolve(std::move(mHost), __func__);
}
+ RecordClearDurationTelemetry();
return NS_OK;
}
+void BounceTrackingProtection::ClearDataCallback::
+ RecordClearDurationTelemetry() {
+ if (mClearDurationTimer) {
+ glean::bounce_tracking_protection::purge_duration.StopAndAccumulate(
+ std::move(mClearDurationTimer));
+ mClearDurationTimer = 0;
+ }
+}
+
} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h
index e99cf895be..98b235ead1 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h
@@ -6,8 +6,10 @@
#include "mozilla/Logging.h"
#include "mozilla/MozPromise.h"
+#include "mozilla/glean/GleanMetrics.h"
#include "nsIBounceTrackingProtection.h"
#include "nsIClearDataService.h"
+#include "mozilla/Maybe.h"
class nsIPrincipal;
class nsITimer;
@@ -37,8 +39,11 @@ class BounceTrackingProtection final : public nsIBounceTrackingProtection {
[[nodiscard]] nsresult RecordStatefulBounces(
BounceTrackingState* aBounceTrackingState);
- // Stores a user activation flag with a timestamp for the given principal.
- [[nodiscard]] nsresult RecordUserActivation(nsIPrincipal* aPrincipal);
+ // Stores a user activation flag with a timestamp for the given principal. The
+ // timestamp defaults to the current time, but can be overridden via
+ // aActivationTime.
+ [[nodiscard]] nsresult RecordUserActivation(
+ nsIPrincipal* aPrincipal, Maybe<PRTime> aActivationTime = Nothing());
// Clears expired user interaction flags for the given state global. If
// aStateGlobal == nullptr, clears expired user interaction flags for all
@@ -50,6 +55,10 @@ class BounceTrackingProtection final : public nsIBounceTrackingProtection {
BounceTrackingProtection();
~BounceTrackingProtection() = default;
+ // Keeps track of whether the feature is enabled based on pref state.
+ // Initialized on first call of GetSingleton.
+ static Maybe<bool> sFeatureIsEnabled;
+
// Timer which periodically runs PurgeBounceTrackers.
nsCOMPtr<nsITimer> mBounceTrackingPurgeTimer;
@@ -82,15 +91,23 @@ class BounceTrackingProtection final : public nsIBounceTrackingProtection {
NS_DECL_NSICLEARDATACALLBACK
explicit ClearDataCallback(ClearDataMozPromise::Private* aPromise,
- const nsACString& aHost)
- : mHost(aHost), mPromise(aPromise){};
+ const nsACString& aHost);
private:
- virtual ~ClearDataCallback() { mPromise->Reject(0, __func__); }
+ virtual ~ClearDataCallback();
nsCString mHost;
+
+ void RecordClearDurationTelemetry();
+
+ glean::TimerId mClearDurationTimer;
RefPtr<ClearDataMozPromise::Private> mPromise;
};
+
+ // Imports user activation permissions from permission manager if needed. This
+ // is important so we don't purge data for sites the user has interacted with
+ // before the feature was enabled.
+ [[nodiscard]] nsresult MaybeMigrateUserInteractionPermissions();
};
} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp
index bdd2c7dc18..d3a7cb9062 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp
@@ -15,9 +15,8 @@
#include "mozStorageCID.h"
#include "mozilla/Components.h"
#include "mozilla/Monitor.h"
-#include "mozilla/AppShutdown.h"
+#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/Services.h"
-#include "mozilla/ShutdownPhase.h"
#include "nsCOMPtr.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIObserverService.h"
@@ -464,19 +463,19 @@ nsresult BounceTrackingProtectionStorage::Init() {
nsresult rv = self->CreateDatabaseConnection();
if (NS_WARN_IF(NS_FAILED(rv))) {
self->mErrored.Flip();
- self->mMonitor.Notify();
+ self->mMonitor.NotifyAll();
return;
}
rv = self->LoadMemoryStateFromDisk();
if (NS_WARN_IF(NS_FAILED(rv))) {
self->mErrored.Flip();
- self->mMonitor.Notify();
+ self->mMonitor.NotifyAll();
return;
}
self->mInitialized.Flip();
- self->mMonitor.Notify();
+ self->mMonitor.NotifyAll();
}),
NS_DISPATCH_EVENT_MAY_BLOCK);
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp
index d4b33f9edb..c522895638 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp
@@ -29,6 +29,8 @@ const nsACString& BounceTrackingRecord::GetFinalHost() const {
}
void BounceTrackingRecord::AddBounceHost(const nsACString& aHost) {
+ MOZ_ASSERT(!aHost.IsEmpty());
+
mBounceHosts.Insert(aHost);
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
("%s: %s", __FUNCTION__, Describe().get()));
@@ -54,6 +56,8 @@ nsCString BounceTrackingRecord::DescribeSet(const nsTHashSet<nsCString>& set) {
}
void BounceTrackingRecord::AddStorageAccessHost(const nsACString& aHost) {
+ MOZ_ASSERT(!aHost.IsEmpty());
+
mStorageAccessHosts.Insert(aHost);
}
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp
index b4af3daa07..08716b44ae 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp
@@ -29,6 +29,7 @@
#include "mozilla/ClearOnShutdown.h"
#include "nsTHashMap.h"
#include "mozilla/dom/Element.h"
+#include "mozilla/dom/WindowContext.h"
namespace mozilla {
@@ -54,8 +55,13 @@ BounceTrackingState::~BounceTrackingState() {
// static
already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate(
- dom::BrowsingContextWebProgress* aWebProgress) {
- MOZ_ASSERT(aWebProgress);
+ dom::BrowsingContextWebProgress* aWebProgress, nsresult& aRv) {
+ aRv = NS_OK;
+
+ if (!aWebProgress) {
+ aRv = NS_ERROR_INVALID_ARG;
+ return nullptr;
+ }
if (!ShouldCreateBounceTrackingStateForWebProgress(aWebProgress)) {
return nullptr;
@@ -73,8 +79,8 @@ already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate(
sStorageObserver = new BounceTrackingStorageObserver();
ClearOnShutdown(&sStorageObserver);
- DebugOnly<nsresult> rv = sStorageObserver->Init();
- NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to init storage observer");
+ aRv = sStorageObserver->Init();
+ NS_ENSURE_SUCCESS(aRv, nullptr);
}
dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext();
@@ -82,7 +88,8 @@ already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate(
return nullptr;
}
uint64_t browserId = browsingContext->BrowserId();
- bool createdNew;
+ bool createdNew = false;
+
RefPtr<BounceTrackingState> bounceTrackingState =
do_AddRef(sBounceTrackingStates->LookupOrInsertWith(browserId, [&] {
createdNew = true;
@@ -90,8 +97,10 @@ already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate(
}));
if (createdNew) {
- nsresult rv = bounceTrackingState->Init(aWebProgress);
- if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv = bounceTrackingState->Init(aWebProgress);
+ if (NS_FAILED(aRv)) {
+ NS_WARNING("Failed to initialize BounceTrackingState.");
+ sBounceTrackingStates->Remove(browserId);
return nullptr;
}
}
@@ -116,6 +125,10 @@ void BounceTrackingState::ResetAllForOriginAttributesPattern(
nsresult BounceTrackingState::Init(
dom::BrowsingContextWebProgress* aWebProgress) {
+ MOZ_ASSERT(!mIsInitialized,
+ "BounceTrackingState must not be initialized twice.");
+ mIsInitialized = true;
+
NS_ENSURE_ARG_POINTER(aWebProgress);
NS_ENSURE_TRUE(
StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(),
@@ -156,9 +169,10 @@ nsCString BounceTrackingState::Describe() {
OriginAttributesRef().CreateSuffix(oaSuffix);
return nsPrintfCString(
- "{ mBounceTrackingRecord: %s, mOriginAttributes: %s }",
+ "{ mBounceTrackingRecord: %s, mOriginAttributes: %s, mBrowserId: %" PRIu64
+ " }",
mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() : "null",
- oaSuffix.get());
+ oaSuffix.get(), mBrowserId);
}
// static
@@ -212,6 +226,28 @@ bool BounceTrackingState::ShouldCreateBounceTrackingStateForWebProgress(
}
// static
+bool BounceTrackingState::ShouldTrackPrincipal(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ // Only track content principals.
+ if (!aPrincipal->GetIsContentPrincipal()) {
+ return false;
+ }
+
+ // Skip non http schemes.
+ if (!aPrincipal->SchemeIs("http") && !aPrincipal->SchemeIs("https")) {
+ return false;
+ }
+
+ // Skip partitioned principals.
+ if (!aPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) {
+ return false;
+ }
+
+ return true;
+}
+
+// static
nsresult BounceTrackingState::HasBounceTrackingStateForSite(
const nsACString& aSiteHost, bool& aResult) {
aResult = false;
@@ -285,6 +321,10 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) {
nsresult rv = aChannel->GetLoadInfo(getter_AddRefs(loadInfo));
NS_ENSURE_SUCCESS(rv, rv);
+ // Used to keep track of whether we added entries to the site list that are
+ // not "null".
+ bool siteListIsEmpty = true;
+
// Collect uri list including any redirects.
nsTArray<nsCString> siteList;
@@ -294,8 +334,8 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) {
rv = redirectHistoryEntry->GetPrincipal(getter_AddRefs(principal));
NS_ENSURE_SUCCESS(rv, rv);
- // Filter out non-content principals.
- if (!principal->GetIsContentPrincipal()) {
+ if (!BounceTrackingState::ShouldTrackPrincipal(principal)) {
+ siteList.AppendElement("null"_ns);
continue;
}
@@ -303,7 +343,12 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) {
rv = principal->GetBaseDomain(baseDomain);
NS_ENSURE_SUCCESS(rv, rv);
- siteList.AppendElement(baseDomain);
+ if (NS_WARN_IF(baseDomain.IsEmpty())) {
+ siteList.AppendElement("null");
+ } else {
+ siteList.AppendElement(baseDomain);
+ siteListIsEmpty = false;
+ }
}
// Add site via the current URI which is the end of the chain.
@@ -311,18 +356,35 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) {
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);
+ if (channelURI->SchemeIs("http") || channelURI->SchemeIs("https")) {
+ nsCOMPtr<nsIEffectiveTLDService> tldService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
- nsAutoCString siteHost;
- rv = tldService->GetSchemelessSite(channelURI, siteHost);
+ nsAutoCString siteHost;
+ rv = tldService->GetSchemelessSite(channelURI, siteHost);
- if (NS_FAILED(rv)) {
- NS_WARNING("Failed to retrieve site for final channel URI.");
+ // Skip URIs where we can't get a site host.
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Failed to get site host from channelURI: %s", __FUNCTION__,
+ channelURI->GetSpecOrDefault().get()));
+ siteList.AppendElement("null"_ns);
+ } else {
+ MOZ_ASSERT(!siteHost.IsEmpty(), "siteHost should not be empty.");
+ siteList.AppendElement(siteHost);
+ siteListIsEmpty = false;
+ }
}
- siteList.AppendElement(siteHost);
+ // Do not record empty site lists. This can happen if none of the principals
+ // are suitable for tracking. It includes when OnDocumentStartRequest is
+ // called for the initial about:blank.
+ if (siteListIsEmpty) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: skip empty site list.", __FUNCTION__));
+ return NS_OK;
+ }
return OnResponseReceived(siteList);
}
@@ -437,16 +499,16 @@ nsresult BounceTrackingState::OnStartNavigation(
// 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))) {
+ // interested in content principals with http/s scheme. Other principal types
+ // or schemes are not considered to be trackers.
+ if (!BounceTrackingState::ShouldTrackPrincipal(aTriggeringPrincipal)) {
siteHost = "";
+ } else {
+ // 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
@@ -474,7 +536,7 @@ nsresult BounceTrackingState::OnStartNavigation(
("%s: site: %s, hasUserActivation? %d", __FUNCTION__, siteHost.get(),
hasUserActivation));
if (hasUserActivation) {
- rv = mBounceTrackingProtection->RecordStatefulBounces(this);
+ nsresult rv = mBounceTrackingProtection->RecordStatefulBounces(this);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(!mBounceTrackingRecord);
@@ -485,7 +547,11 @@ nsresult BounceTrackingState::OnStartNavigation(
}
// There is no transient user activation. Add host as a bounce candidate.
- mBounceTrackingRecord->AddBounceHost(siteHost);
+ if (siteHost.IsEmpty()) {
+ mBounceTrackingRecord->AddBounceHost("null"_ns);
+ } else {
+ mBounceTrackingRecord->AddBounceHost(siteHost);
+ }
return NS_OK;
}
@@ -494,7 +560,12 @@ nsresult BounceTrackingState::OnStartNavigation(
nsresult BounceTrackingState::OnResponseReceived(
const nsTArray<nsCString>& aSiteList) {
- NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE);
+#ifdef DEBUG
+ MOZ_ASSERT(!aSiteList.IsEmpty(), "siteList should not be empty.");
+ for (const nsCString& site : aSiteList) {
+ MOZ_ASSERT(!site.IsEmpty(), "site should not be an empty string.");
+ }
+#endif
// Logging
if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
@@ -510,6 +581,9 @@ nsresult BounceTrackingState::OnResponseReceived(
siteListStr.get()));
}
+ // Record should exist by now. It gets created in OnStartNavigation.
+ NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE);
+
// Check if there is still an active timeout. This shouldn't happen since
// OnStartNavigation already cancels it.
if (NS_WARN_IF(mClientBounceDetectionTimeout)) {
@@ -567,9 +641,6 @@ 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;
@@ -578,14 +649,15 @@ nsresult BounceTrackingState::OnDocumentLoaded(
origin = "err";
}
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
- ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__,
- origin.get(),
- mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get()
- : "null"));
+ ("%s: origin: %s, this: %s", __FUNCTION__, origin.get(),
+ Describe().get()));
}
+ // Assert: navigable’s bounce tracking record is not null.
+ NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE);
+
nsAutoCString siteHost;
- if (!aDocumentPrincipal->GetIsContentPrincipal()) {
+ if (!BounceTrackingState::ShouldTrackPrincipal(aDocumentPrincipal)) {
siteHost = "";
} else {
nsresult rv = aDocumentPrincipal->GetBaseDomain(siteHost);
@@ -614,4 +686,36 @@ nsresult BounceTrackingState::OnCookieWrite(const nsACString& aSiteHost) {
return NS_OK;
}
+nsresult BounceTrackingState::OnStorageAccess(nsIPrincipal* aPrincipal) {
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ // The caller should already filter out principals for us.
+ MOZ_ASSERT(BounceTrackingState::ShouldTrackPrincipal(aPrincipal));
+
+ if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
+ nsAutoCString origin;
+ nsresult rv = aPrincipal->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"));
+ }
+
+ if (!mBounceTrackingRecord) {
+ return NS_OK;
+ }
+
+ nsAutoCString siteHost;
+ nsresult rv = aPrincipal->GetBaseDomain(siteHost);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(!siteHost.IsEmpty(), NS_ERROR_FAILURE);
+
+ mBounceTrackingRecord->AddStorageAccessHost(siteHost);
+
+ return NS_OK;
+}
+
} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h
index 17d324bda9..a9e186206a 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h
@@ -45,7 +45,7 @@ class BounceTrackingState : public nsIWebProgressListener,
// return nullptr if the given web progress / browsing context is not suitable
// (see ShouldCreateBounceTrackingStateForWebProgress).
static already_AddRefed<BounceTrackingState> GetOrCreate(
- dom::BrowsingContextWebProgress* aWebProgress);
+ dom::BrowsingContextWebProgress* aWebProgress, nsresult& aRv);
// Reset state for all BounceTrackingState instances this includes resetting
// BounceTrackingRecords and cancelling any running timers.
@@ -62,31 +62,35 @@ class BounceTrackingState : public nsIWebProgressListener,
// 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);
+ [[nodiscard]] 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);
+ [[nodiscard]] nsresult OnStartNavigation(
+ nsIPrincipal* aTriggeringPrincipal,
+ const bool aHasValidUserGestureActivation);
// Record sites which have written cookies in the current extended
// navigation.
- nsresult OnCookieWrite(const nsACString& aSiteHost);
+ [[nodiscard]] 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);
+ // Whether the given principal should be tracked for bounce tracking.
+ static bool ShouldTrackPrincipal(nsIPrincipal* aPrincipal);
+
// 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);
+ [[nodiscard]] static nsresult HasBounceTrackingStateForSite(
+ const nsACString& aSiteHost, bool& aResult);
// Get the currently associated BrowsingContext. Returns nullptr if it has not
// been attached yet.
@@ -99,10 +103,16 @@ class BounceTrackingState : public nsIWebProgressListener,
// Create a string that describes this object. Used for logging.
nsCString Describe();
+ // Record sites which have accessed storage in the current extended
+ // navigation.
+ [[nodiscard]] nsresult OnStorageAccess(nsIPrincipal* aPrincipal);
+
private:
explicit BounceTrackingState();
virtual ~BounceTrackingState();
+ bool mIsInitialized{false};
+
uint64_t mBrowserId{};
// OriginAttributes associated with the browser this state is attached to.
@@ -130,25 +140,20 @@ class BounceTrackingState : public nsIWebProgressListener,
dom::BrowsingContextWebProgress* aWebProgress);
// Init to be called after creation, attaches nsIWebProgressListener.
- nsresult Init(dom::BrowsingContextWebProgress* aWebProgress);
+ [[nodiscard]] 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);
+ [[nodiscard]] 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();
+ [[nodiscard]] nsresult OnDocumentLoaded(nsIPrincipal* aDocumentPrincipal);
// Record sites which have activated service workers in the current
// extended navigation.
- nsresult OnServiceWorkerActivation();
+ [[nodiscard]] nsresult OnServiceWorkerActivation();
};
} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp
index b94c887c90..bcd9e57bd8 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp
@@ -9,6 +9,7 @@
#include "ErrorList.h"
#include "mozilla/Assertions.h"
#include "mozilla/Logging.h"
+#include "mozilla/IntegerPrintfMacros.h"
#include "nsIPrincipal.h"
namespace mozilla {
@@ -42,6 +43,19 @@ nsresult BounceTrackingStateGlobal::RecordUserActivation(
__FUNCTION__, PromiseFlatCString(aSiteHost).get()));
}
+ // Make sure we don't overwrite an existing, more recent user activation. This
+ // is only relevant for callers that pass in a timestamp that isn't PR_Now(),
+ // e.g. when importing user activation data.
+ Maybe<PRTime> existingUserActivation = mUserActivation.MaybeGet(aSiteHost);
+ if (existingUserActivation.isSome() &&
+ existingUserActivation.value() >= aTime) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
+ ("%s: Skip: A more recent user activation "
+ "already exists for %s",
+ __FUNCTION__, PromiseFlatCString(aSiteHost).get()));
+ return NS_OK;
+ }
+
mUserActivation.InsertOrUpdate(aSiteHost, aTime);
if (aSkipStorage || !ShouldPersistToDisk()) {
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp
index cc9c3ce971..fcc806e0f4 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp
@@ -6,14 +6,15 @@
#include "BounceTrackingState.h"
#include "mozilla/Services.h"
-#include "mozilla/StaticPrefs_privacy.h"
-#include "mozilla/ClearOnShutdown.h"
-#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/dom/WindowGlobalParent.h"
#include "nsCOMPtr.h"
#include "nsICookieNotification.h"
#include "nsIObserverService.h"
#include "mozilla/dom/BrowsingContext.h"
#include "nsICookie.h"
+#include "nsIPrincipal.h"
namespace mozilla {
@@ -69,13 +70,25 @@ BounceTrackingStorageObserver::Observe(nsISupports* aSubject,
return NS_OK;
}
- // Check if the cookie is partitioned. Partitioned cookies can not be used for
- // bounce tracking.
+ // Filter http(s) cookies
nsCOMPtr<nsICookie> cookie;
rv = notification->GetCookie(getter_AddRefs(cookie));
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(cookie);
+ nsICookie::schemeType schemeMap;
+ rv = cookie->GetSchemeMap(&schemeMap);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!(schemeMap & (nsICookie::schemeType::SCHEME_HTTP |
+ nsICookie::schemeType::SCHEME_HTTPS))) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("Skipping non-HTTP(S) cookie."));
+ return NS_OK;
+ }
+
+ // Check if the cookie is partitioned. Partitioned cookies can not be used for
+ // bounce tracking.
if (!cookie->OriginAttributesNative().mPartitionKey.IsEmpty()) {
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
("Skipping partitioned cookie."));
@@ -104,4 +117,65 @@ BounceTrackingStorageObserver::Observe(nsISupports* aSubject,
return bounceTrackingState->OnCookieWrite(baseDomain);
}
+// static
+nsresult BounceTrackingStorageObserver::OnInitialStorageAccess(
+ dom::WindowContext* aWindowContext) {
+ NS_ENSURE_ARG_POINTER(aWindowContext);
+
+ if (!XRE_IsParentProcess()) {
+ // Check if the principal needs to be tracked for bounce tracking. Checking
+ // this in the content process may save us IPC to the parent.
+ nsIPrincipal* storagePrincipal =
+ aWindowContext->GetInnerWindow()->GetEffectiveStoragePrincipal();
+ if (!BounceTrackingState::ShouldTrackPrincipal(storagePrincipal)) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("%s: Skipping principal (content process).", __FUNCTION__));
+ return NS_OK;
+ }
+
+ dom::WindowGlobalChild* windowGlobalChild =
+ aWindowContext->GetWindowGlobalChild();
+ NS_ENSURE_TRUE(windowGlobalChild, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(windowGlobalChild->SendOnInitialStorageAccess(),
+ NS_ERROR_FAILURE);
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIPrincipal> storagePrincipal =
+ aWindowContext->Canonical()->DocumentStoragePrincipal();
+ NS_ENSURE_TRUE(storagePrincipal, NS_ERROR_FAILURE);
+
+ if (!BounceTrackingState::ShouldTrackPrincipal(storagePrincipal)) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("%s: Skipping principal.", __FUNCTION__));
+ return NS_OK;
+ }
+
+ if (!storagePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) {
+ MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose,
+ ("Skipping partitioned storage access."));
+ return NS_OK;
+ }
+
+ dom::BrowsingContext* browsingContext = aWindowContext->GetBrowsingContext();
+ NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE);
+
+ nsresult rv = NS_OK;
+ RefPtr<BounceTrackingState> bounceTrackingState =
+ BounceTrackingState::GetOrCreate(
+ browsingContext->Top()->Canonical()->GetWebProgress(), rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We may not always get a BounceTrackingState, e.g. if the feature is
+ // disabled or we don't keep track of bounce tracking for the given
+ // BrowsingContext.
+ if (!bounceTrackingState) {
+ return NS_OK;
+ }
+
+ return bounceTrackingState->OnStorageAccess(storagePrincipal);
+}
+
} // namespace mozilla
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h
index 1e76c85a3c..50fdc79c64 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h
+++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h
@@ -9,6 +9,10 @@
namespace mozilla {
+namespace dom {
+class WindowContext;
+}
+
extern LazyLogModule gBounceTrackingProtectionLog;
class BounceTrackingStorageObserver final : public nsIObserver {
@@ -17,7 +21,10 @@ class BounceTrackingStorageObserver final : public nsIObserver {
public:
BounceTrackingStorageObserver() = default;
- nsresult Init();
+ [[nodiscard]] nsresult Init();
+
+ [[nodiscard]] static nsresult OnInitialStorageAccess(
+ dom::WindowContext* aWindowContext);
private:
~BounceTrackingStorageObserver() = default;
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml b/toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml
new file mode 100644
index 0000000000..a298fa6d4c
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml
@@ -0,0 +1,64 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: Privacy: Anti-Tracking'
+
+bounce.tracking.protection:
+ purge_duration:
+ type: timing_distribution
+ description: >
+ For every purge that is scheduled, we call the ClearDataService to
+ purge persistent storage for each detected bounce tracker. This may
+ do some blocking work on main thread and dispatch some cleanups to
+ other threads.
+ Collect telemetry on how long it takes to clear in the wild to
+ determine whether we need to improve performance here.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1890582
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1890582#c4
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pbz@mozilla.com
+ - bvandersloot@mozilla.com
+ - manuel@mozilla.com
+ expires: 130
+ enabled_at_startup:
+ type: boolean
+ description: >
+ Keeps track of whether the feature is enabled at startup.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964#c4
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pbz@mozilla.com
+ - bvandersloot@mozilla.com
+ - manuel@mozilla.com
+ expires: never
+ enabled_dry_run_mode_at_startup:
+ type: boolean
+ description: >
+ Keeps track of whether the feature is enabled and running in dry-run mode
+ at startup.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964#c4
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - pbz@mozilla.com
+ - bvandersloot@mozilla.com
+ - manuel@mozilla.com
+ expires: never
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/moz.build b/toolkit/components/antitracking/bouncetrackingprotection/moz.build
index 09107bb782..cc35d81bd9 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/moz.build
+++ b/toolkit/components/antitracking/bouncetrackingprotection/moz.build
@@ -8,6 +8,7 @@ with Files("**"):
BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking")
XPIDL_SOURCES += [
+ "nsIBounceTrackingMapEntry.idl",
"nsIBounceTrackingProtection.idl",
]
@@ -19,6 +20,7 @@ XPCOM_MANIFESTS += [
]
EXPORTS.mozilla += [
+ "BounceTrackingMapEntry.h",
"BounceTrackingProtection.h",
"BounceTrackingProtectionStorage.h",
"BounceTrackingRecord.h",
@@ -28,6 +30,7 @@ EXPORTS.mozilla += [
]
UNIFIED_SOURCES += [
+ "BounceTrackingMapEntry.cpp",
"BounceTrackingProtection.cpp",
"BounceTrackingProtectionStorage.cpp",
"BounceTrackingRecord.cpp",
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingMapEntry.idl b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingMapEntry.idl
new file mode 100644
index 0000000000..2e2f1823ab
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingMapEntry.idl
@@ -0,0 +1,17 @@
+/* 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"
+
+/**
+ * This interface represents an entry in the global bounce tracker or user activation map.
+ */
+[scriptable, uuid(51B0B5AE-0AC2-4A3C-8C7E-3523FA42881B)]
+interface nsIBounceTrackingMapEntry : nsISupports {
+ // The host of the site that has received user activation or is a bounce tracker candidate.
+ readonly attribute ACString siteHost;
+ // The time when the user activation or bounce tracker candidate was added.
+ // This is a PRTime, which is USEC since the epoch.
+ readonly attribute int64_t timeStamp;
+};
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl
index 1163492333..d52113118e 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl
+++ b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl
@@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
+#include "nsIBounceTrackingMapEntry.idl"
[scriptable, uuid(4866F748-29DA-4C10-8EAA-ED2F7851E6B1)]
interface nsIBounceTrackingProtection : nsISupports {
@@ -37,14 +38,20 @@ interface nsIBounceTrackingProtection : nsISupports {
// State is keyed by OriginAttributes.
[implicit_jscontext]
- Array<ACString> testGetBounceTrackerCandidateHosts(in jsval originAttributes);
+ Array<nsIBounceTrackingMapEntry> testGetBounceTrackerCandidateHosts(in jsval originAttributes);
[implicit_jscontext]
- Array<ACString> testGetUserActivationHosts(in jsval originAttributes);
+ Array<nsIBounceTrackingMapEntry> 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);
+
+ // Test helper to trigger user activation import from the permission
+ // manager. Will only import if the pref
+ // privacy.bounceTrackingProtection.hasMigratedUserActivationData is set to
+ // false.
+ void testMaybeMigrateUserInteractionPermissions();
};
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml
index 1c44d7804e..0e8a01db4a 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml
@@ -4,17 +4,31 @@ prefs = [
"privacy.bounceTrackingProtection.enabled=true",
"privacy.bounceTrackingProtection.enableTestMode=true",
"privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0",
+ "privacy.bounceTrackingProtection.enableDryRunMode=false",
]
support-files = [
"file_start.html",
"file_bounce.sjs",
"file_bounce.html",
+ "file_web_worker.js",
]
+["browser_bouncetracking_dry_run.js"]
+
["browser_bouncetracking_oa_isolation.js"]
+["browser_bouncetracking_popup.js"]
+
["browser_bouncetracking_purge.js"]
+["browser_bouncetracking_schemes.js"]
+
["browser_bouncetracking_simple.js"]
-["browser_bouncetracking_stateful.js"]
+["browser_bouncetracking_stateful_cookies.js"]
+
+["browser_bouncetracking_stateful_storage.js"]
+
+["browser_bouncetracking_stateful_web_worker.js"]
+
+["browser_bouncetracking_telemetry_purge_duration.js"]
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js
new file mode 100644
index 0000000000..627a5f233a
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_ORIGIN = "https://itisatracker.org";
+const TEST_BASE_DOMAIN = "itisatracker.org";
+
+async function runPurgeTest(expectPurge) {
+ ok(!SiteDataTestUtils.hasCookies(TEST_ORIGIN), "No cookies initially.");
+
+ // Adding a cookie that should later be purged.
+ info("Add a test cookie to be purged later.");
+ SiteDataTestUtils.addToCookies({ origin: TEST_ORIGIN });
+ ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookie added.");
+
+ // The bounce adds localStorage. Test that there is none initially.
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN),
+ "No localStorage initially."
+ );
+
+ info("Test client bounce with cookie.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "localStorage",
+ skipSiteDataCleanup: true,
+ postBounceCallback: () => {
+ info(
+ "Test that after the bounce but before purging cookies and localStorage are present."
+ );
+ ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies not purged.");
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN),
+ "localStorage not purged."
+ );
+
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost),
+ [TEST_BASE_DOMAIN],
+ `Bounce tracker candidate '${TEST_BASE_DOMAIN}' added`
+ );
+ },
+ });
+
+ if (expectPurge) {
+ info("After purging the site shouldn't have any data.");
+ ok(!SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies purged.");
+ ok(!SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), "localStorage purged.");
+ } else {
+ info("Purging did not run meaning the site should still have data.");
+
+ ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies still set.");
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN),
+ "localStorage still set."
+ );
+ }
+
+ info(
+ "Candidates should have been removed after running the purging algorithm. This is true for both regular and dry-run mode where we pretend to purge."
+ );
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}),
+ [],
+ "No bounce tracker candidates after purging."
+ );
+
+ // Cleanup.
+ bounceTrackingProtection.clearAll();
+ await SiteDataTestUtils.clear();
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.bounceTrackingProtection.requireStatefulBounces", true],
+ ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0],
+ // Required to use SiteDataTestUtils localStorage helpers.
+ ["dom.storage.client_validation", false],
+ ],
+ });
+});
+
+add_task(async function test_purge_in_regular_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.bounceTrackingProtection.enableDryRunMode", false]],
+ });
+
+ await runPurgeTest(true);
+});
+
+add_task(async function test_purge_in_dry_run_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.bounceTrackingProtection.enableDryRunMode", true]],
+ });
+
+ await runPurgeTest(false);
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js
new file mode 100644
index 0000000000..9e6fa8caf1
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://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);
+});
+
+async function runTest(spawnWindowType) {
+ if (!spawnWindowType || !["newTab", "popup"].includes(spawnWindowType)) {
+ throw new Error(`Invalid option '${spawnWindowType}' for spawnWindowType`);
+ }
+
+ Assert.equal(
+ bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length,
+ 0,
+ "No bounce tracker hosts initially."
+ );
+ Assert.equal(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activation hosts initially."
+ );
+
+ // Spawn a tab with A, the start of the bounce chain.
+ await BrowserTestUtils.withNewTab(
+ getBaseUrl(ORIGIN_A) + "file_start.html",
+ async browser => {
+ // The destination site C to navigate to after the bounce.
+ let finalURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html");
+ // The middle hop in the bounce chain B that redirects to finalURL C.
+ let bounceURL = getBounceURL({
+ bounceType: "client",
+ targetURL: finalURL,
+ setState: "cookie-client",
+ });
+
+ // Register a promise for the new popup window. This resolves once the popup
+ // has opened and the final url (C) has been loaded.
+ let openPromise;
+
+ if (spawnWindowType == "newTab") {
+ openPromise = BrowserTestUtils.waitForNewTab(gBrowser, finalURL.href);
+ } else {
+ openPromise = BrowserTestUtils.waitForNewWindow({ url: finalURL.href });
+ }
+
+ // Navigate through the bounce chain by opening a popup to the bounce URL.
+ await navigateLinkClick(browser, bounceURL, {
+ spawnWindow: spawnWindowType,
+ });
+
+ let tabOrWindow = await openPromise;
+
+ let tabOrWindowBrowser;
+ if (spawnWindowType == "newTab") {
+ tabOrWindowBrowser = tabOrWindow.linkedBrowser;
+ } else {
+ tabOrWindowBrowser = tabOrWindow.gBrowser.selectedBrowser;
+ }
+
+ let promiseRecordBounces = waitForRecordBounces(tabOrWindowBrowser);
+
+ // 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(
+ tabOrWindowBrowser,
+ new URL(getBaseUrl(ORIGIN_C) + "file_start.html")
+ );
+
+ info("Wait for bounce trackers to be recorded.");
+ await promiseRecordBounces;
+
+ // Cleanup popup or tab.
+ if (spawnWindowType == "newTab") {
+ await BrowserTestUtils.removeTab(tabOrWindow);
+ } else {
+ await BrowserTestUtils.closeWindow(tabOrWindow);
+ }
+ }
+ );
+
+ // Check that the bounce tracker was detected.
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost),
+ [SITE_TRACKER],
+ "Bounce tracker in popup detected."
+ );
+
+ // Cleanup.
+ bounceTrackingProtection.clearAll();
+ await SiteDataTestUtils.clear();
+}
+
+/**
+ * Tests that bounce trackers which use popups as the first hop in the bounce
+ * chain can not bypass detection.
+ *
+ * A -> popup -> B -> C
+ *
+ * A opens a popup and loads B in it. B is the tracker that performs a
+ * short-lived redirect and C is the final destination.
+ */
+
+add_task(async function test_popup() {
+ await runTest("popup");
+});
+
+add_task(async function test_new_tab() {
+ await runTest("newTab");
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js
index eedd374197..676ac5fe92 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js
@@ -133,8 +133,21 @@ add_task(async function test_purging_skip_content_blocking_allow_list() {
"Should only purge example.net. example.org is within the grace period, example.com is allow-listed."
);
+ // example.net is removed because it is purged, example.com is removed because
+ // it is allow-listed.
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost),
+ ["example.org"],
+ "Should have removed example.net and example.com from bounce tracker candidate list."
+ );
+
+ info("Add example.com as a bounce tracker candidate again.");
+ bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1);
+
info(
- "Remove the allow-list entry for example.com and test that it gets purged now."
+ "Remove the allow-list entry for example.com and test that it gets purged now."
);
await BrowserTestUtils.withNewTab("https://example.com", async browser => {
@@ -146,6 +159,16 @@ add_task(async function test_purging_skip_content_blocking_allow_list() {
"example.com should have been purged now that it is no longer allow-listed."
);
+ // example.org is still in the grace period so it neither gets purged nor
+ // removed from the candidate list.
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost),
+ ["example.org"],
+ "Should have removed example.com from bounce tracker candidate list."
+ );
+
bounceTrackingProtection.clearAll();
});
@@ -166,8 +189,24 @@ add_task(
"Should only purge example.net. example.org is within the grace period, example.com is allow-listed via test1.example.com."
);
+ // example.net is removed because it is purged, example.com is removed because it is allow-listed.
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost),
+ ["example.org"],
+ "Should have removed example.net and example.com from bounce tracker candidate list."
+ );
+
+ info("Add example.com as a bounce tracker candidate again.");
+ bounceTrackingProtection.testAddBounceTrackerCandidate(
+ {},
+ "example.com",
+ 1
+ );
+
info(
- "Remove the allow-list entry for test1.example.com and test that it gets purged now."
+ "Remove the allow-list entry for test1.example.com and test that it gets purged now."
);
await BrowserTestUtils.withNewTab(
@@ -179,7 +218,7 @@ add_task(
Assert.deepEqual(
await bounceTrackingProtection.testRunPurgeBounceTrackers(),
["example.com"],
- "example.com should have been purged now that test1.example.com it is no longer allow-listed."
+ "example.com should have been purged now that test1.example.com is no longer allow-listed."
);
bounceTrackingProtection.clearAll();
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js
new file mode 100644
index 0000000000..21b72cdc45
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let bounceTrackingProtection;
+
+add_setup(async function () {
+ bounceTrackingProtection = Cc[
+ "@mozilla.org/bounce-tracking-protection;1"
+ ].getService(Ci.nsIBounceTrackingProtection);
+ bounceTrackingProtection.clearAll();
+});
+
+async function testInteractWithSite(origin, expectRecorded) {
+ is(
+ bounceTrackingProtection.testGetUserActivationHosts({}).length,
+ 0,
+ "No user activation hosts initially"
+ );
+
+ let baseDomain;
+ let scheme;
+
+ await BrowserTestUtils.withNewTab(origin, async browser => {
+ baseDomain = browser.contentPrincipal.baseDomain;
+ scheme = browser.contentPrincipal.URI.scheme;
+
+ info(
+ `Trigger a user activation, which should ${
+ expectRecorded ? "" : "not "
+ }be recorded.`
+ );
+ // We intentionally turn off this a11y check, because the following click
+ // is purposefully sent on an arbitrary web content that is not expected
+ // to be tested by itself with the browser mochitests, therefore this rule
+ // check shall be ignored by a11y_checks suite.
+ AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
+ await BrowserTestUtils.synthesizeMouseAtPoint(50, 50, {}, browser);
+ AccessibilityUtils.resetEnv();
+ });
+ if (expectRecorded) {
+ Assert.deepEqual(
+ bounceTrackingProtection
+ .testGetUserActivationHosts({})
+ .map(entry => entry.siteHost),
+ [baseDomain],
+ `User activation should be recorded for ${scheme} scheme.`
+ );
+ } else {
+ Assert.deepEqual(
+ bounceTrackingProtection.testGetUserActivationHosts({}),
+ [],
+ `User activation should not be recorded for ${scheme} scheme.`
+ );
+ }
+
+ bounceTrackingProtection.clearAll();
+}
+
+/**
+ * Test that we only record user activation for supported schemes.
+ */
+add_task(async function test_userActivationSchemes() {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await testInteractWithSite("http://example.com", true);
+ await testInteractWithSite("https://example.com", true);
+
+ await testInteractWithSite("about:blank", false);
+ await testInteractWithSite("about:robots", false);
+ await testInteractWithSite(
+ "file://" + Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ false
+ );
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js
index dfbd4d0fc0..cdc21eb788 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js
@@ -73,12 +73,18 @@ add_task(async function test_bounce_chain() {
await promiseRecordBounces;
Assert.deepEqual(
- bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(),
+ bounceTrackingProtection
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
[SITE_TRACKER_B, SITE_TRACKER].sort(),
`Identified all bounce trackers in the redirect chain.`
);
Assert.deepEqual(
- bounceTrackingProtection.testGetUserActivationHosts({}).sort(),
+ bounceTrackingProtection
+ .testGetUserActivationHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
[SITE_A, SITE_B].sort(),
"Should only have user activation for sites where we clicked links."
);
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js
index e7fb4521a7..d4940e668d 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js
@@ -34,6 +34,15 @@ add_task(async function test_bounce_stateful_cookies_client() {
});
});
+add_task(async function test_bounce_stateful_cookies_client_sameSiteFrame() {
+ info("Test client bounce with cookie set in same site frame.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "cookie-client",
+ setStateSameSiteFrame: true,
+ });
+});
+
add_task(async function test_bounce_stateful_cookies_server() {
info("Test server bounce with cookie.");
await runTestBounce({
@@ -49,15 +58,11 @@ add_task(async function test_bounce_stateful_cookies_server() {
});
});
-// 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.");
+add_task(async function test_bounce_stateful_cookies_server_sameSiteFrame() {
+ info("Test client bounce with cookie set in same site frame.");
await runTestBounce({
- bounceType: "client",
- setState: "localStorage",
- expectCandidate: false,
- expectPurge: false,
+ bounceType: "server",
+ setState: "cookie-server",
+ setStateSameSiteFrame: true,
});
});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js
new file mode 100644
index 0000000000..ff9daabcdb
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js
@@ -0,0 +1,54 @@
+/* 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);
+});
+
+// Storage tests.
+
+add_task(async function test_bounce_stateful_localStorage() {
+ info("Client bounce with localStorage.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "localStorage",
+ });
+});
+
+add_task(async function test_bounce_stateful_localStorage_sameSiteFrame() {
+ info("Client bounce with localStorage set in same site frame.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "localStorage",
+ setStateSameSiteFrame: true,
+ });
+});
+
+add_task(async function test_bounce_stateful_indexedDB() {
+ info("Client bounce with indexedDB.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "indexedDB",
+ });
+});
+
+add_task(async function test_bounce_stateful_indexedDB_sameSiteFrame() {
+ info("Client bounce with indexedDB populated in same site frame.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "indexedDB",
+ setStateSameSiteFrame: true,
+ });
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js
new file mode 100644
index 0000000000..1e6672e784
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js
@@ -0,0 +1,38 @@
+/* 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);
+});
+
+add_task(async function test_bounce_stateful_indexedDB() {
+ info("Client bounce with indexedDB.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "indexedDB",
+ setStateInWebWorker: true,
+ });
+});
+
+// FIXME: (Bug 1889898) This test is skipped because it triggers a shutdown
+// hang.
+add_task(async function test_bounce_stateful_indexedDB_nestedWorker() {
+ info("Client bounce with indexedDB access from a nested worker.");
+ await runTestBounce({
+ bounceType: "client",
+ setState: "indexedDB",
+ setStateInNestedWebWorker: true,
+ });
+}).skip();
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js
new file mode 100644
index 0000000000..74b1fdb30d
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let bounceTrackingProtection;
+
+async function test_purge_duration(isDryRunMode) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.bounceTrackingProtection.enableDryRunMode", isDryRunMode]],
+ });
+
+ is(
+ Glean.bounceTrackingProtection.purgeDuration.testGetValue(),
+ null,
+ "Histogram should not exist initially."
+ );
+
+ info("Run server bounce with cookie.");
+ await runTestBounce({
+ bounceType: "server",
+ setState: "cookie-server",
+ postBounceCallback: () => {
+ is(
+ Glean.bounceTrackingProtection.purgeDuration.testGetValue(),
+ null,
+ "Histogram should still be empty after bounce, because we haven't purged yet."
+ );
+ },
+ });
+
+ let events = Glean.bounceTrackingProtection.purgeDuration.testGetValue();
+ if (isDryRunMode) {
+ is(events, null, "Should not collect purge timining in dry mode");
+ } else {
+ is(events.count, 1, "Histogram should contain one value.");
+ }
+
+ // Cleanup
+ Services.fog.testResetFOG();
+ await SpecialPowers.popPrefEnv();
+ bounceTrackingProtection.clearAll();
+}
+
+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);
+
+ // Clear telemetry before test.
+ Services.fog.testResetFOG();
+});
+
+add_task(async function test_purge_duration_dry_mode() {
+ await test_purge_duration(true);
+});
+
+add_task(async function test_purge_duration_enabled() {
+ await test_purge_duration(false);
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html
index 2756555fa5..d7aa117481 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html
@@ -1,59 +1,150 @@
<!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;
- }
+<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 id="test-config"></p>
+ <script>
+ const SET_STATE_HANDLERS = {
+ "cookie-client": setCookie,
+ "localStorage": setLocalStorage,
+ "indexedDB": setIndexedDB,
+ };
+
+ function setCookie(id) {
+ 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;
+ }
+ }
+
+ function setLocalStorage(id) {
+ 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);
+ }
+ }
+
+ function setIndexedDB() {
+ return new Promise((resolve, reject) => {
+ let request = window.indexedDB.open("bounce", 1);
+ request.onsuccess = () => {
+ console.info("Opened indexedDB");
+ resolve()
+ };
+ request.onerror = (event) => {
+ console.error("Error opening indexedDB", event);
+ reject();
+ };
+ request.onupgradeneeded = (event) => {
+ console.info("Initializing indexedDB");
+ let db = event.target.result;
+ db.createObjectStore("bounce");
+ };
+ });
+ }
+
+ function setIndexedDBInWorker(nested = false) {
+ let worker = new Worker("file_web_worker.js");
+ let msg = nested ? "setIndexedDBNested" : "setIndexedDB";
+ worker.postMessage(msg);
+ return new Promise((resolve, reject) => {
+ worker.onmessage = () => {
+ console.info("IndexedDB set in worker");
+ resolve();
+ };
+ worker.onerror = (event) => {
+ console.error("Error setting indexedDB in worker", event);
+ reject();
+ };
+ });
+ }
+
+ /**
+ * Set a state in a child frame.
+ */
+ function setStateInFrame() {
+ // Embed self
+ let iframe = document.createElement("iframe");
+
+ let src = new URL(location.href);
+ // Remove search params we don't need for the iframe.
+ src.searchParams.delete("target");
+ src.searchParams.delete("redirectDelay");
+ src.searchParams.delete("setStateSameSiteFrame");
+ iframe.src = src.href;
+
+ let frameReadyPromise = new Promise((resolve) => {
+ iframe.addEventListener("load", () => {
+ iframe.contentWindow.readyPromise.then(resolve);
+ });
+ });
+ document.body.appendChild(iframe);
+
+ return frameReadyPromise;
+ }
+
+ // Wrap the entire block so we can run async code. Store the result in a
+ // promise so that parent windows can wait for us to be ready.
+ window.readyPromise = (async () => {
+ let url = new URL(location.href);
+ // Display the test config in the body.
+ document.getElementById("test-config").innerText = JSON.stringify(Object.fromEntries(url.searchParams), null, 2);
+
+ if (url.searchParams.get("setStateSameSiteFrame") === "true") {
+ // Set state in a child frame.
+ await setStateInFrame(url);
+ } else if(url.searchParams.get("setStateInWebWorker") === "true") {
+ // Set state in a worker.
+ await setIndexedDBInWorker();
+ } else if(url.searchParams.get("setStateInNestedWebWorker") === "true") {
+ // Set state in a nested worker.
+ await setIndexedDBInWorker(true);
+ } else {
+ // Set a state in this window.
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 handler = SET_STATE_HANDLERS[setState];
+ if (!handler) {
+ throw new Error("Unknown state handler: " + setState);
}
+ await handler(id);
}
+ }
+
+ // Redirect to the target URL after a delay.
+ // If no target is specified, do nothing.
+ let redirectDelay = url.searchParams.get("redirectDelay");
+ if (redirectDelay != null) {
+ redirectDelay = Number.parseInt(redirectDelay);
+ } else {
+ redirectDelay = 50;
+ }
+
+ let target = url.searchParams.get("target");
+ if (target) {
+ console.info("Redirecting to", target);
+ setTimeout(() => {
+ location.href = target;
+ }, redirectDelay);
+ }
+ })();
+ </script>
+</body>
- 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_web_worker.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js
new file mode 100644
index 0000000000..78fdf2a149
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js
@@ -0,0 +1,40 @@
+// A web worker which can set indexedDB.
+
+function setIndexedDB() {
+ return new Promise((resolve, reject) => {
+ let request = self.indexedDB.open("bounce", 1);
+ request.onsuccess = () => {
+ console.info("Opened indexedDB");
+ resolve();
+ };
+ request.onerror = event => {
+ console.error("Error opening indexedDB", event);
+ reject();
+ };
+ request.onupgradeneeded = event => {
+ console.info("Initializing indexedDB");
+ let db = event.target.result;
+ db.createObjectStore("bounce");
+ };
+ });
+}
+
+self.onmessage = function (event) {
+ console.info("Web worker received message", event.data);
+
+ if (event.data === "setIndexedDB") {
+ setIndexedDB().then(() => {
+ self.postMessage("indexedDBSet");
+ });
+ } else if (event.data === "setIndexedDBNested") {
+ console.info("set state nested");
+ // Rather than setting indexedDB in this worker spawn a nested worker to set
+ // indexedDB.
+ let nestedWorker = new Worker("file_web_worker.js");
+ nestedWorker.postMessage("setIndexedDB");
+ nestedWorker.onmessage = () => {
+ console.info("IndexedDB set in nested worker");
+ self.postMessage("indexedDBSet");
+ };
+ }
+};
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js
index f5857b6919..71d9acedc6 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js
@@ -3,6 +3,17 @@
"use strict";
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "bounceTrackingProtection",
+ "@mozilla.org/bounce-tracking-protection;1",
+ "nsIBounceTrackingProtection"
+);
+
const SITE_A = "example.com";
const ORIGIN_A = `https://${SITE_A}`;
@@ -25,13 +36,6 @@ 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.
@@ -48,8 +52,14 @@ function getBaseUrl(origin) {
* 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 {('cookie-server'|'cookie-client'|'localStorage')} [options.setState]
+ * Type of state to set during the redirect. Defaults to non stateful redirect.
+ * @param {boolean} [options.setStateSameSiteFrame=false] - Whether to set the
+ * state in a sub frame that is same site to the top window.
+ * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the
+ * state in a web worker. This only supports setState == "indexedDB".
+ * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the
+ * state in a nested web worker. Otherwise the same as setStateInWebWorker.
* @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
@@ -62,6 +72,9 @@ function getBounceURL({
bounceOrigin = ORIGIN_TRACKER,
targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"),
setState = null,
+ setStateSameSiteFrame = false,
+ setStateInWebWorker = false,
+ setStateInNestedWebWorker = false,
statusCode = 302,
redirectDelayMS = 50,
}) {
@@ -79,6 +92,25 @@ function getBounceURL({
if (setState) {
searchParams.set("setState", setState);
}
+ if (setStateSameSiteFrame) {
+ searchParams.set("setStateSameSiteFrame", setStateSameSiteFrame);
+ }
+ if (setStateInWebWorker) {
+ if (setState != "indexedDB") {
+ throw new Error(
+ "setStateInWebWorker only supports setState == 'indexedDB'"
+ );
+ }
+ searchParams.set("setStateInWebWorker", setStateInWebWorker);
+ }
+ if (setStateInNestedWebWorker) {
+ if (setState != "indexedDB") {
+ throw new Error(
+ "setStateInNestedWebWorker only supports setState == 'indexedDB'"
+ );
+ }
+ searchParams.set("setStateInNestedWebWorker", setStateInNestedWebWorker);
+ }
if (bounceType == "server") {
searchParams.set("statusCode", statusCode);
@@ -94,23 +126,58 @@ function getBounceURL({
* click on it.
* @param {MozBrowser} browser - Browser to insert the link in.
* @param {URL} targetURL - Destination for navigation.
+ * @param {Object} options - Additional options.
+ * @param {string} [options.spawnWindow] - If set to "newTab" or "popup" the
+ * link will be opened in a new tab or popup window respectively. If unset the
+ * link is opened in the given browser.
* @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);
- });
+async function navigateLinkClick(
+ browser,
+ targetURL,
+ { spawnWindow = null } = {}
+) {
+ if (spawnWindow && !["newTab", "popup"].includes(spawnWindow)) {
+ throw new Error(`Invalid option '${spawnWindow}' for spawnWindow`);
+ }
- await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser);
+ await SpecialPowers.spawn(
+ browser,
+ [targetURL.href, spawnWindow],
+ async (targetURL, spawnWindow) => {
+ let link = content.document.createElement("a");
+ link.id = "link";
+ link.textContent = "Click Me";
+ link.style.display = "block";
+ link.style.fontSize = "40px";
+
+ // For opening a popup we attach an event listener to trigger via click.
+ if (spawnWindow) {
+ link.href = "#";
+ link.addEventListener("click", event => {
+ event.preventDefault();
+ if (spawnWindow == "newTab") {
+ // Open a new tab.
+ content.window.open(targetURL, "bounce");
+ } else {
+ // Open a popup window.
+ content.window.open(targetURL, "bounce", "height=200,width=200");
+ }
+ });
+ } else {
+ // For regular navigation add href and click.
+ link.href = targetURL;
+ }
+
+ content.document.body.appendChild(link);
+
+ // TODO: Bug 1892091: Use EventUtils.synthesizeMouse instead for a real click.
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ content.document.userInteractionForTesting();
+ link.click();
+ }
+ );
}
/**
@@ -141,25 +208,38 @@ async function waitForRecordBounces(browser) {
* 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 {boolean} [options.setStateSameSiteFrame=false] - Whether to set the
+ * state in a sub frame that is same site to the top window.
+ * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the
+ * state in a web worker. This only supports setState == "indexedDB".
+ * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the
+ * state in a nested web worker. Otherwise the same as setStateInWebWorker.
+ * @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.
+ * 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.
+ * @param {boolean} [options.skipSiteDataCleanup=false] - Skip the cleanup of
+ * site data after the test. When this is enabled the caller is responsible for
+ * cleaning up site data.
*/
async function runTestBounce(options = {}) {
let {
bounceType,
setState = null,
+ setStateSameSiteFrame = false,
+ setStateInWebWorker = false,
+ setStateInNestedWebWorker = false,
expectCandidate = true,
expectPurge = true,
originAttributes = {},
postBounceCallback = () => {},
+ skipSiteDataCleanup = false,
} = options;
info(`runTestBounce ${JSON.stringify(options)}`);
@@ -191,28 +271,42 @@ async function runTestBounce(options = {}) {
win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
}
- let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", {
+ let initialURL = getBaseUrl(ORIGIN_A) + "file_start.html";
+ let tab = win.gBrowser.addTab(initialURL, {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
userContextId,
});
win.gBrowser.selectedTab = tab;
let browser = tab.linkedBrowser;
- await BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.browserLoaded(browser, true, initialURL);
let promiseRecordBounces = waitForRecordBounces(browser);
// The final destination after the bounce.
let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html");
+ // Wait for the final site to be loaded which complete the BounceTrackingRecord.
+ let targetURLLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ targetURL
+ );
+
// Navigate through the bounce chain.
await navigateLinkClick(
browser,
- getBounceURL({ bounceType, targetURL, setState })
+ getBounceURL({
+ bounceType,
+ targetURL,
+ setState,
+ setStateSameSiteFrame,
+ setStateInWebWorker,
+ setStateInNestedWebWorker,
+ })
);
- // Wait for the final site to be loaded which complete the BounceTrackingRecord.
- await BrowserTestUtils.browserLoaded(browser, false, targetURL);
+ await targetURLLoadedPromise;
// Navigate again with user gesture which triggers
// BounceTrackingProtection::RecordStatefulBounces. We could rely on the
@@ -226,9 +320,9 @@ async function runTestBounce(options = {}) {
await promiseRecordBounces;
Assert.deepEqual(
- bounceTrackingProtection.testGetBounceTrackerCandidateHosts(
- originAttributes
- ),
+ bounceTrackingProtection
+ .testGetBounceTrackerCandidateHosts(originAttributes)
+ .map(entry => entry.siteHost),
expectCandidate ? [SITE_TRACKER] : [],
`Should ${
expectCandidate ? "" : "not "
@@ -237,6 +331,7 @@ async function runTestBounce(options = {}) {
Assert.deepEqual(
bounceTrackingProtection
.testGetUserActivationHosts(originAttributes)
+ .map(entry => entry.siteHost)
.sort(),
[SITE_A, SITE_B].sort(),
"Should only have user activation for sites where we clicked links."
@@ -272,4 +367,7 @@ async function runTestBounce(options = {}) {
);
}
bounceTrackingProtection.clearAll();
+ if (!skipSiteDataCleanup) {
+ await SiteDataTestUtils.clear();
+ }
}
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
index afc3239839..3b0fe351e2 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py
@@ -48,7 +48,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase):
let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
Ci.nsIBounceTrackingProtection
);
- return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort();
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).map(entry => entry.siteHost).sort();
""",
)
self.assertEqual(
@@ -64,7 +64,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase):
let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
Ci.nsIBounceTrackingProtection
);
- return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort();
+ return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).map(entry => entry.siteHost).sort();
""",
)
self.assertEqual(
@@ -109,7 +109,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase):
let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
Ci.nsIBounceTrackingProtection
);
- return bounceTrackingProtection.testGetUserActivationHosts({}).sort();
+ return bounceTrackingProtection.testGetUserActivationHosts({}).map(entry => entry.siteHost).sort();
""",
)
self.assertEqual(
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js
index 28a1350b3e..a52fb7fd46 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js
@@ -69,13 +69,19 @@ add_task(async function test() {
// Assert that expired user activations have been cleared.
Assert.deepEqual(
- btp.testGetUserActivationHosts({}).sort(),
+ btp
+ .testGetUserActivationHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
["not-expired1.com", "not-expired2.com"],
"Expired user activation flags have been cleared for normal browsing."
);
Assert.deepEqual(
- btp.testGetUserActivationHosts({ privateBrowsingId: 1 }).sort(),
+ btp
+ .testGetUserActivationHosts({ privateBrowsingId: 1 })
+ .map(entry => entry.siteHost)
+ .sort(),
["pbm-not-expired.com"],
"Expired user activation flags have been cleared for private browsing."
);
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js
new file mode 100644
index 0000000000..5150d074c2
--- /dev/null
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const DOMAIN_A = "example.com";
+const SUB_DOMAIN_A = "sub." + DOMAIN_A;
+const DOMAIN_B = "example.org";
+const DOMAIN_C = "example.net";
+
+const ORIGIN_A = "https://" + DOMAIN_A;
+const ORIGIN_SUB_A = "https://" + SUB_DOMAIN_A;
+const ORIGIN_B = "https://" + DOMAIN_B;
+const ORIGIN_C = "https://" + DOMAIN_C;
+const ORIGIN_NON_HTTP = "file:///foo/bar.html";
+
+const OA_PBM = { privateBrowsingId: 1 };
+const PRINCIPAL_C_PBM = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(ORIGIN_C),
+ OA_PBM
+);
+
+let btp;
+let userActivationLifetimeSec = Services.prefs.getIntPref(
+ "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec"
+);
+
+function cleanup() {
+ btp.clearAll();
+ Services.perms.removeAll();
+ Services.prefs.setBoolPref(
+ "privacy.bounceTrackingProtection.hasMigratedUserActivationData",
+ false
+ );
+}
+
+add_setup(function () {
+ // Need a profile to data clearing calls.
+ do_get_profile();
+
+ btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
+ Ci.nsIBounceTrackingProtection
+ );
+
+ // Clean initial state.
+ cleanup();
+});
+
+add_task(async function test_user_activation_perm_migration() {
+ // Assert initial test state.
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts({}),
+ [],
+ "No user activation hosts initially."
+ );
+ Assert.equal(
+ Services.perms.getAllByTypes(["storageAccessAPI"]).length,
+ 0,
+ "No user activation permissions initially."
+ );
+
+ info("Add test user activation permissions.");
+
+ let now = Date.now();
+
+ // Non-expired permissions.
+ PermissionTestUtils.addWithModificationTime(
+ ORIGIN_A,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ now
+ );
+ PermissionTestUtils.addWithModificationTime(
+ ORIGIN_C,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ now - 1000
+ );
+
+ // A non expired permission for a subdomain of DOMAIN_A that has an older modification time.
+ PermissionTestUtils.addWithModificationTime(
+ ORIGIN_SUB_A,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ now - 500
+ );
+
+ // An expired permission.
+ PermissionTestUtils.addWithModificationTime(
+ ORIGIN_B,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ now - userActivationLifetimeSec * 1.2 * 1000
+ );
+
+ // A non-HTTP permission.
+ PermissionTestUtils.addWithModificationTime(
+ ORIGIN_NON_HTTP,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ now
+ );
+
+ // A permission for PBM. Ideally we'd test a more persistent permission type
+ // here with custom oa, but permission seperation by userContextId isn't
+ // enabled yet (Bug 1641584).
+ PermissionTestUtils.addWithModificationTime(
+ PRINCIPAL_C_PBM,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ now
+ );
+
+ info("Trigger migration.");
+ btp.testMaybeMigrateUserInteractionPermissions();
+
+ Assert.deepEqual(
+ btp
+ .testGetUserActivationHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
+ [DOMAIN_A, DOMAIN_C].sort(),
+ "Should have imported the correct user activation flags."
+ );
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts(OA_PBM).map(entry => entry.siteHost),
+ [DOMAIN_C],
+ "Should have imported the correct user activation flags for PBM."
+ );
+
+ info("Reset the BTP user activation store");
+ btp.clearAll();
+
+ info("Trigger migration again.");
+ btp.testMaybeMigrateUserInteractionPermissions();
+
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts({}),
+ [],
+ "Should not have imported the user activation flags again."
+ );
+ Assert.deepEqual(
+ btp.testGetUserActivationHosts(OA_PBM),
+ [],
+ "Should not have imported the user activation flags again for PBM."
+ );
+
+ cleanup();
+});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
index 5ede57a08b..2fbd8a0a02 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
@@ -6,6 +6,9 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
const { SiteDataTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/SiteDataTestUtils.sys.mjs"
);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
let btp;
let bounceTrackingGracePeriodSec;
@@ -138,6 +141,14 @@ add_task(async function test_purge() {
message: "Should purge after grace period.",
shouldPurge: true,
},
+ // Don't purge if the site is allowlisted.
+ "example2.net": {
+ bounceTime: timestampOutsideGracePeriodFiveSeconds,
+ userActivationTime: null,
+ isAllowListed: true,
+ message: "Should not purge after grace period if allowlisted.",
+ shouldPurge: false,
+ },
// Also ensure that clear data calls with IP sites succeed.
"1.2.3.4": {
bounceTime: timestampOutsideGracePeriodThreeDays,
@@ -191,16 +202,30 @@ add_task(async function test_purge() {
let expectedBounceTrackerHosts = [];
let expectedUserActivationHosts = [];
+ let allowListedHosts = [];
let expiredUserActivationHosts = [];
let expectedPurgedHosts = [];
// This would normally happen over time while browsing.
let initPromises = Object.entries(TEST_TRACKERS).map(
- async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => {
+ async ([
+ siteHost,
+ { bounceTime, userActivationTime, isAllowListed, shouldPurge },
+ ]) => {
// Add site state so we can later assert it has been purged.
await addStateForHost(siteHost);
+ // Add allowlist entry if needed.
+ if (isAllowListed) {
+ PermissionTestUtils.add(
+ `https://${siteHost}`,
+ "trackingprotection",
+ Services.perms.ALLOW_ACTION
+ );
+ allowListedHosts.push(siteHost);
+ }
+
if (bounceTime != null) {
if (userActivationTime != null) {
throw new Error(
@@ -210,7 +235,7 @@ add_task(async function test_purge() {
expectedBounceTrackerHosts.push(siteHost);
- // Convert bounceTime timestamp to nanoseconds (PRTime).
+ // Convert bounceTime timestamp to microseconds (PRTime).
info(
`Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms`
);
@@ -232,7 +257,7 @@ add_task(async function test_purge() {
expiredUserActivationHosts.push(siteHost);
}
- // Convert userActivationTime timestamp to nanoseconds (PRTime).
+ // Convert userActivationTime timestamp to microseconds (PRTime).
info(
`Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms`
);
@@ -250,12 +275,18 @@ add_task(async function test_purge() {
"Check that bounce and user activation data has been correctly recorded."
);
Assert.deepEqual(
- btp.testGetBounceTrackerCandidateHosts({}).sort(),
+ btp
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
expectedBounceTrackerHosts.sort(),
"Has added bounce tracker hosts."
);
Assert.deepEqual(
- btp.testGetUserActivationHosts({}).sort(),
+ btp
+ .testGetUserActivationHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
expectedUserActivationHosts.sort(),
"Has added user activation hosts."
);
@@ -269,17 +300,29 @@ add_task(async function test_purge() {
"Should have purged all expected hosts."
);
+ // After the purge only the bounce trackers that have not been purged should
+ // remain in the candidate map. Additionally the allowlisted hosts should be
+ // removed from the candidate map.
let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts
- .filter(host => !expectedPurgedHosts.includes(host))
+ .filter(
+ host =>
+ !expectedPurgedHosts.includes(host) && !allowListedHosts.includes(host)
+ )
.sort();
Assert.deepEqual(
- btp.testGetBounceTrackerCandidateHosts({}).sort(),
+ btp
+ .testGetBounceTrackerCandidateHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
expectedBounceTrackerHostsAfterPurge.sort(),
"After purge the bounce tracker candidate host set should be updated correctly."
);
Assert.deepEqual(
- btp.testGetUserActivationHosts({}).sort(),
+ btp
+ .testGetUserActivationHosts({})
+ .map(entry => entry.siteHost)
+ .sort(),
expiredUserActivationHosts.sort(),
"After purge any expired user activation records should have been removed"
);
@@ -302,6 +345,7 @@ add_task(async function test_purge() {
btp.clearAll();
assertEmpty();
- info("Clean up site data.");
+ info("Clean up site data and permissions.");
await SiteDataTestUtils.clear();
+ Services.perms.removeAll();
});
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml
index c3aeee502f..8195ea7224 100644
--- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml
+++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml
@@ -3,8 +3,11 @@ prefs = [
"privacy.bounceTrackingProtection.enabled=true",
"privacy.bounceTrackingProtection.enableTestMode=true",
"privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0",
+ "privacy.bounceTrackingProtection.enableDryRunMode=false",
]
["test_bouncetracking_clearExpiredUserActivation.js"]
+["test_bouncetracking_importUserActivationPermissions.js"]
+
["test_bouncetracking_purge.js"]