#include "content/browser/btm/btm_service_impl.h"
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_deref.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ref.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "content/browser/browser_context_impl.h"
#include "content/browser/btm/btm_storage.h"
#include "content/browser/btm/btm_utils.h"
#include "content/browser/btm/persistent_repeating_timer.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/browsing_data_filter_builder.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/btm_redirect_info.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/btm_utils.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/cookie_partition_key.h"
#include "net/cookies/cookie_partition_key_collection.h"
#include "net/cookies/cookie_setting_override.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/mojom/clear_data_filter.mojom.h"
#include "url/origin.h"
namespace content {
namespace {
BtmRedirectCategory ClassifyRedirect(BtmDataAccessType access,
bool has_user_activation) {
using enum BtmRedirectCategory;
switch (access) {
case BtmDataAccessType::kUnknown:
return has_user_activation ? kUnknownCookies_HasEngagement
: kUnknownCookies_NoEngagement;
case BtmDataAccessType::kNone:
return has_user_activation ? kNoCookies_HasEngagement
: kNoCookies_NoEngagement;
case BtmDataAccessType::kRead:
return has_user_activation ? kReadCookies_HasEngagement
: kReadCookies_NoEngagement;
case BtmDataAccessType::kWrite:
return has_user_activation ? kWriteCookies_HasEngagement
: kWriteCookies_NoEngagement;
case BtmDataAccessType::kReadWrite:
return has_user_activation ? kReadWriteCookies_HasEngagement
: kReadWriteCookies_NoEngagement;
}
}
inline void UmaHistogramBounceCategory(BtmRedirectCategory category,
BtmCookieMode mode,
BtmRedirectType type) {
const std::string histogram_name =
base::StrCat({"Privacy.DIPS.BounceCategory", GetHistogramPiece(type),
GetHistogramSuffix(mode)});
base::UmaHistogramEnumeration(histogram_name, category);
}
inline void UmaHistogramDeletionLatency(base::Time deletion_start) {
base::UmaHistogramLongTimes100("Privacy.DIPS.DeletionLatency2",
base::Time::Now() - deletion_start);
}
inline void UmaHistogramClearedSitesCount(BtmCookieMode mode, int size) {
base::UmaHistogramCounts1000(base::StrCat({"Privacy.DIPS.ClearedSitesCount",
GetHistogramSuffix(mode)}),
size);
}
inline void UmaHistogramBounceDelay(base::TimeDelta sample) {
base::UmaHistogramTimes("Privacy.DIPS.ServerBounceDelay", sample);
}
inline void UmaHistogramBounceChainDelay(base::TimeDelta sample) {
base::UmaHistogramTimes("Privacy.DIPS.ServerBounceChainDelay", sample);
}
inline void UmaHistogramBounceStatusCode(int response_code, bool cached) {
base::UmaHistogramSparse(cached ? "Privacy.DIPS.BounceStatusCode.Cached"
: "Privacy.DIPS.BounceStatusCode.NoCache",
response_code);
}
inline void UmaHistogramDeletion(BtmCookieMode mode, BtmDeletionAction action) {
base::UmaHistogramEnumeration(
base::StrCat({"Privacy.DIPS.Deletion", GetHistogramSuffix(mode)}),
action);
}
inline void UmaHistogramSiteToClearDomainLength(
std::string const& site_to_clear,
bool is_canonical_host) {
base::UmaHistogramSparse(
is_canonical_host ? "Privacy.DIPS.DeletionDomainLength.Serializable"
: "Privacy.DIPS.DeletionDomainLength.NonCanonical",
site_to_clear.length());
}
void RecordRedirectMetrics(const BtmRedirectInfo& redirect,
const BtmRedirectChainInfo& chain) {
DCHECK(redirect.site_had_user_activation.has_value());
DCHECK(redirect.site_had_webauthn_assertion.has_value());
DCHECK(redirect.chain_id.has_value());
DCHECK(redirect.chain_index.has_value());
DCHECK_LT(redirect.chain_index.value(), chain.length);
bool initial_site_same = (redirect.site == chain.initial_site);
bool final_site_same = (redirect.site == chain.final_site);
if (!chain.are_3pcs_generally_enabled) {
ukm::builders::BTM_Redirect(redirect.redirector_source_id)
.SetSiteHadUserActivation(redirect.site_had_user_activation.value())
.SetSiteHadWebAuthnAssertion(
redirect.site_had_webauthn_assertion.value())
.SetRedirectType(static_cast<int64_t>(redirect.redirect_type))
.SetCookieAccessType(static_cast<int64_t>(redirect.access_type))
.SetRedirectAndInitialSiteSame(initial_site_same)
.SetRedirectAndFinalSiteSame(final_site_same)
.SetInitialAndFinalSitesSame(chain.initial_and_final_sites_same)
.SetRedirectChainIndex(redirect.chain_index.value())
.SetRedirectChainLength(chain.length)
.SetIsPartialRedirectChain(chain.is_partial_chain)
.SetClientBounceDelay(
BucketizeBtmBounceDelay(redirect.client_bounce_delay))
.SetHasStickyActivation(redirect.has_sticky_activation)
.SetWebAuthnAssertionRequestSucceeded(
redirect.web_authn_assertion_request_succeeded)
.SetChainId(redirect.chain_id.value())
.Record(ukm::UkmRecorder::Get());
}
if (initial_site_same || final_site_same) {
return;
}
BtmRedirectCategory category = ClassifyRedirect(
redirect.access_type, redirect.site_had_user_activation.value());
UmaHistogramBounceCategory(category, chain.cookie_mode.value(),
redirect.redirect_type);
if (redirect.redirect_type == BtmRedirectType::kServer) {
UmaHistogramBounceDelay(redirect.server_bounce_delay);
UmaHistogramBounceStatusCode(redirect.response_code,
redirect.was_response_cached);
}
}
net::CookiePartitionKeyCollection CookiePartitionKeyCollectionForSites(
const std::vector<std::string>& sites) {
std::vector<net::CookiePartitionKey> keys;
for (const auto& site : sites) {
for (const auto& [scheme, port] :
{std::make_pair("http", 80), std::make_pair("https", 443)}) {
std::optional<url::Origin> origin =
url::Origin::UnsafelyCreateTupleOriginWithoutNormalization(
scheme, site, port);
UmaHistogramSiteToClearDomainLength(site, origin.has_value());
if (!origin.has_value()) {
break;
}
for (auto ancestorChainBit :
{net::CookiePartitionKey::AncestorChainBit::kSameSite,
net::CookiePartitionKey::AncestorChainBit::kCrossSite}) {
std::optional<net::CookiePartitionKey> key =
net::CookiePartitionKey::FromStorageKeyComponents(
net::SchemefulSite(*origin), ancestorChainBit,
std::nullopt);
if (key.has_value()) {
keys.push_back(*key);
}
}
}
}
return net::CookiePartitionKeyCollection(keys);
}
class StateClearer : public BrowsingDataRemover::Observer {
public:
StateClearer(const StateClearer&) = delete;
StateClearer& operator=(const StateClearer&) = delete;
~StateClearer() override { remover_->RemoveObserver(this); }
static void DeleteState(BrowsingDataRemover* remover,
std::vector<std::string> sites_to_clear,
BrowsingDataRemover::DataType remove_mask,
base::OnceClosure callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::unique_ptr<BrowsingDataFilterBuilder> filter =
BrowsingDataFilterBuilder::Create(
BrowsingDataFilterBuilder::Mode::kDelete);
for (const auto& site : sites_to_clear) {
filter->AddRegisterableDomain(site);
}
filter->SetCookiePartitionKeyCollection(
net::CookiePartitionKeyCollection());
std::unique_ptr<BrowsingDataFilterBuilder> partitioned_cookie_filter =
BrowsingDataFilterBuilder::Create(
BrowsingDataFilterBuilder::Mode::kPreserve);
partitioned_cookie_filter->SetCookiePartitionKeyCollection(
CookiePartitionKeyCollectionForSites(sites_to_clear));
partitioned_cookie_filter->SetPartitionedCookiesOnly(true);
StateClearer* clearer =
new StateClearer(remover, 2, std::move(callback));
remove_mask &= ~BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX;
remover->RemoveWithFilterAndReply(
base::Time::Min(), base::Time::Max(),
remove_mask | BrowsingDataRemover::DATA_TYPE_AVOID_CLOSING_CONNECTIONS,
BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB,
std::move(filter), clearer);
remover->RemoveWithFilterAndReply(
base::Time::Min(), base::Time::Max(),
BrowsingDataRemover::DATA_TYPE_COOKIES,
BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB,
std::move(partitioned_cookie_filter), clearer);
}
private:
StateClearer(BrowsingDataRemover* remover,
int callback_count,
base::OnceClosure callback)
: remover_(remover),
deletion_start_(base::Time::Now()),
expected_callback_count_(callback_count),
callback_(std::move(callback)) {
remover_->AddObserver(this);
}
void OnBrowsingDataRemoverDone(uint64_t failed_data_types) override {
CHECK_CURRENTLY_ON(BrowserThread::UI);
if (++callback_count_ == expected_callback_count_) {
UmaHistogramDeletionLatency(deletion_start_);
std::move(callback_).Run();
delete this;
}
}
raw_ptr<BrowsingDataRemover> remover_;
const base::Time deletion_start_;
const int expected_callback_count_;
int callback_count_ = 0;
base::OnceClosure callback_;
};
class DipsTimerStorage : public PersistentRepeatingTimer::Storage {
public:
explicit DipsTimerStorage(base::SequenceBound<BtmStorage>* dips_storage);
~DipsTimerStorage() override;
void GetLastFired(TimeCallback callback) const override {
dips_storage_->AsyncCall(&BtmStorage::GetTimerLastFired)
.Then(std::move(callback));
}
void SetLastFired(base::Time time) override {
dips_storage_->AsyncCall(base::IgnoreResult(&BtmStorage::SetTimerLastFired))
.WithArgs(time);
}
private:
raw_ref<base::SequenceBound<BtmStorage>> dips_storage_;
};
DipsTimerStorage::DipsTimerStorage(
base::SequenceBound<BtmStorage>* dips_storage)
: dips_storage_(CHECK_DEREF(dips_storage)) {}
DipsTimerStorage::~DipsTimerStorage() = default;
}
BtmService* BtmService::Get(BrowserContext* context) {
return BtmServiceImpl::Get(context);
}
BtmServiceImpl::BtmServiceImpl(base::PassKey<BrowserContextImpl>,
BrowserContext* context)
: browser_context_(context) {
DCHECK(base::FeatureList::IsEnabled(features::kBtm));
base::FilePath btm_path = GetBtmFilePath(browser_context_);
const bool use_in_memory_db =
#if BUILDFLAG(IS_FUCHSIA) && defined(IS_WEB_ENGINE)
true;
#else
browser_context_->IsOffTheRecord();
#endif
storage_ =
use_in_memory_db
? base::SequenceBound<BtmStorage>(CreateTaskRunner(), std::nullopt)
: base::SequenceBound<BtmStorage>(
CreateTaskRunnerForResource(btm_path), btm_path);
#if BUILDFLAG(IS_FUCHSIA) && defined(IS_WEB_ENGINE)
BtmStorage::DeleteDatabaseFiles(btm_path,
fuchsia_cleanup_loop_.QuitClosure());
#endif
repeating_timer_ = CreateTimer();
repeating_timer_->Start();
}
std::unique_ptr<PersistentRepeatingTimer> BtmServiceImpl::CreateTimer() {
CHECK(!storage_.is_null());
return std::make_unique<PersistentRepeatingTimer>(
std::make_unique<DipsTimerStorage>(&storage_),
features::kBtmTimerDelay.Get(),
base::BindRepeating(&BtmServiceImpl::OnTimerFired,
base::Unretained(this)));
}
BtmServiceImpl::~BtmServiceImpl() {
ClearAllUserData();
}
BtmServiceImpl* BtmServiceImpl::Get(BrowserContext* context) {
return BrowserContextImpl::From(context)->GetBtmService();
}
scoped_refptr<base::SequencedTaskRunner> BtmServiceImpl::CreateTaskRunner() {
return base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::ThreadPolicy::PREFER_BACKGROUND});
}
scoped_refptr<base::SequencedTaskRunner>
BtmServiceImpl::CreateTaskRunnerForResource(const base::FilePath& path) {
return base::ThreadPool::CreateSequencedTaskRunnerForResource(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::ThreadPolicy::PREFER_BACKGROUND},
path);
}
BtmCookieMode BtmServiceImpl::GetCookieMode() const {
return GetBtmCookieMode(browser_context_->IsOffTheRecord());
}
void BtmServiceImpl::RemoveEvents(const base::Time& delete_begin,
const base::Time& delete_end,
network::mojom::ClearDataFilterPtr filter,
BtmEventRemovalType type) {
storage_.AsyncCall(&BtmStorage::RemoveEvents)
.WithArgs(delete_begin, delete_end, std::move(filter), type);
}
void BtmServiceImpl::HandleRedirectChain(
std::vector<BtmRedirectInfoPtr> redirects,
BtmRedirectChainInfoPtr chain,
StatefulBounceCallback stateful_bounce_callback) {
DCHECK_LE(redirects.size(), chain->length);
if (redirects.empty()) {
DCHECK(!chain->is_partial_chain);
for (auto& observer : observers_) {
observer.OnChainHandled(redirects, chain);
}
return;
}
if (!chain->are_3pcs_generally_enabled &&
chain->initial_source_id != ukm::kInvalidSourceId) {
ukm::builders::BTM_ChainBegin(chain->initial_source_id)
.SetChainId(chain->chain_id)
.SetInitialAndFinalSitesSame(chain->initial_and_final_sites_same)
.Record(ukm::UkmRecorder::Get());
}
if (!chain->are_3pcs_generally_enabled &&
chain->final_source_id != ukm::kInvalidSourceId) {
ukm::builders::BTM_ChainEnd(chain->final_source_id)
.SetChainId(chain->chain_id)
.SetInitialAndFinalSitesSame(chain->initial_and_final_sites_same)
.Record(ukm::UkmRecorder::Get());
}
std::set<std::string> redirect_sites;
base::TimeDelta total_server_bounce_delay;
for (const auto& redirect : redirects) {
if (redirect->redirect_type == BtmRedirectType::kServer) {
total_server_bounce_delay += redirect->server_bounce_delay;
}
redirect_sites.insert(GetSiteForBtm(redirect->redirector_url));
}
UmaHistogramBounceChainDelay(total_server_bounce_delay);
chain->cookie_mode = GetCookieMode();
storage_.AsyncCall(&BtmStorage::FilterSitesWithProtectiveEvent)
.WithArgs(redirect_sites)
.Then(base::BindOnce(&BtmServiceImpl::HandleRedirects,
weak_factory_.GetWeakPtr(), std::move(redirects),
std::move(chain), stateful_bounce_callback));
}
void BtmServiceImpl::HandleRedirects(
std::vector<BtmRedirectInfoPtr> redirects,
BtmRedirectChainInfoPtr chain,
StatefulBounceCallback stateful_bounce_callback,
std::pair<std::set<std::string>, std::set<std::string>>
sites_with_protective_events) {
const auto& [sites_with_user_activation, sites_with_webauthn_assertion] =
sites_with_protective_events;
for (size_t index = 0; index < redirects.size(); index++) {
auto& redirect = *redirects[index];
DCHECK(!redirect.site_had_user_activation.has_value());
redirect.site_had_user_activation =
sites_with_user_activation.contains(redirect.site);
DCHECK(!redirect.site_had_webauthn_assertion.has_value());
redirect.site_had_webauthn_assertion =
sites_with_webauthn_assertion.contains(redirect.site);
DCHECK(!redirect.chain_id.has_value());
redirect.chain_id = chain->chain_id;
DCHECK(!redirect.chain_index.has_value());
redirect.chain_index = chain->length - redirects.size() + index;
RecordRedirectMetrics(redirect, *chain);
bool initial_site_same = (redirect.site == chain->initial_site);
bool final_site_same = (redirect.site == chain->final_site);
if (initial_site_same || final_site_same) {
continue;
}
if (redirect.access_type == BtmDataAccessType::kUnknown) {
continue;
}
RecordBounce(stateful_bounce_callback, redirect, *chain);
}
if (!chain->is_partial_chain) {
for (auto& observer : observers_) {
observer.OnChainHandled(redirects, chain);
}
}
}
void BtmServiceImpl::RecordBounce(
StatefulBounceCallback stateful_bounce_callback,
const BtmRedirectInfo& redirect,
const BtmRedirectChainInfo& chain) {
const GURL& url = redirect.redirector_url;
bool stateful = redirect.access_type > BtmDataAccessType::kRead;
if (redirect.has_3pc_exception.value()) {
bool would_be_cleared;
switch (features::kBtmTriggeringAction.Get()) {
case BtmTriggeringAction::kNone: {
would_be_cleared = false;
break;
}
case BtmTriggeringAction::kBounce: {
would_be_cleared = true;
break;
}
}
if (!chain.are_3pcs_generally_enabled && would_be_cleared) {
if (url.is_empty()) {
UmaHistogramDeletion(GetCookieMode(), BtmDeletionAction::kIgnored);
return;
}
UmaHistogramDeletion(GetCookieMode(), BtmDeletionAction::kExcepted);
}
const std::set<std::string> site_to_clear{GetSiteForBtm(url)};
storage_.AsyncCall(&BtmStorage::RemoveRowsWithoutProtectiveEvent)
.WithArgs(site_to_clear);
return;
}
if (stateful) {
stateful_bounce_callback.Run(chain.final_url);
}
storage_.AsyncCall(&BtmStorage::RecordBounce).WithArgs(url, redirect.time);
}
void BtmServiceImpl::RecordRedirectMetricsForTesting(
const BtmRedirectInfo& redirect,
const BtmRedirectChainInfo& chain) {
RecordRedirectMetrics(redirect, chain);
}
void BtmServiceImpl::OnTimerFired() {
storage_.AsyncCall(&BtmStorage::GetSitesToClear)
.WithArgs(std::nullopt)
.Then(base::BindOnce(&BtmServiceImpl::DeleteBtmEligibleState,
weak_factory_.GetWeakPtr(), base::DoNothing()));
}
void BtmServiceImpl::DeleteEligibleSitesImmediately(
DeletedSitesCallback callback) {
storage_.AsyncCall(&BtmStorage::GetSitesToClear)
.WithArgs(base::Seconds(0))
.Then(base::BindOnce(&BtmServiceImpl::DeleteBtmEligibleState,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void BtmServiceImpl::DeleteBtmEligibleState(
DeletedSitesCallback callback,
std::vector<std::string> sites_to_clear) {
for (const std::pair<std::string, int> site_ctr : open_sites_) {
CHECK(site_ctr.second > 0);
std::erase(sites_to_clear, site_ctr.first);
}
std::vector<std::string> filtered_sites_to_clear;
for (const auto& site : sites_to_clear) {
if (site.empty()) {
UmaHistogramDeletion(GetCookieMode(), BtmDeletionAction::kIgnored);
continue;
}
UmaHistogramDeletion(GetCookieMode(), BtmDeletionAction::kEnforced);
const ukm::SourceId source_id = ukm::UkmRecorder::GetSourceIdForDipsSite(
base::PassKey<BtmServiceImpl>(), site);
ukm::builders::DIPS_Deletion(source_id)
.SetShouldBlockThirdPartyCookies(true)
.SetHasCookieException(false)
.SetIsDeletionEnabled(true)
.Record(ukm::UkmRecorder::Get());
filtered_sites_to_clear.push_back(site);
}
UmaHistogramClearedSitesCount(GetCookieMode(), sites_to_clear.size());
base::OnceClosure finish_callback = base::BindOnce(
std::move(callback), std::vector<std::string>(filtered_sites_to_clear));
if (filtered_sites_to_clear.empty()) {
std::move(finish_callback).Run();
return;
}
RunDeletionTaskOnUIThread(std::move(filtered_sites_to_clear),
std::move(finish_callback));
}
void BtmServiceImpl::RunDeletionTaskOnUIThread(std::vector<std::string> sites,
base::OnceClosure callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
uint64_t remove_mask = GetContentClient()->browser()->GetBtmRemoveMask();
StateClearer::DeleteState(browser_context_->GetBrowsingDataRemover(),
std::move(sites), remove_mask, std::move(callback));
}
void BtmServiceImpl::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void BtmServiceImpl::RemoveObserver(const Observer* observer) {
observers_.RemoveObserver(observer);
}
void BtmServiceImpl::RecordUserActivationForTesting(const GURL& url) {
storage_.AsyncCall(&BtmStorage::RecordUserActivation)
.WithArgs(url, base::Time::Now());
}
void BtmServiceImpl::DidSiteHaveUserActivationSince(
const GURL& url,
base::Time bound,
CheckUserActivationCallback callback) const {
storage_.AsyncCall(&BtmStorage::DidSiteHaveUserActivationSince)
.WithArgs(url, bound)
.Then(std::move(callback));
}
void BtmServiceImpl::RecordBrowserSignIn(std::string_view domain) {
storage()
->AsyncCall(&BtmStorage::RecordUserActivation)
.WithArgs(url::SchemeHostPort("http", domain, 80).GetURL(),
base::Time::Now());
}
void BtmServiceImpl::NotifyStatefulBounce(WebContents* web_contents) {
for (auto& observer : observers_) {
observer.OnStatefulBounce(web_contents);
}
}
}