// Copyright (c) 2012 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 "components/webdata/common/web_database.h"

#include <algorithm>

#include "base/logging.h"
#include "base/stl_util.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 = 92;

const int WebDatabase::kDeprecatedVersionNumber = 51;

const base::FilePath::CharType WebDatabase::kInMemoryPath[] =
    FILE_PATH_LITERAL(":memory");

namespace {

const int kCompatibleVersionNumber = 83;

// Change the version number and possibly the compatibility version of
// |meta_table_|.
void ChangeVersion(sql::MetaTable* meta_table,
                   int version_num,
                   bool update_compatible_version_num) {
  meta_table->SetVersionNumber(version_num);
  if (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
               << ".";
  NOTREACHED();
  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() {}

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)) {
    return sql::INIT_FAILURE;
  }

  // Clobber really old databases.
  static_assert(kDeprecatedVersionNumber < kCurrentVersionNumber,
                "Deprecation version must be less than current");
  sql::MetaTable::RazeIfDeprecated(&db_, kDeprecatedVersionNumber);

  // Scope initialization in a transaction so we can't be partially
  // initialized.
  sql::Transaction transaction(&db_);
  if (!transaction.Begin())
    return sql::INIT_FAILURE;

  // Version check.
  if (!meta_table_.Init(&db_, kCurrentVersionNumber, kCompatibleVersionNumber))
    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 (auto it = tables_.begin(); it != tables_.end(); ++it) {
    it->second->Init(&db_, &meta_table_);
  }

  // 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 (auto it = tables_.begin(); it != tables_.end(); ++it) {
    if (!it->second->CreateTablesIfNecessary()) {
      LOG(WARNING) << "Unable to initialize the web database.";
      return sql::INIT_FAILURE;
    }
  }

  return transaction.Commit() ? 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);

  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))
      return FailedMigrationTo(next_version);

    ChangeVersion(&meta_table_, next_version, update_compatible_version);

    // Give each table a chance to migrate to this version.
    for (auto it = tables_.begin(); it != tables_.end(); ++it) {
      // Any of the tables may set this to true, but by default it is false.
      update_compatible_version = false;
      if (!it->second->MigrateToVersion(next_version,
                                        &update_compatible_version)) {
        return FailedMigrationTo(next_version);
      }

      ChangeVersion(&meta_table_, next_version, update_compatible_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();
  }

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