blob: 8d2a4ea94a3d63f3ac2c204cb12f68b203a4b6c6 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/webdata/common/web_database.h"
#include <algorithm>
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/stringprintf.h"
#include "sql/transaction.h"
const base::FilePath::CharType WebDatabase::kInMemoryPath[] =
FILE_PATH_LITERAL(":memory");
namespace {
// Limits the duration of transaction to the scope of their modifications. Avoid
// keeping pending transactions and pending modifications outside of their
// scope.
//
// TODO(6175955): When this is launched, replace
// `WebDatabase::AcquireTransaction()` with the typical pattern:
// sql::Transaction transaction(db());
// if (!transaction.Begin()) {...}
BASE_FEATURE(kSqlScopedTransactionWebDatabase,
"SqlScopedTransactionWebDatabase",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kSqlWALModeOnWebDatabase,
"SqlWALModeOnWebDatabase",
base::FEATURE_DISABLED_BY_DEFAULT);
// These values are logged as histogram buckets and most not be changed nor
// reused.
enum class WebDatabaseInitResult {
kSuccess = 0,
kCouldNotOpen = 1,
kDatabaseLocked = 2,
kCouldNotRazeIncompatibleVersion = 3,
kFailedToBeginInitTransaction = 4,
kMetaTableInitFailed = 5,
kCurrentVersionTooNew = 6,
kMigrationError = 7,
kFailedToCreateTable = 8,
kFailedToCommitInitTransaction = 9,
kMaxValue = kFailedToCommitInitTransaction
};
void LogInitResult(WebDatabaseInitResult result) {
base::UmaHistogramEnumeration("WebDatabase.InitResult", result);
}
// Version 139 migrates valuables tables to a new format, changing the column
// names. It is thus is no longer compatible with version 138.
constexpr int kCompatibleVersionNumber = 139;
// Change the version number and possibly the compatibility version of
// |meta_table_|.
[[nodiscard]] bool ChangeVersion(sql::MetaTable* meta_table,
int version_num,
bool update_compatible_version_num) {
return meta_table->SetVersionNumber(version_num) &&
(!update_compatible_version_num ||
meta_table->SetCompatibleVersionNumber(
std::min(version_num, kCompatibleVersionNumber)));
}
// Outputs the failed version number as a warning and always returns
// |sql::INIT_FAILURE|.
sql::InitStatus FailedMigrationTo(int version_num) {
LOG(WARNING) << "Unable to update web database to version " << version_num
<< ".";
base::UmaHistogramExactLinear("WebDatabase.FailedMigrationToVersion",
version_num,
WebDatabase::kCurrentVersionNumber + 1);
LogInitResult(WebDatabaseInitResult::kMigrationError);
return sql::INIT_FAILURE;
}
} // namespace
WebDatabase::WebDatabase()
: db_(sql::DatabaseOptions()
.set_wal_mode(
base::FeatureList::IsEnabled(kSqlWALModeOnWebDatabase))
// We don't store that much data in the tables so use a small page
// size. This provides a large benefit for empty tables (which is
// very likely with the tables we create).
.set_page_size(2048)
// We shouldn't have much data and what access we currently have
// is quite infrequent. So we go with a small cache size.
.set_cache_size(32),
/*tag=*/"Web"),
use_scoped_transaction_(
base::FeatureList::IsEnabled(kSqlScopedTransactionWebDatabase)) {}
WebDatabase::~WebDatabase() {
for (auto& [key, table] : tables_) {
table->Shutdown();
}
}
void WebDatabase::AddTable(WebDatabaseTable* table) {
tables_[table->GetTypeKey()] = table;
}
WebDatabaseTable* WebDatabase::GetTable(WebDatabaseTable::TypeKey key) {
WebDatabaseTable* table = tables_[key];
CHECK(table);
return table;
}
void WebDatabase::BeginTransaction() {
if (!use_scoped_transaction_) {
db_.BeginTransactionDeprecated();
}
}
void WebDatabase::CommitTransaction() {
if (!use_scoped_transaction_) {
db_.CommitTransactionDeprecated();
}
}
std::unique_ptr<sql::Transaction> WebDatabase::AcquireTransaction() {
if (use_scoped_transaction_) {
// Only one active transaction at the time is allowed.
DCHECK(!db_.HasActiveTransactions());
auto transaction = std::make_unique<sql::Transaction>(&db_);
if (transaction->Begin()) {
return transaction;
}
}
return nullptr;
}
std::string WebDatabase::GetDiagnosticInfo(int extended_error,
sql::Statement* statement) {
return db_.GetDiagnosticInfo(extended_error, statement);
}
sql::Database* WebDatabase::GetSQLConnection() {
return &db_;
}
sql::InitStatus WebDatabase::Init(const base::FilePath& db_name,
const os_crypt_async::Encryptor* encryptor) {
// Only unit tests whose tables don't use any crypto for their tables pass in
// a null encryptor.
if (!encryptor) {
CHECK_IS_TEST();
}
if ((db_name.value() == kInMemoryPath) ? !db_.OpenInMemory()
: !db_.Open(db_name)) {
LogInitResult(WebDatabaseInitResult::kCouldNotOpen);
return sql::INIT_FAILURE;
}
DCHECK(db_.is_open());
// Dummy transaction to check whether the database is writeable and bail
// early if that's not the case.
if (!db_.Execute("BEGIN EXCLUSIVE") || !db_.Execute("COMMIT")) {
LogInitResult(WebDatabaseInitResult::kDatabaseLocked);
return sql::INIT_FAILURE;
}
// Clobber really old databases.
static_assert(kDeprecatedVersionNumber < kCurrentVersionNumber,
"Deprecation version must be less than current");
if (sql::MetaTable::RazeIfIncompatible(
&db_, /*lowest_supported_version=*/kDeprecatedVersionNumber + 1,
kCurrentVersionNumber) == sql::RazeIfIncompatibleResult::kFailed) {
LogInitResult(WebDatabaseInitResult::kCouldNotRazeIncompatibleVersion);
return sql::INIT_FAILURE;
}
// Scope initialization in a transaction so we can't be partially
// initialized.
sql::Transaction transaction(&db_);
if (!transaction.Begin()) {
LogInitResult(WebDatabaseInitResult::kFailedToBeginInitTransaction);
return sql::INIT_FAILURE;
}
// Version check.
if (!meta_table_.Init(&db_, kCurrentVersionNumber,
kCompatibleVersionNumber)) {
LogInitResult(WebDatabaseInitResult::kMetaTableInitFailed);
return sql::INIT_FAILURE;
}
if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) {
LogInitResult(WebDatabaseInitResult::kCurrentVersionTooNew);
LOG(WARNING) << "Web database is too new.";
return sql::INIT_TOO_NEW;
}
// Initialize the tables.
for (const auto& table : tables_) {
table.second->Init(&db_, &meta_table_, encryptor);
}
// If the file on disk is an older database version, bring it up to date.
// If the migration fails we return an error to caller and do not commit
// the migration.
sql::InitStatus migration_status = MigrateOldVersionsAsNeeded();
if (migration_status != sql::INIT_OK) {
return migration_status;
}
// Create the desired SQL tables if they do not already exist.
// It's important that this happen *after* the migration code runs.
// Otherwise, the migration code would have to explicitly check for empty
// tables created in the new format, and skip the migration in that case.
for (const auto& table : tables_) {
if (!table.second->CreateTablesIfNecessary()) {
LOG(WARNING) << "Unable to initialize the web database.";
LogInitResult(WebDatabaseInitResult::kFailedToCreateTable);
return sql::INIT_FAILURE;
}
}
bool result = transaction.Commit();
if (!result) {
LogInitResult(WebDatabaseInitResult::kFailedToCommitInitTransaction);
return sql::INIT_FAILURE;
}
LogInitResult(WebDatabaseInitResult::kSuccess);
DCHECK(db_.is_open());
return sql::INIT_OK;
}
sql::InitStatus WebDatabase::MigrateOldVersionsAsNeeded() {
// Some malware used to lower the version number, causing migration to
// fail. Ensure the version number is at least as high as the compatible
// version number.
int current_version = std::max(meta_table_.GetVersionNumber(),
meta_table_.GetCompatibleVersionNumber());
if (current_version > meta_table_.GetVersionNumber() &&
!ChangeVersion(&meta_table_, current_version, false)) {
return FailedMigrationTo(current_version);
}
DCHECK_GT(current_version, kDeprecatedVersionNumber);
for (int next_version = current_version + 1;
next_version <= kCurrentVersionNumber; ++next_version) {
// Do any database-wide migrations.
bool update_compatible_version = false;
if (!MigrateToVersion(next_version, &update_compatible_version) ||
!ChangeVersion(&meta_table_, next_version, update_compatible_version)) {
return FailedMigrationTo(next_version);
}
// Give each table a chance to migrate to this version.
for (const auto& table : tables_) {
// Any of the tables may set this to true, but by default it is false.
update_compatible_version = false;
if (!table.second->MigrateToVersion(next_version,
&update_compatible_version) ||
!ChangeVersion(&meta_table_, next_version,
update_compatible_version)) {
return FailedMigrationTo(next_version);
}
}
base::UmaHistogramExactLinear("WebDatabase.SucceededMigrationToVersion",
next_version,
WebDatabase::kCurrentVersionNumber + 1);
}
return sql::INIT_OK;
}
bool WebDatabase::MigrateToVersion(int version,
bool* update_compatible_version) {
// Migrate if necessary.
switch (version) {
case 58:
*update_compatible_version = true;
return MigrateToVersion58DropWebAppsAndIntents();
case 79:
*update_compatible_version = true;
return MigrateToVersion79DropLoginsTable();
case 105:
*update_compatible_version = true;
return MigrateToVersion105DropIbansTable();
}
return true;
}
bool WebDatabase::MigrateToVersion58DropWebAppsAndIntents() {
sql::Transaction transaction(&db_);
return transaction.Begin() && db_.Execute("DROP TABLE IF EXISTS web_apps") &&
db_.Execute("DROP TABLE IF EXISTS web_app_icons") &&
db_.Execute("DROP TABLE IF EXISTS web_intents") &&
db_.Execute("DROP TABLE IF EXISTS web_intents_defaults") &&
transaction.Commit();
}
bool WebDatabase::MigrateToVersion79DropLoginsTable() {
sql::Transaction transaction(&db_);
return transaction.Begin() &&
db_.Execute("DROP TABLE IF EXISTS ie7_logins") &&
db_.Execute("DROP TABLE IF EXISTS logins") && transaction.Commit();
}
bool WebDatabase::MigrateToVersion105DropIbansTable() {
return db_.Execute("DROP TABLE IF EXISTS ibans");
}