// 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/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/stringprintf.h"
#include "components/os_crypt/async/common/encryptor.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);

void LogInitResult(WebDatabase::InitResult 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(WebDatabase::InitResult::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,
    scoped_refptr<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(InitResult::kCouldNotOpen);
    return sql::INIT_FAILURE;
  }
  DCHECK(db_.is_open());

  // 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(InitResult::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(InitResult::kFailedToBeginInitTransaction);
    return sql::INIT_FAILURE;
  }

  // Version check.
  if (!meta_table_.Init(&db_, kCurrentVersionNumber,
                        kCompatibleVersionNumber)) {
    LogInitResult(InitResult::kMetaTableInitFailed);
    return sql::INIT_FAILURE;
  }

  // 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(InitResult::kFailedToCreateTable);
      return sql::INIT_FAILURE;
    }
  }

  bool result = transaction.Commit();
  if (!result) {
    LogInitResult(InitResult::kFailedToCommitInitTransaction);
    return sql::INIT_FAILURE;
  }

  LogInitResult(InitResult::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();
    case 154:
      *update_compatible_version = true;
      return MigrateToVersion154DropPlusAddressTables();
  }

  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");
}

bool WebDatabase::MigrateToVersion154DropPlusAddressTables() {
  sql::Transaction transaction(&db_);
  return transaction.Begin() &&
         db_.Execute("DROP TABLE IF EXISTS plus_addresses") &&
         db_.Execute(
             "DROP TABLE IF EXISTS plus_address_sync_model_type_state") &&
         db_.Execute(
             "DROP TABLE IF EXISTS plus_address_sync_entity_metadata") &&
         transaction.Commit();
}
