| // 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, | |
| base::FEATURE_DISABLED_BY_DEFAULT); | |
| BASE_FEATURE(kSqlWALModeOnWebDatabase, 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 151 writes tuples to one of `autofill::EntityTable`'s tables that | |
| // are not processed correctly by clients with version 150. As a result, | |
| // some `autofill::EntityInstance`s on these old clients would be incomplete. | |
| constexpr int kCompatibleVersionNumber = 151; | |
| // 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"); | |
| } |