| // 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 |