blob: 0c3862f82df02e007652c3cd7db5a9812c354cfd [file] [log] [blame]
// 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 {
constexpr char kPrepopulatedKey[] = "prepopulated";
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);
}
TimestampRange RangeFromColumns(sql::Statement* statement,
int start_column_idx,
int end_column_idx,
std::vector<DIPSErrorCode>& errors) {
absl::optional<base::Time> first_time =
ColumnOptionalTime(statement, start_column_idx);
absl::optional<base::Time> last_time =
ColumnOptionalTime(statement, end_column_idx);
if (!first_time.has_value() && !last_time.has_value()) {
return absl::nullopt;
}
if (!first_time.has_value()) {
errors.push_back(DIPSErrorCode::kRead_OpenEndedRange_NullStart);
return absl::nullopt;
}
if (!last_time.has_value()) {
errors.push_back(DIPSErrorCode::kRead_OpenEndedRange_NullEnd);
return absl::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);
}
}
// 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;
// The lowest current version embedded in Chrome code that can use the current
// version of this database.
const int kCompatibleVersionNumber = 2;
} // 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 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();
}
// 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;
}
return meta_table_.SetVersionNumber(2);
}
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());
sql::MetaTable::RazeIfIncompatible(db_.get(),
sql::MetaTable::kNoLowestSupportedVersion,
kCurrentVersionNumber);
// 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() {
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() {
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::ExecuteSqlForTesting(const char* sql) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
return db_->ExecuteScriptForTesting(sql); // IN-TEST
}
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(
IsNullOrWithin(/*inner=*/stateful_bounce_times, /*outer=*/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);
BindTimesOrNull(statement, storage_times, 1, 2);
BindTimesOrNull(statement, interaction_times, 3, 4);
BindTimesOrNull(statement, stateful_bounce_times, 5, 6);
BindTimesOrNull(statement, bounce_times, 7, 8);
return statement.Run();
}
absl::optional<StateValue> DIPSDatabase::Read(const std::string& site) {
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;
}
std::vector<DIPSErrorCode> errors;
TimestampRange site_storage_times =
RangeFromColumns(&statement, 1, 2, errors);
TimestampRange user_interaction_times =
RangeFromColumns(&statement, 3, 4, errors);
TimestampRange stateful_bounce_times =
RangeFromColumns(&statement, 5, 6, errors);
TimestampRange bounce_times = RangeFromColumns(&statement, 7, 8, errors);
if (!IsNullOrWithin(stateful_bounce_times, bounce_times)) {
DCHECK(stateful_bounce_times.has_value());
errors.push_back(
DIPSErrorCode::kRead_BounceTimesIsntSupersetOfStatefulBounces);
if (!bounce_times.has_value()) {
bounce_times = stateful_bounce_times;
} else {
base::Time start =
std::min(stateful_bounce_times->first, bounce_times->first);
base::Time end =
std::max(stateful_bounce_times->second, bounce_times->second);
bounce_times = {start, end};
}
}
if (errors.empty()) {
base::UmaHistogramEnumeration("Privacy.DIPS.DIPSErrorCodes",
DIPSErrorCode::kRead_None);
} else {
for (const DIPSErrorCode& error : errors) {
base::UmaHistogramEnumeration("Privacy.DIPS.DIPSErrorCodes", error);
}
}
return StateValue{site_storage_times, user_interaction_times,
stateful_bounce_times, bounce_times};
}
std::vector<std::string> DIPSDatabase::GetAllSitesForTesting() {
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 base::TimeDelta& grace_period) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
ClearRowsWithExpiredInteractions();
static constexpr char kBounceSql[] = // clang-format off
"SELECT site FROM bounces "
"WHERE first_bounce_time < ? AND "
"last_user_interaction_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::vector<std::string> DIPSDatabase::GetSitesThatBouncedWithState(
const base::TimeDelta& grace_period) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
ClearRowsWithExpiredInteractions();
static constexpr char kStatefulBounceSql[] = // clang-format off
"SELECT site FROM bounces "
"WHERE first_stateful_bounce_time < ? AND "
"last_user_interaction_time IS NULL "
"ORDER BY site"; // clang-format on
DCHECK(db_->IsSQLValid(kStatefulBounceSql));
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE, kStatefulBounceSql));
statement.BindTime(0, clock_->Now() - grace_period);
std::vector<std::string> sites;
while (statement.Step()) {
sites.push_back(statement.ColumnString(0));
}
return sites;
}
std::vector<std::string> DIPSDatabase::GetSitesThatUsedStorage(
const base::TimeDelta& grace_period) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
ClearRowsWithExpiredInteractions();
static constexpr char kStorageSql[] = // clang-format off
"SELECT site FROM bounces "
"WHERE first_site_storage_time < ? AND "
"last_user_interaction_time IS NULL "
"ORDER BY site"; // clang-format on
DCHECK(db_->IsSQLValid(kStorageSql));
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kStorageSql));
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> DIPSDatabase::FilterSitesWithInteraction(
const std::set<std::string>& sites) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return {};
}
ClearRowsWithExpiredInteractions();
sql::Statement s_interactions(db_->GetUniqueStatement(
base::StrCat({"SELECT site,last_user_interaction_time FROM bounces "
"WHERE site IN(",
base::JoinString(
std::vector<base::StringPiece>(sites.size(), "?"), ","),
")"})
.c_str()));
int i = 0;
for (const auto& site : sites) {
s_interactions.BindString(i, site);
i++;
}
std::set<std::string> interacted_sites;
while (s_interactions.Step()) {
if (ColumnOptionalTime(&s_interactions, 1).has_value()) {
interacted_sites.insert(s_interactions.ColumnString(0));
}
}
return interacted_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;
}
if (sites.empty()) {
return true;
}
sql::Statement s_remove_rows(db_->GetUniqueStatement(
base::StrCat({"DELETE FROM bounces "
"WHERE site IN(",
base::JoinString(
std::vector<base::StringPiece>(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();
return (ClearTimestamps(delete_begin, delete_end, type) &&
transaction.Commit());
}
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;
return transaction.Commit();
}
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;
}
}
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;
}
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 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 (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 & DIPSEventRemovalType::kHistory) ==
DIPSEventRemovalType::kHistory) {
static constexpr char kUpdateFirstInteractionSql[] = // clang-format off
"UPDATE bounces SET first_user_interaction_time=?2 "
"WHERE first_user_interaction_time>=?1 AND "
"first_user_interaction_time<?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateFirstInteractionSql));
sql::Statement s_first_interaction(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstInteractionSql));
s_first_interaction.BindTime(0, delete_begin);
s_first_interaction.BindTime(1, delete_end);
if (!s_first_interaction.Run()) {
return false;
}
}
if ((type & DIPSEventRemovalType::kStorage) ==
DIPSEventRemovalType::kStorage) {
static constexpr char kUpdateFirstStorageSql[] = // clang-format off
"UPDATE bounces SET first_site_storage_time=?2 "
"WHERE first_site_storage_time>=?1 AND "
"first_site_storage_time<?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateFirstStorageSql));
sql::Statement s_first_storage(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstStorageSql));
s_first_storage.BindTime(0, delete_begin);
s_first_storage.BindTime(1, delete_end);
if (!s_first_storage.Run()) {
return false;
}
static constexpr char kUpdateFirstStatefulSql[] = // clang-format off
"UPDATE bounces SET first_stateful_bounce_time=?2 "
"WHERE first_stateful_bounce_time>=?1 AND "
"first_stateful_bounce_time<?2";
// clang-format on
DCHECK(db_->IsSQLValid(kUpdateFirstStatefulSql));
sql::Statement s_first_stateful(
db_->GetCachedStatement(SQL_FROM_HERE, kUpdateFirstStatefulSql));
s_first_stateful.BindTime(0, delete_begin);
s_first_stateful.BindTime(1, delete_end);
if (!s_first_stateful.Run()) {
return false;
}
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 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 (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 & 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;
}
}
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;
}
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 DIPSDatabase::ClearTimestampsBySite(bool preserve,
const std::vector<std::string>& sites,
const DIPSEventRemovalType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (sites.empty())
return true;
std::string placeholders =
base::JoinString(std::vector<base::StringPiece>(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,"
"first_bounce_time=NULL,"
"last_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() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit())
return 0;
ClearRowsWithExpiredInteractions();
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();
}
bool DIPSDatabase::MarkAsPrepopulated() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
return meta_table_.SetValue(kPrepopulatedKey, 1);
}
bool DIPSDatabase::IsPrepopulated() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!CheckDBInit()) {
return false;
}
int result;
bool has_key = meta_table_.GetValue(kPrepopulatedKey, &result);
if (!has_key) {
meta_table_.SetValue(kPrepopulatedKey, 0);
return false;
}
return result;
}