blob: b0291dcc9ae30ff530d0a8fe3e4f9884b8f3f0ef [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 "content/browser/btm/btm_database.h"
#include <cstddef>
#include <limits>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "content/browser/btm/btm_database_migrator.h"
#include "content/browser/btm/btm_utils.h"
#include "content/public/common/content_features.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"
#include "sql/init_status.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
namespace content {
namespace {
// NOTE: This is flag is intended for local testing and debugging only.
BASE_FEATURE(kDisableExclusiveLockingOnDipsDatabase,
base::FEATURE_DISABLED_BY_DEFAULT);
constexpr char kTimerLastFiredKey[] = "timer_last_fired";
std::optional<base::Time> ColumnOptionalTime(sql::Statement& statement,
int column_index) {
if (statement.GetColumnType(column_index) == sql::ColumnType::kNull) {
return std::nullopt;
}
return statement.ColumnTime(column_index);
}
TimestampRange RangeFromColumns(sql::Statement& statement,
int start_column_idx,
int end_column_idx,
std::vector<BtmErrorCode>& errors) {
std::optional<base::Time> first_time =
ColumnOptionalTime(statement, start_column_idx);
std::optional<base::Time> last_time =
ColumnOptionalTime(statement, end_column_idx);
if (!first_time.has_value() && !last_time.has_value()) {
return std::nullopt;
}
if (!first_time.has_value()) {
errors.push_back(BtmErrorCode::kRead_OpenEndedRange_NullStart);
return std::nullopt;
}
if (!last_time.has_value()) {
errors.push_back(BtmErrorCode::kRead_OpenEndedRange_NullEnd);
return std::nullopt;
}
return std::make_pair(first_time.value(), last_time.value());
}
// Binds either the start/ends of `range` or NULL at
// `start_param_idx`/`end_param_idx` in `statement` if time is provided.
void BindTimesOrNull(sql::Statement& statement,
TimestampRange time,
int start_param_idx,
int end_param_idx) {
if (time.has_value()) {
statement.BindTime(start_param_idx, time->first);
statement.BindTime(end_param_idx, time->second);
} else {
statement.BindNull(start_param_idx);
statement.BindNull(end_param_idx);
}
}
} // namespace
BtmDatabase::BtmDatabase(const std::optional<base::FilePath>& db_path)
: db_path_(db_path.value_or(base::FilePath())) {
DCHECK(base::FeatureList::IsEnabled(features::kBtm));
sql::DatabaseOptions db_options =
sql::DatabaseOptions()
.set_wal_mode(true)
.set_cache_size(32)
.set_exclusive_locking(!base::FeatureList::IsEnabled(
kDisableExclusiveLockingOnDipsDatabase));
db_ = std::make_unique<sql::Database>(db_options, sql::Database::Tag("DIPS"));
base::AssertLongCPUWorkAllowed();
if (db_path.has_value()) {
DCHECK(!db_path->empty())
<< "To create an in-memory BtmDatabase, explicitly pass an "
"std::nullopt `db_path`.";
if (base::PathExists(db_path.value())) {
if (!base::PathIsReadable(db_path.value())) {
DLOG(ERROR) << "The BTM SQLite database is not readable.";
return;
}
if (!base::PathIsWritable(db_path.value())) {
DLOG(ERROR) << "The BTM SQLite database is not writable.";
return;
}
}
}
if (Init() != sql::INIT_OK) {
LOG(WARNING) << "Failed to initialize the DIPS SQLite database.";
}
}
BtmDatabase::~BtmDatabase() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
// Invoked on a db error.
void BtmDatabase::DatabaseErrorCallback(int extended_error,
sql::Statement* stmt) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
sql::UmaHistogramSqliteResult("Privacy.DIPS.DatabaseErrors", extended_error);
if (sql::IsErrorCatastrophic(extended_error) && db_->is_open()) {
// Normally this will poison the database, causing any subsequent operations
// to silently fail without any side effects. However, if RazeAndPoison() is
// called from the error callback in response to an error raised from within
// sql::Database::Open, opening the now-razed database will be retried.
db_->RazeAndPoison();
return;
}
// The default handling is to assert on debug and to ignore on release.
if (!sql::Database::IsExpectedSqliteError(extended_error)) {
DLOG(FATAL) << db_->GetErrorMessage();
}
}
sql::InitStatus BtmDatabase::OpenDatabase() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_);
// If this is not the first call to `OpenDatabase()` which can happen when
// retrying the DB's initialization, then the error callback would've
// previously been set.
db_->reset_error_callback();
db_->set_error_callback(base::BindRepeating(
&BtmDatabase::DatabaseErrorCallback, base::Unretained(this)));
if (in_memory()) {
if (!db_->OpenInMemory()) {
return sql::INIT_FAILURE;
}
} else {
if (!db_->Open(db_path_)) {
return sql::INIT_FAILURE;
}
}
return sql::INIT_OK;
}
bool BtmDatabase::InitTables() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
static constexpr char kBouncesSql[] = // clang-format off
"CREATE TABLE bounces("
"site TEXT PRIMARY KEY NOT NULL,"
"first_user_activation_time INTEGER,"
"last_user_activation_time INTEGER,"
"first_bounce_time INTEGER,"
"last_bounce_time INTEGER,"
"first_web_authn_assertion_time INTEGER,"
"last_web_authn_assertion_time INTEGER"
")";
// clang-format on
DCHECK(db_->IsSQLValid(kBouncesSql));
static constexpr char kPopupsSql[] = // clang-format off
"CREATE TABLE popups("
"opener_site TEXT NOT NULL,"
"popup_site TEXT NOT NULL,"
"access_id INT64,"
"last_popup_time INTEGER,"
"is_current_interaction BOOLEAN,"
"is_authentication_interaction BOOLEAN,"
"PRIMARY KEY (`opener_site`,`popup_site`)"
")";
// clang-format on
DCHECK(db_->IsSQLValid(kPopupsSql));
static constexpr char kConfigSql[] = // clang-format off
"CREATE TABLE config("
"key TEXT NOT NULL,"
"int_value INTEGER,"
"PRIMARY KEY (`key`)"
")";
// clang-format on
DCHECK(db_->IsSQLValid(kConfigSql));
if (!db_->Execute(kConfigSql)) {
return false;
}
return db_->Execute(kBouncesSql) && db_->Execute(kPopupsSql);
}
sql::InitStatus BtmDatabase::InitImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
SCOPED_UMA_HISTOGRAM_TIMER("Privacy.DIPS.Database.Operation.InitTime");
sql::InitStatus status = OpenDatabase();
if (status != sql::INIT_OK) {
return status;
}
DCHECK(db_->is_open());
if (sql::MetaTable::RazeIfIncompatible(
db_.get(), sql::MetaTable::kNoLowestSupportedVersion,
kLatestSchemaVersion) == sql::RazeIfIncompatibleResult::kFailed) {
return sql::INIT_FAILURE;
}
// Scope initialization in a transaction so we can't be partially initialized.
sql::Transaction transaction(db_.get());
if (!transaction.Begin()) {
return sql::INIT_FAILURE;
}
// Check if the table already exists to update schema if needed.
bool table_already_exists = sql::MetaTable::DoesTableExist(db_.get());
// Create the tables.
if (!meta_table_.Init(db_.get(), kLatestSchemaVersion,
kMinCompatibleSchemaVersion)) {
db_->Close();
return sql::INIT_FAILURE;
}
if (table_already_exists
? !MigrateBtmSchemaToLatestVersion(*(db_.get()), meta_table_)
: !InitTables()) {
return sql::INIT_FAILURE;
}
// Initialization is complete.
if (!transaction.Commit()) {
return sql::INIT_FAILURE;
}
return sql::INIT_OK;
}
sql::InitStatus BtmDatabase::Init() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
sql::InitStatus status = InitImpl();
int attempts = 1;
if (status != sql::INIT_OK) {
db_->Close();
// Try to initialize the database once more in case it failed once and was
// razed.
status = InitImpl();
attempts++;
if (status != sql::INIT_OK) {
attempts = 0;
}
}
db_init_ = (status == sql::INIT_OK);
base::UmaHistogramExactLinear("Privacy.DIPS.DatabaseInit", attempts, 3);
last_health_metrics_time_ = clock_->Now();
LogDatabaseMetrics();
return status;
}
void BtmDatabase::LogDatabaseMetrics() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeTicks start_time = base::TimeTicks::Now();
std::optional<int64_t> db_size = base::GetFileSize(db_path_);
if (db_size.has_value()) {
base::UmaHistogramMemoryKB("Privacy.DIPS.DatabaseSize",
db_size.value() / 1024);
}
base::UmaHistogramCounts10000("Privacy.DIPS.DatabaseEntryCount",
GetEntryCount(BtmDatabaseTable::kBounces));
base::UmaHistogramTimes("Privacy.DIPS.DatabaseHealthMetricsTime",
base::TimeTicks::Now() - start_time);
}
bool BtmDatabase::CheckDBInit() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!db_ || !db_->is_open() || !db_init_) {
return false;
}
// Computing these metrics may be costly, so we only do it every
// |kMetricsInterval|.
base::Time now = clock_->Now();
if (now > last_health_metrics_time_ + kMetricsInterval) {
last_health_metrics_time_ = now;
LogDatabaseMetrics();
}
return true;
}
bool BtmDatabase::ExecuteSqlForTesting(const base::cstring_view sql) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
return db_->ExecuteScriptForTesting(sql); // IN-TEST
}
bool BtmDatabase::Write(const std::string& site,
const TimestampRange& user_activation_times,
const TimestampRange& bounce_times,
const TimestampRange& web_authn_assertion_times) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
if (site.empty()) {
base::UmaHistogramEnumeration("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kWrite_EmptySite);
return false;
}
static constexpr char kWriteSql[] = // clang-format off
"INSERT OR REPLACE INTO bounces("
"site,"
"first_user_activation_time,"
"last_user_activation_time,"
"first_bounce_time,"
"last_bounce_time,"
"first_web_authn_assertion_time,"
"last_web_authn_assertion_time"
") VALUES(?,?,?,?,?,?,?)";
// clang-format on
DCHECK(db_->IsSQLValid(kWriteSql));
SCOPED_UMA_HISTOGRAM_TIMER("Privacy.DIPS.Database.Operation.WriteTime");
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kWriteSql));
statement.BindString(0, site);
BindTimesOrNull(statement, user_activation_times, 1, 2);
BindTimesOrNull(statement, bounce_times, 3, 4);
BindTimesOrNull(statement, web_authn_assertion_times, 5, 6);
if (!statement.Run()) {
return false;
}
base::UmaHistogramEnumeration("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kWrite_None);
return true;
}
bool BtmDatabase::WritePopup(const std::string& opener_site,
const std::string& popup_site,
const uint64_t access_id,
const base::Time& popup_time,
bool is_current_interaction,
bool is_authentication_interaction) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
static constexpr char kWriteSql[] = // clang-format off
"INSERT OR REPLACE INTO popups("
"opener_site,"
"popup_site,"
"access_id,"
"last_popup_time,"
"is_current_interaction,"
"is_authentication_interaction"
") VALUES(?,?,?,?,?,?)";
// clang-format on
DCHECK(db_->IsSQLValid(kWriteSql));
SCOPED_UMA_HISTOGRAM_TIMER("Privacy.DIPS.Database.Operation.WritePopupTime");
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kWriteSql));
statement.BindString(0, opener_site);
statement.BindString(1, popup_site);
statement.BindInt64(2, access_id);
statement.BindTime(3, popup_time);
statement.BindBool(4, is_current_interaction);
statement.BindBool(5, is_authentication_interaction);
return statement.Run();
}
std::optional<StateValue> BtmDatabase::Read(const std::string& site) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return std::nullopt;
}
static constexpr char kReadSql[] = R"SQL(
SELECT
site,
first_user_activation_time,
last_user_activation_time,
first_bounce_time,
last_bounce_time,
first_web_authn_assertion_time,
last_web_authn_assertion_time
FROM bounces
WHERE site=?
)SQL";
DCHECK(db_->IsSQLValid(kReadSql));
SCOPED_UMA_HISTOGRAM_TIMER("Privacy.DIPS.Database.Operation.ReadTime");
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kReadSql));
statement.BindString(0, site);
if (!statement.Step()) {
if (statement.Succeeded() && site.empty()) {
base::UmaHistogramEnumeration("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kRead_EmptySite_NotInDb);
}
return std::nullopt;
}
std::optional<base::Time> last_user_activation_time =
ColumnOptionalTime(statement, 2);
std::optional<base::Time> last_web_authn_assertion_time =
ColumnOptionalTime(statement, 6);
// If the last user activation and last web authn assertion have expired,
// treat this entry as not in the database so that callers rewrite the entry
// for `site` as if it were deleted.
if ((last_user_activation_time.has_value() ||
last_web_authn_assertion_time.has_value()) &&
IsNullOrExpired(last_user_activation_time) &&
IsNullOrExpired(last_web_authn_assertion_time)) {
return std::nullopt;
}
std::vector<BtmErrorCode> errors;
TimestampRange user_activation_times =
RangeFromColumns(statement, 1, 2, errors);
TimestampRange bounce_times = RangeFromColumns(statement, 3, 4, errors);
TimestampRange web_authn_assertion_times =
RangeFromColumns(statement, 5, 6, errors);
if (site.empty()) {
errors.push_back(BtmErrorCode::kRead_EmptySite_InDb);
}
if (errors.empty()) {
errors.push_back(BtmErrorCode::kRead_None);
}
for (const BtmErrorCode& error : errors) {
base::UmaHistogramEnumeration("Privacy.DIPS.DIPSErrorCodes", error);
}
// If `site` is an empty string, treat the entry as not in the database and
// remove it. See crbug.com/1447035 for context.
if (site.empty()) {
RemoveRow(BtmDatabaseTable::kBounces, site);
return std::nullopt;
}
return StateValue{user_activation_times, bounce_times,
web_authn_assertion_times};
}
std::optional<PopupsStateValue> BtmDatabase::ReadPopup(
const std::string& opener_site,
const std::string& popup_site) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return std::nullopt;
}
static constexpr char kReadSql[] = // clang-format off
"SELECT opener_site,"
"popup_site,"
"access_id,"
"last_popup_time, "
"is_current_interaction, "
"is_authentication_interaction "
"FROM popups "
"WHERE opener_site=? AND popup_site=?";
// clang-format on
DCHECK(db_->IsSQLValid(kReadSql));
SCOPED_UMA_HISTOGRAM_TIMER("Privacy.DIPS.Database.Operation.ReadPopupTime");
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kReadSql));
statement.BindString(0, opener_site);
statement.BindString(1, popup_site);
if (!statement.Step()) {
return std::nullopt;
}
uint64_t access_id = statement.ColumnInt64(2);
std::optional<base::Time> popup_time = ColumnOptionalTime(statement, 3);
if (!popup_time.has_value()) {
return std::nullopt;
}
bool is_current_interaction = statement.ColumnBool(4);
bool is_authentication_interaction = statement.ColumnBool(5);
return PopupsStateValue{access_id, popup_time.value(), is_current_interaction,
is_authentication_interaction};
}
std::vector<PopupWithTime> BtmDatabase::ReadRecentPopupsWithInteraction(
const base::TimeDelta& lookback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return std::vector<PopupWithTime>();
}
static constexpr char kReadSql[] = // clang-format off
"SELECT opener_site,popup_site,last_popup_time "
"FROM popups "
"WHERE "
"is_current_interaction "
"AND last_popup_time>?";
// clang-format on
DCHECK(db_->IsSQLValid(kReadSql));
SCOPED_UMA_HISTOGRAM_TIMER(
"Privacy.DIPS.Database.Operation.ReadRecentPopupsWithInteractionTime");
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kReadSql));
statement.BindTime(0, clock_->Now() - lookback);
std::vector<PopupWithTime> popups;
while (statement.Step()) {
popups.push_back(PopupWithTime{.opener_site = statement.ColumnString(0),
.popup_site = statement.ColumnString(1),
.last_popup_time = statement.ColumnTime(2)});
}
return popups;
}
std::vector<std::string> BtmDatabase::GetAllSitesForTesting(
BtmDatabaseTable table) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
std::vector<std::string> sites;
if (table == BtmDatabaseTable::kBounces) {
static constexpr char kReadBounceTableSqlStr[] = "SELECT site FROM bounces";
DCHECK(db_->IsSQLValid(kReadBounceTableSqlStr));
sql::Statement s_bounces(
db_->GetCachedStatement(SQL_FROM_HERE, kReadBounceTableSqlStr));
while (s_bounces.Step()) {
sites.push_back(s_bounces.ColumnString(0));
}
} else if (table == BtmDatabaseTable::kPopups) {
static constexpr char kReadPopupTableSqlStr[] =
"SELECT opener_site,popup_site FROM popups";
DCHECK(db_->IsSQLValid(kReadPopupTableSqlStr));
sql::Statement s_popups(
db_->GetCachedStatement(SQL_FROM_HERE, kReadPopupTableSqlStr));
while (s_popups.Step()) {
sites.push_back(s_popups.ColumnString(0));
sites.push_back(s_popups.ColumnString(1));
}
}
return sites;
}
std::vector<std::string> BtmDatabase::GetSitesThatBounced(
base::TimeDelta grace_period) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
SCOPED_UMA_HISTOGRAM_TIMER(
"Privacy.DIPS.Database.Operation.GetSitesThatBouncedTime");
ClearExpiredRows();
static constexpr char kBounceSql[] = // clang-format off
"SELECT site FROM bounces "
"WHERE "
"first_bounce_time<? "
"AND last_user_activation_time IS NULL "
"AND last_web_authn_assertion_time IS NULL "
"ORDER BY site";
// clang-format on
DCHECK(db_->IsSQLValid(kBounceSql));
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kBounceSql));
statement.BindTime(0, clock_->Now() - grace_period);
std::vector<std::string> sites;
while (statement.Step()) {
sites.push_back(statement.ColumnString(0));
}
return sites;
}
std::set<std::string> BtmDatabase::FilterSites(
const std::set<std::string>& sites,
BounceFilterType filter) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
static constexpr char kReadSqlFmt[] = R"SQL(
SELECT
site,
last_user_activation_time,
last_web_authn_assertion_time
FROM bounces
WHERE site IN (%s)
)SQL";
// Interpolate unnamed parameters (i.e. the "?") into the SQL query for each
// site in the list.
const std::string kReadSql = absl::StrFormat(
kReadSqlFmt,
base::JoinString(std::vector<std::string_view>(sites.size(), "?"), ","));
DCHECK(db_->IsSQLValid(kReadSql));
std::string histogram_name;
switch (filter) {
case BounceFilterType::kProtectiveEvent:
histogram_name =
"Privacy.DIPS.Database.Operation.FilterSitesWithProtectiveEventTime";
break;
case BounceFilterType::kUserActivation:
histogram_name =
"Privacy.DIPS.Database.Operation.FilterSitesWithUserActivationTime";
break;
case BounceFilterType::kWebAuthnAssertion:
histogram_name =
"Privacy.DIPS.Database.Operation."
"FilterSitesWithWebAuthnAssertionTime";
break;
}
base::ScopedUmaHistogramTimer histogram_timer(histogram_name);
ClearExpiredRows();
sql::Statement statement(db_->GetUniqueStatement(kReadSql));
int param_index = 0;
for (std::string site : sites) {
statement.BindString(param_index++, site);
}
std::set<std::string> filtered_sites;
while (statement.Step()) {
std::optional<base::Time> last_user_activation_time =
ColumnOptionalTime(statement, 1);
std::optional<base::Time> last_web_authn_assertion_time =
ColumnOptionalTime(statement, 2);
bool should_pass_filter = false;
switch (filter) {
case BounceFilterType::kProtectiveEvent:
should_pass_filter = last_user_activation_time.has_value() ||
last_web_authn_assertion_time.has_value();
break;
case BounceFilterType::kUserActivation:
should_pass_filter = last_user_activation_time.has_value();
break;
case BounceFilterType::kWebAuthnAssertion:
should_pass_filter = last_web_authn_assertion_time.has_value();
break;
}
if (should_pass_filter) {
filtered_sites.insert(statement.ColumnString(0));
}
}
return filtered_sites;
}
size_t BtmDatabase::ClearExpiredRows() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(clock_);
if (!CheckDBInit()) {
return false;
}
size_t change_count = 0;
// NOTE: The SQLITE `MAX` and `MIN` return `NULL` if any value is `NULL`.
// That's why `COALESCE` is used.
static constexpr char kClearAllExpiredBouncesTableSql[] = // clang-format off
"DELETE FROM bounces "
"WHERE MAX("
"COALESCE(last_user_activation_time,last_web_authn_assertion_time),"
"COALESCE(last_web_authn_assertion_time,last_user_activation_time)"
")<?";
// clang-format on
DCHECK(db_->IsSQLValid(kClearAllExpiredBouncesTableSql));
sql::Statement bounces_statement(
db_->GetCachedStatement(SQL_FROM_HERE, kClearAllExpiredBouncesTableSql));
bounces_statement.BindTime(
0, clock_->Now() - features::kBtmInteractionTtl.Get());
if (!bounces_statement.Run()) {
return 0;
}
change_count += db_->GetLastChangeCount();
static constexpr char kClearAllExpiredPopupsTableSql[] =
"DELETE FROM popups "
"WHERE last_popup_time<?";
DCHECK(db_->IsSQLValid(kClearAllExpiredPopupsTableSql));
sql::Statement popups_statement(
db_->GetCachedStatement(SQL_FROM_HERE, kClearAllExpiredPopupsTableSql));
popups_statement.BindTime(0, clock_->Now() - kPopupTtl);
if (!popups_statement.Run()) {
return 0;
}
change_count += db_->GetLastChangeCount();
return change_count;
}
bool BtmDatabase::RemoveRow(const BtmDatabaseTable table,
std::string_view site) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
ClearExpiredRows();
if (table == BtmDatabaseTable::kBounces) {
static constexpr char kRemoveSql[] = "DELETE FROM bounces WHERE site=?";
DCHECK(db_->IsSQLValid(kRemoveSql));
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kRemoveSql));
statement.BindString(0, site);
return statement.Run();
} else if (table == BtmDatabaseTable::kPopups) {
static constexpr char kRemoveSql[] =
"DELETE FROM popups WHERE opener_site=? OR popup_site=?";
DCHECK(db_->IsSQLValid(kRemoveSql));
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kRemoveSql));
statement.BindString(0, site);
statement.BindString(1, site);
return statement.Run();
}
// This should never be called - both BtmDatabaseTable types are handled and
// return above.
return false;
}
bool BtmDatabase::RemoveRows(const BtmDatabaseTable table,
const std::vector<std::string>& sites) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
if (sites.empty()) {
return true;
}
SCOPED_UMA_HISTOGRAM_TIMER("Privacy.DIPS.Database.Operation.RemoveRowsTime");
const std::string site_list =
base::JoinString(std::vector<std::string_view>(sites.size(), "?"), ",");
if (table == BtmDatabaseTable::kBounces) {
sql::Statement statement(db_->GetUniqueStatement(base::StrCat(
{"DELETE FROM bounces ", "WHERE site IN(", site_list, ")"})));
for (size_t i = 0; i < sites.size(); i++) {
statement.BindString(i, sites[i]);
}
return statement.Run();
} else if (table == BtmDatabaseTable::kPopups) {
sql::Statement statement(db_->GetUniqueStatement(
base::StrCat({"DELETE FROM popups ", "WHERE opener_site IN(", site_list,
") OR popup_site IN(", site_list, ")"})));
for (size_t i = 0; i < sites.size(); i++) {
// There are 2 * sites.size() total bind locations, in the first and
// second site_list. Each site should be bound in both lists.
statement.BindString(i, sites[i]);
statement.BindString(i + sites.size(), sites[i]);
}
return statement.Run();
}
// This should never be called - both BtmDatabaseTable types are handled and
// return above.
return false;
}
bool BtmDatabase::RemoveEventsByTime(const base::Time& delete_begin,
const base::Time& delete_end,
const BtmEventRemovalType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
ClearExpiredRows();
sql::Transaction transaction(db_.get());
if (!transaction.Begin()) {
return false;
}
GarbageCollect();
return (ClearTimestamps(delete_begin, delete_end, type) &&
transaction.Commit());
}
bool BtmDatabase::RemoveEventsBySite(bool preserve,
const std::vector<std::string>& sites,
const BtmEventRemovalType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
sql::Transaction transaction(db_.get());
if (!transaction.Begin()) {
return false;
}
GarbageCollect();
if (!ClearTimestampsBySite(preserve, sites, type)) {
return false;
}
return transaction.Commit();
}
bool BtmDatabase::ClearTimestamps(const base::Time& delete_begin,
const base::Time& delete_end,
const BtmEventRemovalType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
SCOPED_UMA_HISTOGRAM_TIMER(
"Privacy.DIPS.Database.Operation.ClearTimestampsTime");
ClearExpiredRows();
if ((type & BtmEventRemovalType::kHistory) == BtmEventRemovalType::kHistory) {
static constexpr char kClearUserActivationSql[] = // clang-format off
"UPDATE bounces SET "
"first_user_activation_time=NULL,"
"last_user_activation_time=NULL "
"WHERE first_user_activation_time>=? AND "
"last_user_activation_time<=?";
// clang-format on
DCHECK(db_->IsSQLValid(kClearUserActivationSql));
sql::Statement s_clear_user_activation(
db_->GetCachedStatement(SQL_FROM_HERE, kClearUserActivationSql));
s_clear_user_activation.BindTime(0, delete_begin);
s_clear_user_activation.BindTime(1, delete_end);
if (!s_clear_user_activation.Run()) {
return false;
}
static constexpr char kClearWaaSql[] = // clang-format off
"UPDATE bounces SET "
"first_web_authn_assertion_time=NULL,"
"last_web_authn_assertion_time=NULL "
"WHERE first_web_authn_assertion_time>=? AND "
"last_web_authn_assertion_time<=?";
// clang-format on
DCHECK(db_->IsSQLValid(kClearWaaSql));
sql::Statement s_clear_waa(
db_->GetCachedStatement(SQL_FROM_HERE, kClearWaaSql));
s_clear_waa.BindTime(0, delete_begin);
s_clear_waa.BindTime(1, delete_end);
if (!s_clear_waa.Run()) {
return false;
}
static constexpr char kClearPopupsSql[] = // clang-format off
"DELETE FROM popups "
"WHERE last_popup_time>=? AND last_popup_time<=?";
// clang-format on
DCHECK(db_->IsSQLValid(kClearPopupsSql));
sql::Statement s_clear_popups(
db_->GetCachedStatement(SQL_FROM_HERE, kClearPopupsSql));
s_clear_popups.BindTime(0, delete_begin);
s_clear_popups.BindTime(1, delete_end);
if (!s_clear_popups.Run()) {
return false;
}
}
if ((type & BtmEventRemovalType::kStorage) == BtmEventRemovalType::kStorage) {
static constexpr char kClearBounceSql[] = // clang-format off
"UPDATE bounces SET "
"first_bounce_time=NULL,"
"last_bounce_time=NULL "
"WHERE first_bounce_time>=? AND "
"last_bounce_time<=?";
// clang-format on
DCHECK(db_->IsSQLValid(kClearBounceSql));
sql::Statement s_clear_bounce(
db_->GetCachedStatement(SQL_FROM_HERE, kClearBounceSql));
s_clear_bounce.BindTime(0, delete_begin);
s_clear_bounce.BindTime(1, delete_end);
if (!s_clear_bounce.Run()) {
return false;
}
}
return (RemoveEmptyRows() &&
AdjustFirstTimestamps(delete_begin, delete_end, type) &&
AdjustLastTimestamps(delete_begin, delete_end, type));
}
bool BtmDatabase::AdjustFirstTimestamps(const base::Time& delete_begin,
const base::Time& delete_end,
const BtmEventRemovalType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
ClearExpiredRows();
if (delete_end == base::Time::Max()) {
// When `delete_end` is `base::Time::Max()`, any timestamp range that would
// be altered by the below queries should have already been removed by
// ClearTimestamps(), which MUST always be called before this method.
return true;
}
if ((type & BtmEventRemovalType::kHistory) == BtmEventRemovalType::kHistory) {
static constexpr char kUpdateFirstUserActivationSql[] = // clang-format off
"UPDATE bounces SET first_user_activation_time=?2 "
"WHERE first_user_activation_time>=?1 AND "
"first_user_activation_time<?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateFirstUserActivationSql));
sql::Statement s_first_user_activation(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstUserActivationSql));
s_first_user_activation.BindTime(0, delete_begin);
s_first_user_activation.BindTime(1, delete_end);
if (!s_first_user_activation.Run()) {
return false;
}
static constexpr char kUpdateFirstWaaSql[] = // clang-format off
"UPDATE bounces SET first_web_authn_assertion_time=?2 "
"WHERE first_web_authn_assertion_time>=?1 AND "
"first_web_authn_assertion_time<?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateFirstWaaSql));
sql::Statement s_first_waa(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstWaaSql));
s_first_waa.BindTime(0, delete_begin);
s_first_waa.BindTime(1, delete_end);
if (!s_first_waa.Run()) {
return false;
}
}
if ((type & BtmEventRemovalType::kStorage) == BtmEventRemovalType::kStorage) {
static constexpr char kUpdateFirstBounceSql[] = // clang-format off
"UPDATE bounces SET first_bounce_time=?2 "
"WHERE first_bounce_time>=?1 AND "
"first_bounce_time<?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateFirstBounceSql));
sql::Statement s_first_bounce(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstBounceSql));
s_first_bounce.BindTime(0, delete_begin);
s_first_bounce.BindTime(1, delete_end);
if (!s_first_bounce.Run()) {
return false;
}
}
return true;
}
bool BtmDatabase::AdjustLastTimestamps(const base::Time& delete_begin,
const base::Time& delete_end,
const BtmEventRemovalType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
ClearExpiredRows();
if (delete_begin == base::Time::Min()) {
// When `delete_begin` is `base::Time::Min()`, any timestamp range that
// would be altered by the below queries should have already been removed by
// ClearTimestamps(), which MUST always be called before this method.
return true;
}
if ((type & BtmEventRemovalType::kHistory) == BtmEventRemovalType::kHistory) {
static constexpr char kUpdateLastUserActivationSql[] = // clang-format off
"UPDATE bounces SET last_user_activation_time=?1 "
"WHERE last_user_activation_time>?1 AND "
"last_user_activation_time<=?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateLastUserActivationSql));
sql::Statement s_last_user_activation(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateLastUserActivationSql));
s_last_user_activation.BindTime(0, delete_begin);
s_last_user_activation.BindTime(1, delete_end);
if (!s_last_user_activation.Run()) {
return false;
}
static constexpr char kUpdateLastWaaSql[] = // clang-format off
"UPDATE bounces SET last_web_authn_assertion_time=?1 "
"WHERE last_web_authn_assertion_time>?1 AND "
"last_web_authn_assertion_time<=?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateLastWaaSql));
sql::Statement s_last_waa(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateLastWaaSql));
s_last_waa.BindTime(0, delete_begin);
s_last_waa.BindTime(1, delete_end);
if (!s_last_waa.Run()) {
return false;
}
}
if ((type & BtmEventRemovalType::kStorage) == BtmEventRemovalType::kStorage) {
static constexpr char kUpdateLastBounceSql[] = // clang-format off
"UPDATE bounces SET last_bounce_time=?1 "
"WHERE last_bounce_time>?1 AND "
"last_bounce_time<=?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateLastBounceSql));
sql::Statement s_last_bounce(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateLastBounceSql));
s_last_bounce.BindTime(0, delete_begin);
s_last_bounce.BindTime(1, delete_end);
if (!s_last_bounce.Run()) {
return false;
}
}
return true;
}
bool BtmDatabase::ClearTimestampsBySite(bool preserve,
const std::vector<std::string>& sites,
const BtmEventRemovalType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (sites.empty()) {
return true;
}
std::string placeholders =
base::JoinString(std::vector<std::string_view>(sites.size(), "?"), ",");
if ((type & BtmEventRemovalType::kStorage) == BtmEventRemovalType::kStorage) {
sql::Statement s_clear_storage(db_->GetUniqueStatement( // clang-format off
base::StrCat({"UPDATE bounces SET "
"first_bounce_time=NULL,"
"last_bounce_time=NULL "
"WHERE site ", (preserve ? "NOT " : ""),
"IN(", placeholders, ")" }) // clang-format on
));
for (size_t i = 0; i < sites.size(); i++) {
s_clear_storage.BindString(i, sites[i]);
}
if (!s_clear_storage.Run()) {
return false;
}
}
return RemoveEmptyRows();
}
bool BtmDatabase::RemoveEmptyRows() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
static constexpr char kCleanUpSql[] = // clang-format off
"DELETE FROM bounces "
"WHERE first_user_activation_time IS NULL "
"AND last_user_activation_time IS NULL "
"AND first_bounce_time IS NULL "
"AND last_bounce_time IS NULL "
"AND first_web_authn_assertion_time IS NULL "
"AND last_web_authn_assertion_time IS NULL";
// clang-format on
DCHECK(db_->IsSQLValid(kCleanUpSql));
sql::Statement s_clean(db_->GetCachedStatement(SQL_FROM_HERE, kCleanUpSql));
// Clearing the `popups` table is unnecessary because there are no operations
// to nullify individual rows.
return s_clean.Run();
}
size_t BtmDatabase::GetEntryCount(const BtmDatabaseTable table) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return 0;
}
ClearExpiredRows();
if (table == BtmDatabaseTable::kBounces) {
static constexpr char kBounceTableEntryCountSqlStr[] =
"SELECT COUNT(*) FROM bounces";
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kBounceTableEntryCountSqlStr));
return (statement.Step() ? statement.ColumnInt(0) : 0);
} else if (table == BtmDatabaseTable::kPopups) {
static constexpr char kPopupTableEntryCountSqlStr[] =
"SELECT COUNT(*) FROM popups";
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kPopupTableEntryCountSqlStr));
return (statement.Step() ? statement.ColumnInt(0) : 0);
}
// This should never be called - both BtmDatabaseTable types are handled and
// return above.
return false;
}
size_t BtmDatabase::GarbageCollect() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return 0;
}
size_t num_deleted = ClearExpiredRows();
for (const BtmDatabaseTable table :
{BtmDatabaseTable::kBounces, BtmDatabaseTable::kPopups}) {
// NOTE: `GetEntryCount()` might perform other row deletions whilst
// re-calling `ClearExpiredRows()`, but possible precision lost in the final
// num_delete isn't deemed crucial.
const size_t num_entries = GetEntryCount(table);
if (num_entries <= max_entries_) {
continue;
}
const int purge_goal = num_entries - (max_entries_ - purge_entries_);
DCHECK_GT(purge_goal, 0);
num_deleted += GarbageCollectOldest(table, purge_goal);
}
return num_deleted;
}
size_t BtmDatabase::GarbageCollectOldest(const BtmDatabaseTable table,
int purge_goal) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return 0;
}
if (table == BtmDatabaseTable::kBounces) {
static constexpr char kGarbageCollectOldestSql[] = // clang-format off
"DELETE FROM bounces "
"WHERE site IN("
"SELECT site FROM bounces "
"ORDER BY "
"MAX("
"COALESCE("
"last_user_activation_time,"
"last_web_authn_assertion_time"
"),"
"COALESCE("
"last_web_authn_assertion_time,"
"last_user_activation_time"
")"
") ASC,"
"last_user_activation_time ASC,"
"last_web_authn_assertion_time ASC "
"LIMIT ?"
")";
// clang-format on
DCHECK(db_->IsSQLValid(kGarbageCollectOldestSql));
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kGarbageCollectOldestSql));
statement.BindInt(0, purge_goal);
return statement.Run() ? db_->GetLastChangeCount() : 0;
} else if (table == BtmDatabaseTable::kPopups) {
static constexpr char kGarbageCollectOldestSql[] = // clang-format off
"DELETE FROM popups "
"WHERE (opener_site,popup_site) IN("
"SELECT opener_site,popup_site "
"FROM popups "
"ORDER BY last_popup_time ASC "
"LIMIT ?"
")";
// clang-format on
DCHECK(db_->IsSQLValid(kGarbageCollectOldestSql));
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kGarbageCollectOldestSql));
statement.BindInt(0, purge_goal);
return statement.Run() ? db_->GetLastChangeCount() : 0;
}
// This should never be called - both BtmDatabaseTable types are handled and
// return above.
return false;
}
std::vector<std::string> BtmDatabase::GetGarbageCollectOldestSitesForTesting(
BtmDatabaseTable table) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
std::vector<std::string> sites;
if (table == BtmDatabaseTable::kBounces) {
static constexpr char kReadSql[] = // clang-format off
"SELECT site FROM bounces "
"ORDER BY "
"MAX("
"COALESCE("
"last_user_activation_time,"
"last_web_authn_assertion_time"
"),"
"COALESCE("
"last_web_authn_assertion_time,"
"last_user_activation_time"
")"
") ASC,"
"last_user_activation_time ASC,"
"last_web_authn_assertion_time ASC";
// clang-format on
DCHECK(db_->IsSQLValid(kReadSql));
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kReadSql));
while (statement.Step()) {
sites.push_back(statement.ColumnString(0));
}
} else if (table == BtmDatabaseTable::kPopups) {
static constexpr char kReadSql[] =
"SELECT opener_site,popup_site "
"FROM popups "
"ORDER BY last_popup_time ASC";
DCHECK(db_->IsSQLValid(kReadSql));
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kReadSql));
while (statement.Step()) {
sites.push_back(statement.ColumnString(0));
}
}
return sites;
}
bool BtmDatabase::SetConfigValue(std::string_view key, int64_t value) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
static constexpr char kInsertValueSql[] =
"INSERT OR REPLACE INTO config(key,int_value) VALUES(?,?)";
DCHECK(db_->IsSQLValid(kInsertValueSql));
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kInsertValueSql));
statement.BindString(0, key);
statement.BindInt64(1, value);
return statement.Run();
}
std::optional<int64_t> BtmDatabase::GetConfigValue(std::string_view key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return std::nullopt;
}
static constexpr char kSelectValueSql[] =
"SELECT int_value FROM config WHERE key = ?";
DCHECK(db_->IsSQLValid(kSelectValueSql));
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kSelectValueSql));
statement.BindString(0, key);
if (!statement.Step()) {
return std::nullopt;
}
return statement.ColumnInt64(0);
}
std::optional<base::Time> BtmDatabase::GetTimerLastFired() {
std::optional<int64_t> raw_value = GetConfigValue(kTimerLastFiredKey);
if (!raw_value.has_value()) {
return std::nullopt;
}
return base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(*raw_value));
}
bool BtmDatabase::SetTimerLastFired(base::Time time) {
return SetConfigValue(kTimerLastFiredKey,
time.ToDeltaSinceWindowsEpoch().InMicroseconds());
}
bool BtmDatabase::IsNullOrExpired(std::optional<base::Time> time) {
return !time.has_value() ||
time.value() + features::kBtmInteractionTtl.Get() < clock_->Now();
}
} // namespace content