| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/dips/dips_storage.h" |
| |
| #include <memory> |
| |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/strcat.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/dips/dips_features.h" |
| #include "chrome/browser/dips/dips_utils.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| inline void UmaHistogramTimeToInteraction(base::TimeDelta sample, |
| DIPSCookieMode mode) { |
| const std::string name = base::StrCat( |
| {"Privacy.DIPS.TimeFromStorageToInteraction", GetHistogramSuffix(mode)}); |
| |
| base::UmaHistogramCustomTimes(name, sample, |
| /*min=*/base::TimeDelta(), |
| /*max=*/base::Days(7), 100); |
| } |
| |
| inline void UmaHistogramTimeToStorage(base::TimeDelta sample, |
| DIPSCookieMode mode) { |
| const std::string name = base::StrCat( |
| {"Privacy.DIPS.TimeFromInteractionToStorage", GetHistogramSuffix(mode)}); |
| |
| base::UmaHistogramCustomTimes(name, sample, |
| /*min=*/base::TimeDelta(), |
| /*max=*/base::Days(7), 100); |
| } |
| |
| // The number of sites to process in each call to DIPSStorage::Prepopulate(). |
| // Intended to be constant; settable only for testing. |
| size_t g_prepopulate_chunk_size = 100; |
| |
| } // namespace |
| |
| DIPSStorage::PrepopulateArgs::PrepopulateArgs(base::Time time, |
| size_t offset, |
| std::vector<std::string> sites) |
| : time(time), offset(offset), sites(std::move(sites)) {} |
| |
| DIPSStorage::PrepopulateArgs::PrepopulateArgs(PrepopulateArgs&&) = default; |
| |
| DIPSStorage::PrepopulateArgs::~PrepopulateArgs() = default; |
| |
| DIPSStorage::DIPSStorage(const absl::optional<base::FilePath>& path) |
| : db_(std::make_unique<DIPSDatabase>(path)) { |
| base::AssertLongCPUWorkAllowed(); |
| } |
| |
| DIPSStorage::~DIPSStorage() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| // DIPSDatabase interaction functions ------------------------------------------ |
| |
| DIPSState DIPSStorage::Read(const GURL& url) { |
| return ReadSite(GetSiteForDIPS(url)); |
| } |
| |
| DIPSState DIPSStorage::ReadSite(std::string site) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| |
| absl::optional<StateValue> state = db_->Read(site); |
| |
| if (state.has_value()) { |
| // We should not have entries in the DB without any timestamps. |
| DCHECK(state->site_storage_times.first.has_value() || |
| state->site_storage_times.last.has_value() || |
| state->user_interaction_times.first.has_value() || |
| state->user_interaction_times.last.has_value() || |
| state->stateful_bounce_times.first.has_value() || |
| state->stateful_bounce_times.last.has_value() || |
| state->bounce_times.first.has_value() || |
| state->bounce_times.last.has_value()); |
| |
| return DIPSState(this, std::move(site), state.value()); |
| } |
| return DIPSState(this, std::move(site)); |
| } |
| |
| void DIPSStorage::Write(const DIPSState& state) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| |
| db_->Write(state.site(), state.site_storage_times(), |
| state.user_interaction_times(), state.stateful_bounce_times(), |
| state.bounce_times()); |
| } |
| |
| void DIPSStorage::RemoveEvents(base::Time delete_begin, |
| base::Time delete_end, |
| network::mojom::ClearDataFilterPtr filter, |
| const DIPSEventRemovalType type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| DCHECK(delete_end.is_null() || delete_begin <= delete_end); |
| |
| if (delete_end.is_null()) { |
| delete_end = base::Time::Max(); |
| } |
| |
| if (filter.is_null()) { |
| db_->RemoveEventsByTime(delete_begin, delete_end, type); |
| } else if (type == DIPSEventRemovalType::kStorage && |
| filter->origins.empty()) { |
| // Site-filtered deletion is only supported for cookie-related |
| // DIPS events, since only cookie deletion allows domains but not hosts. |
| // |
| // TODO(jdh): Assess the use of cookie deletions with both a time range and |
| // a list of domains to determine whether supporting time ranges here is |
| // necessary. |
| // Time ranges aren't currently supported for site-filtered |
| // deletion of DIPS Events. |
| if (delete_begin != base::Time() || delete_end != base::Time::Max()) { |
| return; |
| } |
| |
| bool preserve = |
| (filter->type == network::mojom::ClearDataFilter::Type::KEEP_MATCHES); |
| std::vector<std::string> sites = std::move(filter->domains); |
| |
| db_->RemoveEventsBySite(preserve, sites, type); |
| } |
| } |
| |
| void DIPSStorage::RemoveRows(const std::vector<std::string>& sites) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| db_->RemoveRows(sites); |
| } |
| |
| // DIPSTabHelper Function Impls ------------------------------------------------ |
| |
| void DIPSStorage::RecordStorage(const GURL& url, |
| base::Time time, |
| DIPSCookieMode mode) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| |
| DIPSState state = Read(url); |
| if (!state.site_storage_times().first.has_value() && |
| state.user_interaction_times().last.has_value()) { |
| // First storage, but previous interaction. |
| UmaHistogramTimeToStorage( |
| time - state.user_interaction_times().last.value(), mode); |
| } |
| |
| state.update_site_storage_time(time); |
| } |
| |
| void DIPSStorage::RecordInteraction(const GURL& url, |
| base::Time time, |
| DIPSCookieMode mode) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| |
| DIPSState state = Read(url); |
| if (!state.user_interaction_times().first.has_value() && |
| state.site_storage_times().first.has_value()) { |
| // Site previously wrote to storage. Record metric for the time delay |
| // between first storage and interaction. |
| UmaHistogramTimeToInteraction( |
| time - state.site_storage_times().first.value(), mode); |
| } |
| |
| state.update_user_interaction_time(time); |
| } |
| |
| void DIPSStorage::RecordBounce(const GURL& url, |
| base::Time time, |
| bool stateful) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| DIPSState state = Read(url); |
| state.update_bounce_time(time); |
| if (stateful) { |
| state.update_stateful_bounce_time(time); |
| } |
| } |
| |
| std::vector<std::string> DIPSStorage::GetSitesThatBounced() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| return db_->GetSitesThatBounced(); |
| } |
| |
| std::vector<std::string> DIPSStorage::GetSitesThatBouncedWithState() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| return db_->GetSitesThatBouncedWithState(); |
| } |
| |
| std::vector<std::string> DIPSStorage::GetSitesThatUsedStorage() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| return db_->GetSitesThatUsedStorage(); |
| } |
| |
| void DIPSStorage::DeleteDIPSEligibleState(DIPSCookieMode mode) { |
| std::vector<std::string> sites_to_clear; |
| switch (dips::kTriggeringAction.Get()) { |
| case DIPSTriggeringAction::kStorage: { |
| sites_to_clear = GetSitesThatUsedStorage(); |
| break; |
| } |
| case DIPSTriggeringAction::kBounce: { |
| sites_to_clear = GetSitesThatBounced(); |
| break; |
| } |
| case DIPSTriggeringAction::kStatefulBounce: { |
| sites_to_clear = GetSitesThatBouncedWithState(); |
| break; |
| } |
| } |
| |
| base::UmaHistogramCounts1000(base::StrCat({"Privacy.DIPS.ClearedSitesCount", |
| GetHistogramSuffix(mode)}), |
| sites_to_clear.size()); |
| |
| // Perform clearing of sites. |
| // TODO: Actually clear the site-data for `sites_to_clear` here as well. |
| RemoveRows(sites_to_clear); |
| } |
| |
| /* static */ |
| size_t DIPSStorage::SetPrepopulateChunkSizeForTesting(size_t size) { |
| return std::exchange(g_prepopulate_chunk_size, size); |
| } |
| |
| void DIPSStorage::PrepopulateChunk(PrepopulateArgs args) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK_LE(args.offset, args.sites.size()); |
| |
| size_t chunk_size = |
| std::min(args.sites.size() - args.offset, g_prepopulate_chunk_size); |
| for (size_t i = 0; i < chunk_size; i++) { |
| DIPSState state = ReadSite(args.sites[args.offset + i]); |
| if (state.user_interaction_times().first) { |
| continue; |
| } |
| |
| state.update_user_interaction_time(args.time); |
| |
| if (!state.site_storage_times().first) { |
| // If we set a fake interaction time but no storage time, then when |
| // storage does happen we'll report an incorrect |
| // TimeFromInteractionToStorage metric. So set the storage time too. |
| state.update_site_storage_time(args.time); |
| } |
| } |
| |
| // Increment chunk offset in args and resubmit task if incomplete. |
| args.offset += chunk_size; |
| if (args.offset < args.sites.size()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&DIPSStorage::PrepopulateChunk, |
| weak_factory_.GetWeakPtr(), std::move(args))); |
| } |
| } |