| // 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/debug/crash_logging.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/strings/stringprintf.h" |
| #include "sql/transaction.h" |
| |
| // Current version number. Note: when changing the current version number, |
| // corresponding changes must happen in the unit tests, and new migration test |
| // added. See `WebDatabaseMigrationTest::kCurrentTestedVersionNumber`. |
| // static |
| const int WebDatabase::kCurrentVersionNumber = 112; |
| |
| const int WebDatabase::kDeprecatedVersionNumber = 82; |
| |
| const base::FilePath::CharType WebDatabase::kInMemoryPath[] = |
| FILE_PATH_LITERAL(":memory"); |
| |
| namespace { |
| |
| BASE_FEATURE(kWebDatabaseDumpWithoutCrashingOnInitProblems, |
| "WebDatabaseDumpWithoutCrashingOnInitProblems", |
| base::FEATURE_ENABLED_BY_DEFAULT); |
| |
| std::string GetDiagnostics(const sql::Database& db) { |
| if (!db.is_open()) { |
| return "Database is not open"; |
| } |
| return base::StringPrintf("ErrorCode: %d, LastErrorno: %d, Error: %s", |
| db.GetErrorCode(), db.GetLastErrno(), |
| db.GetErrorMessage()); |
| } |
| |
| // TODO(crbug.com/1430313): Remove when bug is fixed. |
| NOINLINE void LogDiagnostics(sql::Database& db) { |
| if (!base::FeatureList::IsEnabled( |
| kWebDatabaseDumpWithoutCrashingOnInitProblems)) { |
| return; |
| } |
| SCOPED_CRASH_KEY_STRING1024("db_init_error", "diagnostics", |
| GetDiagnostics(db)); |
| base::debug::DumpWithoutCrashing(); |
| } |
| |
| const int kCompatibleVersionNumber = 106; |
| |
| // 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 |
| << "."; |
| return sql::INIT_FAILURE; |
| } |
| |
| } // namespace |
| |
| WebDatabase::WebDatabase() |
| : db_({// Run the database in exclusive mode. Nobody else should be |
| // accessing the database while we're running, and this will give |
| // somewhat improved perf. |
| .exclusive_locking = true, |
| // 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). |
| .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. |
| .cache_size = 32}) {} |
| |
| WebDatabase::~WebDatabase() = default; |
| |
| void WebDatabase::AddTable(WebDatabaseTable* table) { |
| tables_[table->GetTypeKey()] = table; |
| } |
| |
| WebDatabaseTable* WebDatabase::GetTable(WebDatabaseTable::TypeKey key) { |
| return tables_[key]; |
| } |
| |
| void WebDatabase::BeginTransaction() { |
| db_.BeginTransaction(); |
| } |
| |
| void WebDatabase::CommitTransaction() { |
| db_.CommitTransaction(); |
| } |
| |
| 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) { |
| db_.set_histogram_tag("Web"); |
| |
| if ((db_name.value() == kInMemoryPath) ? !db_.OpenInMemory() |
| : !db_.Open(db_name)) { |
| LogDiagnostics(db_); |
| return sql::INIT_FAILURE; |
| } |
| |
| // Check whether we have write access at the earliest possible time. |
| // While failures can happen later as well, this gives us some clarity |
| // that we could at least use the database when we opened it. |
| // TODO(crbug.com/1430313): Remove when bug is fixed. |
| { |
| if (!db_.Execute("BEGIN EXCLUSIVE")) { |
| LogDiagnostics(db_); |
| return sql::INIT_FAILURE; |
| } |
| if (!db_.Execute("COMMIT")) { |
| LogDiagnostics(db_); |
| 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)) { |
| LogDiagnostics(db_); |
| return sql::INIT_FAILURE; |
| } |
| |
| // TODO(crbug.com/1430313): Remove when bug is fixed. |
| if (!db_.is_open()) { |
| LogDiagnostics(db_); |
| } |
| |
| // Scope initialization in a transaction so we can't be partially |
| // initialized. |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) { |
| LogDiagnostics(db_); |
| return sql::INIT_FAILURE; |
| } |
| |
| // Version check. |
| if (!meta_table_.Init(&db_, kCurrentVersionNumber, |
| kCompatibleVersionNumber)) { |
| LogDiagnostics(db_); |
| return sql::INIT_FAILURE; |
| } |
| if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) { |
| 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_); |
| // TODO(crbug.com/1430313): Remove when bug is fixed. |
| if (!db_.is_open()) { |
| LogDiagnostics(db_); |
| } |
| } |
| |
| // 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) { |
| LogDiagnostics(db_); |
| 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()) { |
| LogDiagnostics(db_); |
| LOG(WARNING) << "Unable to initialize the web database."; |
| return sql::INIT_FAILURE; |
| } |
| } |
| |
| bool result = transaction.Commit(); |
| if (!result) { |
| LogDiagnostics(db_); |
| } |
| return result ? sql::INIT_OK : sql::INIT_FAILURE; |
| } |
| |
| 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)) { |
| LogDiagnostics(db_); |
| 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)) { |
| LogDiagnostics(db_); |
| 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)) { |
| LogDiagnostics(db_); |
| return FailedMigrationTo(next_version); |
| } |
| } |
| } |
| 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"); |
| } |