| // Copyright 2022 The Chromium Authors. All rights reserved. |
| // 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_database.h" |
| |
| #include <cstddef> |
| #include <limits> |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/dips/dips_features.h" |
| #include "chrome/browser/dips/dips_utils.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/types/optional.h" |
| |
| namespace { |
| |
| absl::optional<base::Time> ColumnOptionalTime(sql::Statement* statement, |
| int column_index) { |
| if (statement->GetColumnType(column_index) == sql::ColumnType::kNull) { |
| return absl::nullopt; |
| } |
| return statement->ColumnTime(column_index); |
| } |
| |
| // Binds either the value of `time` or NULL at `param_index` in `statement` if |
| // time is provided. |
| void BindTimeOrNull(sql::Statement& statement, |
| absl::optional<base::Time> time, |
| int param_index) { |
| if (time.has_value()) { |
| statement.BindTime(param_index, time.value()); |
| } else { |
| statement.BindNull(param_index); |
| } |
| } |
| |
| // Version number of the database. |
| // NOTE: When changing the version, add a new golden file for the new version |
| // and a test to verify that Init() works with it. |
| const int kCurrentVersionNumber = 2; |
| const int kCompatibleVersionNumber = 1; |
| |
| } // namespace |
| |
| // See comments at declaration of these variables in dips_database.h |
| // for details. |
| const base::TimeDelta DIPSDatabase::kMetricsInterval = base::Hours(24); |
| |
| DIPSDatabase::DIPSDatabase(const absl::optional<base::FilePath>& db_path) |
| : db_path_(db_path.value_or(base::FilePath())), |
| db_(std::make_unique<sql::Database>( |
| sql::DatabaseOptions{.exclusive_locking = true, |
| .page_size = 4096, |
| .cache_size = 32})) { |
| DCHECK(base::FeatureList::IsEnabled(dips::kFeature)); |
| base::AssertLongCPUWorkAllowed(); |
| if (db_path.has_value()) { |
| DCHECK(!db_path->empty()) |
| << "To create an in-memory DIPSDatabase, explicitly pass an " |
| "absl::nullopt `db_path`."; |
| } |
| |
| if (Init() != sql::INIT_OK) |
| LOG(WARNING) << "Failed to initialize the DIPS SQLite database."; |
| } |
| |
| DIPSDatabase::~DIPSDatabase() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| // Invoked on a db error. |
| void DIPSDatabase::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)) { |
| // Normally this will poison the database, causing any subsequent operations |
| // to silently fail without any side effects. However, if RazeAndClose() 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_->RazeAndClose(); |
| } |
| |
| // 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 DIPSDatabase::OpenDatabase() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_); |
| db_->set_histogram_tag("DIPS"); |
| db_->set_error_callback(base::BindRepeating( |
| &DIPSDatabase::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 DIPSDatabase::InitTables() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| static constexpr char kBounceSql[] = |
| // clang-format off |
| "CREATE TABLE bounces(" |
| "site TEXT PRIMARY KEY NOT NULL," |
| "first_site_storage_time INTEGER," |
| "last_site_storage_time INTEGER," |
| "first_user_interaction_time INTEGER," |
| "last_user_interaction_time INTEGER," |
| "first_stateful_bounce_time INTEGER," |
| "last_stateful_bounce_time INTEGER," |
| "first_bounce_time INTEGER," |
| "last_bounce_time INTEGER)"; |
| // clang-format on |
| |
| DCHECK(db_->IsSQLValid(kBounceSql)); |
| return db_->Execute(kBounceSql); |
| } |
| |
| bool DIPSDatabase::UpdateSchema() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (meta_table_.GetVersionNumber() == 1 && !MigrateToVersion2()) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool DIPSDatabase::MigrateToVersion2() { |
| // This migration: |
| // - Makes all timestamp columns nullable instead of using base::Time() as |
| // default. |
| // - Replaces both the first and last stateless bounce columns to track |
| // the first and last bounce times instead. |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(db_->HasActiveTransactions()); |
| // First make a new table that allows for null values in the timestamps |
| // columns. |
| static constexpr char kNewTableSql[] = // clang-format off |
| "CREATE TABLE new_bounces(" |
| "site TEXT PRIMARY KEY NOT NULL," |
| "first_site_storage_time INTEGER," |
| "last_site_storage_time INTEGER," |
| "first_user_interaction_time INTEGER," |
| "last_user_interaction_time INTEGER," |
| "first_stateful_bounce_time INTEGER," |
| "last_stateful_bounce_time INTEGER," |
| "first_stateless_bounce_time INTEGER," |
| "last_stateless_bounce_time INTEGER)"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kNewTableSql)); |
| if (!db_->Execute(kNewTableSql)) { |
| return false; |
| } |
| |
| static constexpr char kCopyEverythingSql[] = |
| "INSERT INTO new_bounces " |
| "SELECT * FROM bounces"; |
| DCHECK(db_->IsSQLValid(kCopyEverythingSql)); |
| if (!db_->Execute(kCopyEverythingSql)) { |
| return false; |
| } |
| |
| const std::array<std::string, 8> timestamp_columns{ |
| "first_site_storage_time", "last_site_storage_time", |
| "first_user_interaction_time", "last_user_interaction_time", |
| "first_stateless_bounce_time", "last_stateless_bounce_time", |
| "first_stateful_bounce_time", "last_stateful_bounce_time"}; |
| |
| for (const std::string& column : timestamp_columns) { |
| std::string command = base::StringPrintf( |
| "UPDATE new_bounces " |
| "SET %s=NULL " |
| "WHERE %s=0 ", |
| column.c_str(), column.c_str()); |
| sql::Statement s_nullify(db_->GetUniqueStatement(command.c_str())); |
| |
| if (!s_nullify.Run()) { |
| return false; |
| } |
| } |
| |
| // Replace the first_stateless_bounce with the first bounce overall. |
| // We have to first case on whether either of the bounce fields are NULL, |
| // since MIN will return NULL if either are NULL. |
| static constexpr char kReplaceFirstStatelessBounceSql[] = // clang-format off |
| "UPDATE new_bounces " |
| "SET first_stateless_bounce_time = " |
| "CASE " |
| "WHEN first_stateful_bounce_time IS NULL " |
| "THEN first_stateless_bounce_time " |
| "WHEN first_stateless_bounce_time IS NULL " |
| "THEN first_stateful_bounce_time " |
| "ELSE MIN(first_stateful_bounce_time, first_stateless_bounce_time) " |
| "END"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kReplaceFirstStatelessBounceSql)); |
| if (!db_->Execute(kReplaceFirstStatelessBounceSql)) { |
| return false; |
| } |
| |
| // Replace the last_stateless_bounce with the last bounce overall. |
| // We have to first case on whether either of the bounce fields are NULL, |
| // since MAX will return NULL if either are NULL. |
| static constexpr char kReplaceLastStatelessBounceSql[] = // clang-format off |
| "UPDATE new_bounces " |
| "SET last_stateless_bounce_time = " |
| "CASE " |
| "WHEN last_stateful_bounce_time IS NULL " |
| "THEN last_stateless_bounce_time " |
| "WHEN last_stateless_bounce_time IS NULL " |
| "THEN last_stateful_bounce_time " |
| "ELSE MAX(last_stateful_bounce_time, last_stateless_bounce_time) " |
| "END"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kReplaceLastStatelessBounceSql)); |
| if (!db_->Execute(kReplaceLastStatelessBounceSql)) { |
| return false; |
| } |
| // Rename this column to be reflect its new purpose. |
| static constexpr char kRenameFirstStatelessBounceTimeSql[] = |
| "ALTER TABLE new_bounces RENAME COLUMN first_stateless_bounce_time TO " |
| "first_bounce_time"; |
| DCHECK(db_->IsSQLValid(kRenameFirstStatelessBounceTimeSql)); |
| if (!db_->Execute(kRenameFirstStatelessBounceTimeSql)) { |
| return false; |
| } |
| |
| // Rename this column to be reflect its new purpose. |
| static constexpr char kRenameLastStatelessBounceTimeSql[] = |
| "ALTER TABLE new_bounces RENAME COLUMN last_stateless_bounce_time TO " |
| "last_bounce_time"; |
| if (!db_->Execute(kRenameLastStatelessBounceTimeSql)) { |
| return false; |
| } |
| |
| // Replace the old `bounces` table with the new one. |
| static constexpr char kDropOldTableSql[] = "DROP TABLE bounces"; |
| if (!db_->Execute(kDropOldTableSql)) { |
| return false; |
| } |
| |
| static constexpr char kReplaceOldTable[] = |
| "ALTER TABLE new_bounces RENAME TO bounces"; |
| if (!db_->Execute(kReplaceOldTable)) { |
| return false; |
| } |
| |
| meta_table_.SetVersionNumber(2); |
| return true; |
| } |
| |
| sql::InitStatus DIPSDatabase::InitImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| sql::InitStatus status = OpenDatabase(); |
| if (status != sql::INIT_OK) { |
| return status; |
| } |
| |
| DCHECK(db_->is_open()); |
| |
| // 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(), kCurrentVersionNumber, |
| kCompatibleVersionNumber)) { |
| db_->Close(); |
| return sql::INIT_FAILURE; |
| } |
| |
| if (!table_already_exists) { |
| if (!InitTables()) { |
| return sql::INIT_FAILURE; |
| } |
| } else { |
| if (!UpdateSchema()) { |
| return sql::INIT_FAILURE; |
| } |
| } |
| |
| // Initialization is complete. |
| if (!transaction.Commit()) |
| return sql::INIT_FAILURE; |
| |
| return sql::INIT_OK; |
| } |
| |
| sql::InitStatus DIPSDatabase::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; |
| } |
| } |
| |
| base::UmaHistogramExactLinear("Privacy.DIPS.DatabaseInit", attempts, 3); |
| |
| last_health_metrics_time_ = clock_->Now(); |
| LogDatabaseMetrics(); |
| |
| return status; |
| } |
| |
| void DIPSDatabase::LogDatabaseMetrics() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| |
| int64_t db_size; |
| if (base::GetFileSize(db_path_, &db_size)) { |
| base::UmaHistogramMemoryKB("Privacy.DIPS.DatabaseSize", db_size / 1024); |
| } |
| |
| base::UmaHistogramCounts10000("Privacy.DIPS.DatabaseEntryCount", |
| GetEntryCount()); |
| |
| base::UmaHistogramTimes("Privacy.DIPS.DatabaseHealthMetricsTime", |
| base::TimeTicks::Now() - start_time); |
| } |
| |
| bool DIPSDatabase::CheckDBInit() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!db_ || !db_->is_open()) |
| 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 DIPSDatabase::Write(const std::string& site, |
| const TimestampRange& storage_times, |
| const TimestampRange& interaction_times, |
| const TimestampRange& stateful_bounce_times, |
| const TimestampRange& bounce_times) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(stateful_bounce_times.IsNullOrWithin(bounce_times)); |
| if (!CheckDBInit()) |
| return false; |
| |
| static constexpr char kWriteSql[] = // clang-format off |
| "INSERT OR REPLACE INTO bounces(" |
| "site," |
| "first_site_storage_time," |
| "last_site_storage_time," |
| "first_user_interaction_time," |
| "last_user_interaction_time," |
| "first_stateful_bounce_time," |
| "last_stateful_bounce_time," |
| "first_bounce_time," |
| "last_bounce_time) " |
| "VALUES (?,?,?,?,?,?,?,?,?)"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kWriteSql)); |
| |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kWriteSql)); |
| statement.BindString(0, site); |
| BindTimeOrNull(statement, storage_times.first, 1); |
| BindTimeOrNull(statement, storage_times.last, 2); |
| BindTimeOrNull(statement, interaction_times.first, 3); |
| BindTimeOrNull(statement, interaction_times.last, 4); |
| BindTimeOrNull(statement, stateful_bounce_times.first, 5); |
| BindTimeOrNull(statement, stateful_bounce_times.last, 6); |
| BindTimeOrNull(statement, bounce_times.first, 7); |
| BindTimeOrNull(statement, bounce_times.last, 8); |
| return statement.Run(); |
| } |
| |
| absl::optional<StateValue> DIPSDatabase::Read(const std::string& site) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return absl::nullopt; |
| |
| static constexpr char kReadSql[] = // clang-format off |
| "SELECT site," |
| "first_site_storage_time," |
| "last_site_storage_time," |
| "first_user_interaction_time," |
| "last_user_interaction_time," |
| "first_stateful_bounce_time," |
| "last_stateful_bounce_time," |
| "first_bounce_time," |
| "last_bounce_time " |
| "FROM bounces WHERE site=?"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kReadSql)); |
| |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kReadSql)); |
| statement.BindString(0, site); |
| |
| if (!statement.Step()) { |
| return absl::nullopt; |
| } |
| // If the last interaction has expired, treat this entry as not in the |
| // database so that callers rewrite the entry for `site` as if it was deleted. |
| absl::optional<base::Time> last_user_interaction = |
| ColumnOptionalTime(&statement, 4); |
| if (last_user_interaction.has_value() && |
| last_user_interaction.value() + dips::kInteractionTtl.Get() < |
| clock_->Now()) { |
| return absl::nullopt; |
| } |
| |
| return StateValue{TimestampRange{ColumnOptionalTime(&statement, 1), |
| ColumnOptionalTime(&statement, 2)}, |
| TimestampRange{ColumnOptionalTime(&statement, 3), |
| ColumnOptionalTime(&statement, 4)}, |
| TimestampRange{ColumnOptionalTime(&statement, 5), |
| ColumnOptionalTime(&statement, 6)}, |
| TimestampRange{ColumnOptionalTime(&statement, 7), |
| ColumnOptionalTime(&statement, 8)}}; |
| } |
| |
| std::vector<std::string> DIPSDatabase::GetAllSitesForTesting() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) { |
| return {}; |
| } |
| |
| static constexpr char kReadSql[] = // clang-format off |
| "SELECT site FROM bounces ORDER BY site"; |
| // clang-format on |
| |
| DCHECK(db_->IsSQLValid(kReadSql)); |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kReadSql)); |
| |
| std::vector<std::string> sites; |
| while (statement.Step()) { |
| sites.push_back(statement.ColumnString(0)); |
| } |
| return sites; |
| } |
| |
| std::vector<std::string> DIPSDatabase::GetSitesThatBounced() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) { |
| return {}; |
| } |
| static constexpr char kBounceSql[] = // clang-format off |
| "SELECT site FROM bounces " |
| "WHERE first_bounce_time < ? AND " |
| "(last_user_interaction_time IS NULL OR " |
| // Only return a protected site if its protection has expired. |
| // Note: protected => expired ≡ (NOT protected) OR expired. |
| "NOT(first_user_interaction_time <= first_bounce_time + ?) OR " |
| "last_user_interaction_time < ?) " |
| "ORDER BY site"; // clang-format on |
| DCHECK(db_->IsSQLValid(kBounceSql)); |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kBounceSql)); |
| statement.BindTime(0, clock_->Now() - dips::kGracePeriod.Get()); |
| statement.BindTimeDelta(1, dips::kGracePeriod.Get()); |
| statement.BindTime(2, clock_->Now() - dips::kInteractionTtl.Get()); |
| |
| std::vector<std::string> sites; |
| while (statement.Step()) { |
| sites.push_back(statement.ColumnString(0)); |
| } |
| return sites; |
| } |
| |
| std::vector<std::string> DIPSDatabase::GetSitesThatBouncedWithState() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) { |
| return {}; |
| } |
| static constexpr char kStatefulBounceSql[] = // clang-format off |
| "SELECT site FROM bounces " |
| "WHERE first_stateful_bounce_time < ? AND " |
| "(last_user_interaction_time IS NULL OR " |
| // Only return a protected site if its protection has expired. |
| // Note: protected => expired ≡ (NOT protected) OR expired. |
| "NOT(first_user_interaction_time <= first_stateful_bounce_time + ?) OR " |
| "last_user_interaction_time < ?) " |
| "ORDER BY site"; // clang-format on |
| DCHECK(db_->IsSQLValid(kStatefulBounceSql)); |
| sql::Statement statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kStatefulBounceSql)); |
| statement.BindTime(0, clock_->Now() - dips::kGracePeriod.Get()); |
| statement.BindTimeDelta(1, dips::kGracePeriod.Get()); |
| statement.BindTime(2, clock_->Now() - dips::kInteractionTtl.Get()); |
| |
| std::vector<std::string> sites; |
| while (statement.Step()) { |
| sites.push_back(statement.ColumnString(0)); |
| } |
| return sites; |
| } |
| |
| std::vector<std::string> DIPSDatabase::GetSitesThatUsedStorage() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) { |
| return {}; |
| } |
| static constexpr char kStorageSql[] = // clang-format off |
| "SELECT site FROM bounces " |
| "WHERE first_site_storage_time < ? AND " |
| "(last_user_interaction_time IS NULL OR " |
| // Only return a protected site if its protection has expired. |
| // Note: protected => expired ≡ (NOT protected) OR expired. |
| "NOT(first_user_interaction_time <= first_site_storage_time + ?) OR " |
| "last_user_interaction_time < ?) " |
| "ORDER BY site"; // clang-format on |
| DCHECK(db_->IsSQLValid(kStorageSql)); |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kStorageSql)); |
| statement.BindTime(0, clock_->Now() - dips::kGracePeriod.Get()); |
| statement.BindTimeDelta(1, dips::kGracePeriod.Get()); |
| statement.BindTime(2, clock_->Now() - dips::kInteractionTtl.Get()); |
| |
| std::vector<std::string> sites; |
| while (statement.Step()) { |
| sites.push_back(statement.ColumnString(0)); |
| } |
| return sites; |
| } |
| |
| size_t DIPSDatabase::ClearRowsWithExpiredInteractions() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(clock_); |
| if (!CheckDBInit()) { |
| return false; |
| } |
| |
| static constexpr char kClearAllExpiredSql[] = |
| "DELETE FROM bounces WHERE last_user_interaction_time < ?"; |
| |
| DCHECK(db_->IsSQLValid(kClearAllExpiredSql)); |
| sql::Statement statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kClearAllExpiredSql)); |
| |
| statement.BindTime(0, clock_->Now() - dips::kInteractionTtl.Get()); |
| if (!statement.Run()) { |
| return 0; |
| } |
| |
| return db_->GetLastChangeCount(); |
| } |
| |
| bool DIPSDatabase::RemoveRow(const std::string& site) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return false; |
| ClearRowsWithExpiredInteractions(); |
| |
| 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(); |
| } |
| |
| bool DIPSDatabase::RemoveRows(const std::vector<std::string>& sites) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) { |
| return false; |
| } |
| |
| sql::Statement s_remove_rows(db_->GetUniqueStatement( |
| base::StrCat( |
| {"DELETE FROM bounces " |
| "WHERE site IN(", |
| base::JoinString(std::vector<std::string>(sites.size(), "?"), ","), |
| ")"}) |
| .c_str())); |
| |
| for (size_t i = 0; i < sites.size(); i++) { |
| s_remove_rows.BindString(i, sites[i]); |
| } |
| |
| return s_remove_rows.Run(); |
| } |
| |
| bool DIPSDatabase::RemoveEventsByTime(const base::Time& delete_begin, |
| const base::Time& delete_end, |
| const DIPSEventRemovalType type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return false; |
| ClearRowsWithExpiredInteractions(); |
| |
| sql::Transaction transaction(db_.get()); |
| if (!transaction.Begin()) |
| return false; |
| |
| GarbageCollect(); |
| |
| if (!ClearTimestamps(delete_begin, delete_end, type) || |
| !AdjustFirstTimestamps(delete_begin, delete_end, type) || |
| !AdjustLastTimestamps(delete_begin, delete_end, type)) { |
| return false; |
| } |
| |
| transaction.Commit(); |
| return true; |
| } |
| |
| bool DIPSDatabase::RemoveEventsBySite(bool preserve, |
| const std::vector<std::string>& sites, |
| const DIPSEventRemovalType 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; |
| |
| transaction.Commit(); |
| return true; |
| } |
| |
| bool DIPSDatabase::ClearTimestamps(const base::Time& delete_begin, |
| const base::Time& delete_end, |
| const DIPSEventRemovalType type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return false; |
| ClearRowsWithExpiredInteractions(); |
| |
| if ((type & DIPSEventRemovalType::kHistory) == |
| DIPSEventRemovalType::kHistory) { |
| static constexpr char kClearInteractionSql[] = // clang-format off |
| "UPDATE bounces SET " |
| "first_user_interaction_time=NULL," |
| "last_user_interaction_time=NULL " |
| "WHERE first_user_interaction_time>=? AND " |
| "last_user_interaction_time<=?"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kClearInteractionSql)); |
| |
| sql::Statement s_clear_interaction( |
| db_->GetCachedStatement(SQL_FROM_HERE, kClearInteractionSql)); |
| s_clear_interaction.BindTime(0, delete_begin); |
| s_clear_interaction.BindTime(1, delete_end); |
| |
| if (!s_clear_interaction.Run()) |
| return false; |
| |
| 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; |
| } |
| } |
| |
| if ((type & DIPSEventRemovalType::kStorage) == |
| DIPSEventRemovalType::kStorage) { |
| static constexpr char kClearStorageSql[] = // clang-format off |
| "UPDATE bounces SET " |
| "first_site_storage_time=NULL, " |
| "last_site_storage_time=NULL " |
| "WHERE first_site_storage_time>=? AND " |
| "last_site_storage_time<=?"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kClearStorageSql)); |
| |
| sql::Statement s_clear_storage( |
| db_->GetCachedStatement(SQL_FROM_HERE, kClearStorageSql)); |
| s_clear_storage.BindTime(0, delete_begin); |
| s_clear_storage.BindTime(1, delete_end); |
| |
| if (!s_clear_storage.Run()) |
| return false; |
| |
| static constexpr char kClearStatefulSql[] = // clang-format off |
| "UPDATE bounces SET " |
| "first_stateful_bounce_time=NULL," |
| "last_stateful_bounce_time=NULL " |
| "WHERE first_stateful_bounce_time>=? AND " |
| "last_stateful_bounce_time<=?"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kClearStatefulSql)); |
| |
| sql::Statement s_clear_stateful( |
| db_->GetCachedStatement(SQL_FROM_HERE, kClearStatefulSql)); |
| s_clear_stateful.BindTime(0, delete_begin); |
| s_clear_stateful.BindTime(1, delete_end); |
| |
| if (!s_clear_stateful.Run()) |
| return false; |
| } |
| |
| return RemoveEmptyRows(); |
| } |
| |
| bool DIPSDatabase::AdjustFirstTimestamps(const base::Time& delete_begin, |
| const base::Time& delete_end, |
| const DIPSEventRemovalType type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return false; |
| ClearRowsWithExpiredInteractions(); |
| |
| if ((type & DIPSEventRemovalType::kHistory) == |
| DIPSEventRemovalType::kHistory) { |
| static constexpr char kUpdateFirstInteractionSql[] = // clang-format off |
| "UPDATE bounces SET first_user_interaction_time=?1 " |
| "WHERE first_user_interaction_time>=?2 AND " |
| "first_user_interaction_time<?1"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kUpdateFirstInteractionSql)); |
| |
| sql::Statement s_first_interaction( |
| db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstInteractionSql)); |
| s_first_interaction.BindTime(0, delete_end); |
| s_first_interaction.BindTime(1, delete_begin); |
| |
| if (!s_first_interaction.Run()) |
| return false; |
| |
| static constexpr char kUpdateFirstBounceSql[] = // clang-format off |
| "UPDATE bounces SET first_bounce_time=?1 " |
| "WHERE first_bounce_time>=?2 AND " |
| "first_bounce_time<?1"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kUpdateFirstBounceSql)); |
| |
| sql::Statement s_first_bounce( |
| db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstBounceSql)); |
| s_first_bounce.BindTime(0, delete_end); |
| s_first_bounce.BindTime(1, delete_begin); |
| |
| if (!s_first_bounce.Run()) { |
| return false; |
| } |
| } |
| |
| if ((type & DIPSEventRemovalType::kStorage) == |
| DIPSEventRemovalType::kStorage) { |
| static constexpr char kUpdateFirstStorageSql[] = // clang-format off |
| "UPDATE bounces SET first_site_storage_time=?1 " |
| "WHERE first_site_storage_time>=?2 AND " |
| "first_site_storage_time<?1"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kUpdateFirstStorageSql)); |
| |
| sql::Statement s_first_storage( |
| db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstStorageSql)); |
| s_first_storage.BindTime(0, delete_end); |
| s_first_storage.BindTime(1, delete_begin); |
| |
| if (!s_first_storage.Run()) |
| return false; |
| |
| static constexpr char kUpdateFirstStatefulSql[] = // clang-format off |
| "UPDATE bounces SET first_stateful_bounce_time=?1 " |
| "WHERE first_stateful_bounce_time>=?2 AND " |
| "first_stateful_bounce_time<?1"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kUpdateFirstStatefulSql)); |
| |
| sql::Statement s_first_stateful( |
| db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstStatefulSql)); |
| s_first_stateful.BindTime(0, delete_end); |
| s_first_stateful.BindTime(1, delete_begin); |
| |
| if (!s_first_stateful.Run()) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool DIPSDatabase::AdjustLastTimestamps(const base::Time& delete_begin, |
| const base::Time& delete_end, |
| const DIPSEventRemovalType type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return false; |
| ClearRowsWithExpiredInteractions(); |
| |
| if ((type & DIPSEventRemovalType::kHistory) == |
| DIPSEventRemovalType::kHistory) { |
| static constexpr char kUpdateLastInteractionSql[] = // clang-format off |
| "UPDATE bounces SET last_user_interaction_time=?1 " |
| "WHERE last_user_interaction_time>?1 AND " |
| "last_user_interaction_time<=?2"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kUpdateLastInteractionSql)); |
| |
| sql::Statement s_last_interaction( |
| db_->GetCachedStatement(SQL_FROM_HERE, kUpdateLastInteractionSql)); |
| s_last_interaction.BindTime(0, delete_begin); |
| s_last_interaction.BindTime(1, delete_end); |
| |
| if (!s_last_interaction.Run()) |
| return false; |
| |
| 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; |
| } |
| } |
| |
| if ((type & DIPSEventRemovalType::kStorage) == |
| DIPSEventRemovalType::kStorage) { |
| static constexpr char kUpdateLastStorageSql[] = // clang-format off |
| "UPDATE bounces SET last_site_storage_time=?1 " |
| "WHERE last_site_storage_time>?1 AND " |
| "last_site_storage_time<=?2"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kUpdateLastStorageSql)); |
| |
| sql::Statement s_last_storage( |
| db_->GetCachedStatement(SQL_FROM_HERE, kUpdateLastStorageSql)); |
| s_last_storage.BindTime(0, delete_begin); |
| s_last_storage.BindTime(1, delete_end); |
| |
| if (!s_last_storage.Run()) |
| return false; |
| |
| static constexpr char kUpdateLastStatefulSql[] = // clang-format off |
| "UPDATE bounces SET last_stateful_bounce_time=?1 " |
| "WHERE last_stateful_bounce_time>?1 AND " |
| "last_stateful_bounce_time<=?2"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kUpdateLastStatefulSql)); |
| |
| sql::Statement s_last_stateful( |
| db_->GetCachedStatement(SQL_FROM_HERE, kUpdateLastStatefulSql)); |
| s_last_stateful.BindTime(0, delete_begin); |
| s_last_stateful.BindTime(1, delete_end); |
| |
| if (!s_last_stateful.Run()) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool DIPSDatabase::ClearTimestampsBySite(bool preserve, |
| const std::vector<std::string>& sites, |
| const DIPSEventRemovalType type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (sites.empty()) |
| return false; |
| |
| std::string placeholders = |
| base::JoinString(std::vector<std::string>(sites.size(), "?"), ","); |
| |
| if ((type & DIPSEventRemovalType::kStorage) == |
| DIPSEventRemovalType::kStorage) { |
| sql::Statement s_clear_storage(db_->GetUniqueStatement( // clang-format off |
| base::StrCat({"UPDATE bounces SET first_site_storage_time=NULL," |
| "last_site_storage_time=NULL," |
| "first_stateful_bounce_time=NULL," |
| "last_stateful_bounce_time=NULL " |
| "WHERE site ", (preserve ? "NOT " : ""), |
| "IN(", placeholders, ")" }) // clang-format on |
| .c_str())); |
| |
| 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 DIPSDatabase::RemoveEmptyRows() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| static constexpr char kCleanUpSql[] = // clang-format off |
| "DELETE FROM bounces " |
| "WHERE first_site_storage_time IS NULL AND " |
| "last_site_storage_time IS NULL AND " |
| "first_user_interaction_time IS NULL AND " |
| "last_user_interaction_time IS NULL AND " |
| "first_stateful_bounce_time IS NULL AND " |
| "last_stateful_bounce_time IS NULL AND " |
| "first_bounce_time IS NULL AND " |
| "last_bounce_time IS NULL"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kCleanUpSql)); |
| |
| sql::Statement s_clean(db_->GetCachedStatement(SQL_FROM_HERE, kCleanUpSql)); |
| |
| return s_clean.Run(); |
| } |
| |
| size_t DIPSDatabase::GetEntryCount() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return 0; |
| |
| sql::Statement s_entry_count( |
| db_->GetCachedStatement(SQL_FROM_HERE, "SELECT COUNT(*) FROM bounces")); |
| return (s_entry_count.Step() ? s_entry_count.ColumnInt(0) : 0); |
| } |
| |
| size_t DIPSDatabase::GarbageCollect() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return 0; |
| |
| size_t num_deleted = ClearRowsWithExpiredInteractions(); |
| ; |
| size_t num_entries = GetEntryCount(); |
| int purge_goal = num_entries - (max_entries_ - purge_entries_); |
| |
| if (num_entries <= max_entries_) |
| return num_deleted; |
| |
| DCHECK_GT(purge_goal, 0); |
| |
| // If expiration did not purge enough entries, remove entries with the oldest |
| // |MAX(last_user_interaction_time,last_site_storage_time)| values until the |
| // |purge_goal| is satisfied. |
| if (num_deleted < static_cast<size_t>(purge_goal)) { |
| num_deleted += GarbageCollectOldest(purge_goal - num_deleted); |
| } |
| |
| return num_deleted; |
| } |
| |
| size_t DIPSDatabase::GarbageCollectOldest(int purge_goal) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!CheckDBInit()) |
| return 0; |
| |
| static constexpr char kGarbageCollectOldestSql[] = // clang-format off |
| "DELETE FROM bounces WHERE site " |
| "IN(SELECT site FROM bounces " |
| "ORDER BY " |
| "MAX(last_user_interaction_time,last_site_storage_time) ASC," |
| "last_site_storage_time ASC " |
| "LIMIT ?)"; |
| // clang-format on |
| DCHECK(db_->IsSQLValid(kGarbageCollectOldestSql)); |
| |
| sql::Statement s_garbage_collect_oldest( |
| db_->GetCachedStatement(SQL_FROM_HERE, kGarbageCollectOldestSql)); |
| s_garbage_collect_oldest.BindInt(0, purge_goal); |
| |
| if (!s_garbage_collect_oldest.Run()) |
| return 0; |
| |
| return db_->GetLastChangeCount(); |
| } |