blob: 8158c0d58744fe44f0777881c946089780af3334 [file] [log] [blame]
// 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/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/dips/dips_utils.h"
#include "content/public/common/content_features.h"
#include "content/public/common/dips_utils.h"
#include "services/network/public/mojom/clear_data_filter.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "url/gurl.h"
namespace {
// 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,
base::OnceClosure on_complete)
: time(time),
offset(offset),
sites(std::move(sites)),
on_complete(std::move(on_complete)) {}
DIPSStorage::PrepopulateArgs::PrepopulateArgs(PrepopulateArgs&&) = default;
DIPSStorage::PrepopulateArgs::~PrepopulateArgs() = default;
DIPSStorage::DIPSStorage(const std::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_);
std::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.has_value() ||
state->user_interaction_times.has_value() ||
state->stateful_bounce_times.has_value() ||
state->bounce_times.has_value() ||
state->web_authn_assertion_times.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(), state.web_authn_assertion_times());
}
std::optional<PopupsStateValue> DIPSStorage::ReadPopup(
const std::string& first_party_site,
const std::string& tracking_site) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
return db_->ReadPopup(first_party_site, tracking_site);
}
std::vector<PopupWithTime> DIPSStorage::ReadRecentPopupsWithInteraction(
const base::TimeDelta& lookback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
return db_->ReadRecentPopupsWithInteraction(lookback);
}
bool DIPSStorage::WritePopup(const std::string& first_party_site,
const std::string& tracking_site,
const uint64_t access_id,
const base::Time& popup_time,
bool is_current_interaction) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
return db_->WritePopup(first_party_site, tracking_site, access_id, popup_time,
is_current_interaction);
}
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 (delete_begin.is_null()) {
delete_begin = base::Time::Min();
}
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::Min() || delete_end != base::Time::Max()) {
// TODO (kaklilu@): Add a UMA metric to record if this happens.
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_);
DCHECK(db_);
db_->RemoveRows(DIPSDatabaseTable::kBounces, sites);
}
void DIPSStorage::RemoveRowsWithoutProtectiveEvent(
const std::set<std::string>& sites) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
std::set<std::string> filtered_sites =
FilterSitesWithoutProtectiveEvent(sites);
RemoveRows(
std::vector<std::string>(filtered_sites.begin(), filtered_sites.end()));
}
// 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);
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);
state.update_user_interaction_time(time);
}
void DIPSStorage::RecordWebAuthnAssertion(const GURL& url,
base::Time time,
DIPSCookieMode mode) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
DIPSState state = Read(url);
state.update_web_authn_assertion_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::set<std::string> DIPSStorage::FilterSitesWithoutProtectiveEvent(
std::set<std::string> sites) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
std::set<std::string> interacted_sites =
db_->FilterSitesWithProtectiveEvent(sites);
for (const auto& site : interacted_sites) {
if (sites.count(site)) {
sites.erase(site);
}
}
return sites;
}
std::vector<std::string> DIPSStorage::GetSitesThatBounced(
base::TimeDelta grace_period) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
return db_->GetSitesThatBounced(grace_period);
}
std::vector<std::string> DIPSStorage::GetSitesThatBouncedWithState(
base::TimeDelta grace_period) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
return db_->GetSitesThatBouncedWithState(grace_period);
}
std::vector<std::string> DIPSStorage::GetSitesThatUsedStorage(
base::TimeDelta grace_period) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
return db_->GetSitesThatUsedStorage(grace_period);
}
std::vector<std::string> DIPSStorage::GetSitesToClear(
std::optional<base::TimeDelta> custom_period) const {
std::vector<std::string> sites_to_clear;
base::TimeDelta grace_period =
custom_period.value_or(features::kDIPSGracePeriod.Get());
switch (features::kDIPSTriggeringAction.Get()) {
case content::DIPSTriggeringAction::kNone: {
return {};
}
case content::DIPSTriggeringAction::kStorage: {
sites_to_clear = GetSitesThatUsedStorage(grace_period);
break;
}
case content::DIPSTriggeringAction::kBounce: {
sites_to_clear = GetSitesThatBounced(grace_period);
break;
}
case content::DIPSTriggeringAction::kStatefulBounce: {
sites_to_clear = GetSitesThatBouncedWithState(grace_period);
break;
}
}
return sites_to_clear;
}
bool DIPSStorage::DidSiteHaveInteractionSince(const GURL& url,
base::Time bound) {
auto last_user_interaction_time = LastInteractionTime(url);
return last_user_interaction_time.has_value() &&
last_user_interaction_time >= bound;
}
std::optional<base::Time> DIPSStorage::LastInteractionTime(const GURL& url) {
const DIPSState state = Read(url);
if (!state.user_interaction_times().has_value()) {
return std::nullopt;
}
return state.user_interaction_times()->second;
}
/* static */
void DIPSStorage::DeleteDatabaseFiles(base::FilePath path,
base::OnceClosure on_complete) {
// TODO (jdh): Decide how to handle the case of failing to delete db files.
base::ThreadPool::PostTaskAndReply(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(IgnoreResult(&sql::Database::Delete), std::move(path)),
std::move(on_complete));
}
/* 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]);
// TODO(crbug.com/40913154): Verify whether we need to ignore if WAA is
// non-empty regardless of interaction.
if (state.user_interaction_times().has_value()) {
continue;
}
state.update_user_interaction_time(args.time);
if (!state.site_storage_times().has_value()) {
// 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)));
} else {
db_->MarkAsPrepopulated();
std::move(args.on_complete).Run();
}
}
std::optional<base::Time> DIPSStorage::GetTimerLastFired() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return db_->GetTimerLastFired();
}
bool DIPSStorage::SetTimerLastFired(base::Time time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return db_->SetTimerLastFired(time);
}