910e62b5创建于 1月15日历史提交
// 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_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