blob: ce96b32dd5522156be5b98a9af5dc8352bc17e82 [file] [log] [blame]
// Copyright 2021 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 "content/browser/conversions/rate_limit_table.h"
#include "base/check.h"
#include "base/time/clock.h"
#include "content/browser/conversions/sql_utils.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
RateLimitTable::RateLimitTable(const ConversionStorage::Delegate* delegate,
const base::Clock* clock)
: delegate_(delegate), clock_(clock) {
DCHECK(delegate_);
DETACH_FROM_SEQUENCE(sequence_checker_);
}
RateLimitTable::~RateLimitTable() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
bool RateLimitTable::CreateTable(sql::Database* db) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// All columns in this table are const.
// |attribution_type| is the type of the report, currently always kNavigation.
// |impression_id| is the primary key of a row in the |impressions| table,
// though the row may not exist.
// |impression_site| is the eTLD+1 of the impression.
// |impression_origin| is the origin of the impression.
// |conversion_destination| is the destination of the conversion.
// |conversion_origin| is the origin of the conversion.
// |conversion_time| is the report's conversion time.
const char kRateLimitTableSql[] =
"CREATE TABLE IF NOT EXISTS rate_limits"
"(rate_limit_id INTEGER PRIMARY KEY,"
"attribution_type INTEGER NOT NULL,"
"impression_id INTEGER NOT NULL,"
"impression_site TEXT NOT NULL,"
"impression_origin TEXT NOT NULL,"
"conversion_destination TEXT NOT NULL,"
"conversion_origin TEXT NOT NULL,"
"conversion_time INTEGER NOT NULL)";
if (!db->Execute(kRateLimitTableSql))
return false;
// Optimizes calls to |IsAttributionAllowed()|.
const char kRateLimitImpressionSiteTypeIndexSql[] =
"CREATE INDEX IF NOT EXISTS rate_limit_impression_site_type_idx "
"ON rate_limits(attribution_type, conversion_destination, "
"impression_site, conversion_time)";
if (!db->Execute(kRateLimitImpressionSiteTypeIndexSql))
return false;
// Optimizes calls to |DeleteExpiredRateLimits()| and |ClearAllDataInRange()|.
const char kRateLimitConversionTimeIndexSql[] =
"CREATE INDEX IF NOT EXISTS rate_limit_conversion_time_idx "
"ON rate_limits(conversion_time)";
if (!db->Execute(kRateLimitConversionTimeIndexSql))
return false;
// Optimizes calls to |ClearDataForImpressionIds()|.
const char kRateLimitImpressionIndexSql[] =
"CREATE INDEX IF NOT EXISTS rate_limit_impression_id_idx "
"ON rate_limits(impression_id)";
return db->Execute(kRateLimitImpressionIndexSql);
}
bool RateLimitTable::AddRateLimit(sql::Database* db,
const ConversionReport& report) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(report.impression.impression_id().has_value());
// TODO(apaseltiner): Call this at most every X minutes.
DeleteExpiredRateLimits(db);
const char kStoreRateLimitSql[] =
"INSERT INTO rate_limits "
"(attribution_type, impression_id, impression_site, impression_origin, "
"conversion_destination, conversion_origin, conversion_time) "
"VALUES(?,?,?,?,?,?,?)";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kStoreRateLimitSql));
statement.BindInt(0, static_cast<int>(AttributionType::kNavigation));
statement.BindInt64(1, *report.impression.impression_id());
statement.BindString(
2, net::SchemefulSite(report.impression.impression_origin()).Serialize());
statement.BindString(3, report.impression.impression_origin().Serialize());
statement.BindString(4,
report.impression.ConversionDestination().Serialize());
statement.BindString(5, report.impression.conversion_origin().Serialize());
statement.BindTime(6, report.conversion_time);
return statement.Run();
}
bool RateLimitTable::IsAttributionAllowed(sql::Database* db,
const ConversionReport& report,
base::Time now) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ConversionStorage::Delegate::RateLimitConfig rate_limits =
delegate_->GetRateLimits();
base::Time min_timestamp = now - rate_limits.time_window;
const char kAttributionAllowedSql[] =
"SELECT COUNT(*) FROM rate_limits "
"WHERE attribution_type = ? "
"AND impression_site = ? "
"AND conversion_destination = ? "
"AND conversion_time > ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kAttributionAllowedSql));
statement.BindInt(0, static_cast<int>(AttributionType::kNavigation));
statement.BindString(
1, net::SchemefulSite(report.impression.impression_origin()).Serialize());
statement.BindString(2,
report.impression.ConversionDestination().Serialize());
statement.BindTime(3, min_timestamp);
if (!statement.Step())
return false;
int64_t count = statement.ColumnInt64(0);
return count < rate_limits.max_attributions_per_window;
}
bool RateLimitTable::ClearAllDataInRange(sql::Database* db,
base::Time delete_begin,
base::Time delete_end) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!((delete_begin.is_null() || delete_begin.is_min()) &&
delete_end.is_max()));
const char kDeleteRateLimitRangeSql[] =
"DELETE FROM rate_limits WHERE conversion_time BETWEEN ? AND "
"?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteRateLimitRangeSql));
statement.BindTime(0, delete_begin);
statement.BindTime(1, delete_end);
return statement.Run();
}
bool RateLimitTable::ClearAllDataAllTime(sql::Database* db) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const char kDeleteAllRateLimitsSql[] = "DELETE FROM rate_limits";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteAllRateLimitsSql));
return statement.Run();
}
bool RateLimitTable::ClearDataForOriginsInRange(
sql::Database* db,
base::Time delete_begin,
base::Time delete_end,
base::RepeatingCallback<bool(const url::Origin&)> filter) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!filter.is_null());
std::vector<int64_t> rate_limit_ids_to_delete;
{
const char kScanCandidateData[] =
"SELECT rate_limit_id, impression_site, impression_origin, "
"conversion_destination, conversion_origin FROM rate_limits "
"WHERE conversion_time BETWEEN ? AND ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kScanCandidateData));
statement.BindTime(0, delete_begin);
statement.BindTime(1, delete_end);
while (statement.Step()) {
int64_t rate_limit_id = statement.ColumnInt64(0);
if (filter.Run(DeserializeOrigin(statement.ColumnString(1))) ||
filter.Run(DeserializeOrigin(statement.ColumnString(2))) ||
filter.Run(DeserializeOrigin(statement.ColumnString(3))) ||
filter.Run(DeserializeOrigin(statement.ColumnString(4)))) {
rate_limit_ids_to_delete.push_back(rate_limit_id);
}
}
if (!statement.Succeeded())
return false;
}
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
for (int64_t rate_limit_id : rate_limit_ids_to_delete) {
const char kDeleteRateLimitSql[] =
"DELETE FROM rate_limits WHERE rate_limit_id = ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteRateLimitSql));
statement.BindInt64(0, rate_limit_id);
if (!statement.Run())
return false;
}
return transaction.Commit();
}
int RateLimitTable::DeleteExpiredRateLimits(sql::Database* db) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::Time timestamp = clock_->Now() - delegate_->GetRateLimits().time_window;
const char kDeleteExpiredRateLimits[] =
"DELETE FROM rate_limits WHERE conversion_time <= ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteExpiredRateLimits));
statement.BindTime(0, timestamp);
if (!statement.Run())
return 0;
return db->GetLastChangeCount();
}
bool RateLimitTable::ClearDataForImpressionIds(
sql::Database* db,
const base::flat_set<int64_t>& impression_ids) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
for (int64_t id : impression_ids) {
const char kDeleteRateLimitSql[] =
"DELETE FROM rate_limits WHERE impression_id = ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteRateLimitSql));
statement.BindInt64(0, id);
if (!statement.Run())
return false;
}
return transaction.Commit();
}
} // namespace content