| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #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_forward.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()); |
| } |
| |
| // Don't record UMA metrics for same-site redirects. |
| 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()); |
| // The host may be non-canonical or invalid. In such a case, we ignore it, |
| // since it will cause IPC deserialization issues later on. |
| 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, |
| /*nonce=*/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); } |
| |
| // Clears state for the sites in `sites_to_clear`. Runs `callback` once |
| // clearing is complete. |
| // |
| // NOTE: This deletion task removing rows for `sites_to_clear` from the |
| // BtmStorage backend relies on the assumption that rows flagged as BTM |
| // eligible don't have user activation time values. So even though 'remover' |
| // will only clear the storage timestamps, that's sufficient to delete the |
| // entire row. |
| static void DeleteState(BrowsingDataRemover* remover, |
| std::vector<std::string> sites_to_clear, |
| BrowsingDataRemover::DataType remove_mask, |
| base::OnceClosure callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // This filter will match unpartitioned cookies and storage, as well as |
| // storage (but not cookies) that is partitioned under tracking domains. |
| std::unique_ptr<BrowsingDataFilterBuilder> filter = |
| BrowsingDataFilterBuilder::Create( |
| BrowsingDataFilterBuilder::Mode::kDelete); |
| for (const auto& site : sites_to_clear) { |
| filter->AddRegisterableDomain(site); |
| } |
| // Don't delete CHIPS partitioned under non-tracking sites. |
| filter->SetCookiePartitionKeyCollection( |
| net::CookiePartitionKeyCollection()); |
| |
| // This filter will match cookies partitioned under tracking domains. |
| 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); |
| // We don't add any domains to this filter, so with mode=kPreserve it will |
| // delete everything partitioned under the sites. |
| |
| // StateClearer manages its own lifetime and deletes itself when finished. |
| StateClearer* clearer = |
| new StateClearer(remover, /*callback_count=*/2, std::move(callback)); |
| |
| // Don't delete Privacy Sandbox data - see crbug.com/41488981. |
| 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 will run `callback` and delete itself after |
| // OnBrowsingDataRemoverDone() is called `callback_count` times. |
| 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); |
| } |
| |
| // BrowsingDataRemover::Observer overrides: |
| 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; // Matches the new in DeleteState() |
| } |
| } |
| |
| 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; |
| |
| // Reads the timestamp from the DIPS DB. |
| void GetLastFired(TimeCallback callback) const override { |
| dips_storage_->AsyncCall(&BtmStorage::GetTimerLastFired) |
| .Then(std::move(callback)); |
| } |
| // Write the timestamp to the DIPS DB. |
| 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; |
| |
| } // namespace |
| |
| // static |
| 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_); |
| // This feature explicitly uses in-memory storage on WebEngine on Fuchsia to |
| // avoid consuming too much storage space. WebEngine has only 2MB of storage |
| // for the user data directory. |
| 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) |
| // WebEngine on Fuchsia has a limited amount of storage, so we don't want to |
| // keep around any data from previous sessions before the change was made to |
| // always use an in-memory database. |
| 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()); |
| // base::Unretained(this) is safe here since the timer that is created has the |
| // same lifetime as this service. |
| return std::make_unique<PersistentRepeatingTimer>( |
| std::make_unique<DipsTimerStorage>(&storage_), |
| features::kBtmTimerDelay.Get(), |
| base::BindRepeating(&BtmServiceImpl::OnTimerFired, |
| base::Unretained(this))); |
| } |
| |
| BtmServiceImpl::~BtmServiceImpl() { |
| // Some UserData may interact with `this` during their destruction. Delete |
| // them now, before it's too late. If we don't delete them manually, |
| // ~SupportsUserData() will, but `this` will be invalid at that time. |
| // |
| // Note that we can't put this call in ~BtmService() either, even though |
| // BtmService is the class that directly inherits from SupportsUserData. |
| // Because when ~BtmService() is called, it's undefined behavior to call |
| // pure virtual functions like BtmService::RemoveObserver(). |
| ClearAllUserData(); |
| } |
| |
| // static |
| 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 init should be finished by now, so no need to delay until then. |
| 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; |
| // If the chain was too long, some redirects may have been trimmed already, |
| // which would make `index` not the "true" index of the redirect in the |
| // whole chain. `chain->length` is accurate though. `chain->length - |
| // redirects.size()` is then the number of trimmed redirects; so add that to |
| // `index` to get the "true" index to report in our metrics. |
| 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); |
| } |
| |
| // All redirects handled. |
| 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 the bounced URL has a 3PC exception when embedded under the initial or |
| // final URL in the redirect, then clear the tracking site from the BTM |
| // database to avoid deleting its storage. The exception overrides any bounces |
| // from non-excepted sites. |
| if (redirect.has_3pc_exception.value()) { |
| // Check whether the site would have hypothetically been cleared. |
| bool would_be_cleared; |
| // TODO(crbug.com/430921459): Refactor killswitch behavior into the |
| // top-level feature so there's no need to maintain multiple triggering |
| // actions. |
| 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) { |
| // TODO(crbug.com/40268849): Investigate and fix the presence of empty |
| // site(s) in the `site_to_clear` list. Once this is fixed remove this |
| // escape. |
| if (url.is_empty()) { |
| UmaHistogramDeletion(GetCookieMode(), BtmDeletionAction::kIgnored); |
| return; |
| } |
| UmaHistogramDeletion(GetCookieMode(), BtmDeletionAction::kExcepted); |
| } |
| |
| const std::set<std::string> site_to_clear{GetSiteForBtm(url)}; |
| // Don't clear the row if the tracker has history indicating that we |
| // should preserve that context for future bounces. |
| storage_.AsyncCall(&BtmStorage::RemoveRowsWithoutProtectiveEvent) |
| .WithArgs(site_to_clear); |
| |
| return; |
| } |
| |
| // If the bounce is stateful and not excepted by cookie settings, run the |
| // callback. |
| if (stateful) { |
| stateful_bounce_callback.Run(chain.final_url); |
| } |
| |
| // Record the bounce at the storage layer. |
| storage_.AsyncCall(&BtmStorage::RecordBounce).WithArgs(url, redirect.time); |
| } |
| |
| // static |
| void BtmServiceImpl::RecordRedirectMetricsForTesting( |
| const BtmRedirectInfo& redirect, |
| const BtmRedirectChainInfo& chain) { |
| RecordRedirectMetrics(redirect, chain); |
| } |
| |
| void BtmServiceImpl::OnTimerFired() { |
| // Storage init should be finished by now, so no need to delay until then. |
| storage_.AsyncCall(&BtmStorage::GetSitesToClear) |
| .WithArgs(std::nullopt) |
| .Then(base::BindOnce(&BtmServiceImpl::DeleteBtmEligibleState, |
| weak_factory_.GetWeakPtr(), base::DoNothing())); |
| } |
| |
| void BtmServiceImpl::DeleteEligibleSitesImmediately( |
| DeletedSitesCallback callback) { |
| // Storage init should be finished by now, so no need to delay until then. |
| 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) { |
| // Do not clear sites from currently open tabs. |
| 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) { |
| // TODO(crbug.com/40268849): Investigate and fix the presence of empty |
| // site(s) in the `site_to_clear` list. Once this is fixed remove this loop |
| // escape. |
| 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) |
| // These settings are checked at bounce time, before logging the bounce. |
| // At this time, we guarantee that 3PC are blocked and this site is not |
| // excepted (provided the user hasn't changed their settings in the |
| // meantime). |
| .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; |
| } |
| |
| // Perform state deletion on the filtered list of sites. |
| 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); |
| } |
| } |
| |
| } // namespace content |