// 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/omnibox/browser/shortcuts_database.h"

#include <string>
#include <tuple>

#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/common/omnibox_features.h"
#include "sql/meta_table.h"
#include "sql/recovery.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "ui/base/page_transition_types.h"

// Helpers --------------------------------------------------------------------

namespace {

// Current version number. We write databases at the "current" version number,
// but any previous version that can read the "compatible" one can make do with
// our database without *too* many bad effects.
const int kCurrentVersionNumber = 2;
const int kCompatibleVersionNumber = 1;

void BindShortcutToStatement(const ShortcutsDatabase::Shortcut& shortcut,
                             sql::Statement& s) {
  DCHECK(base::Uuid::ParseCaseInsensitive(shortcut.id).is_valid());
  s.BindString(0, shortcut.id);
  s.BindString16(1, shortcut.text);
  s.BindString16(2, shortcut.match_core.fill_into_edit);
  s.BindString(3, shortcut.match_core.destination_url.spec());
  s.BindInt(4, base::checked_cast<int>(shortcut.match_core.document_type));
  s.BindString16(5, shortcut.match_core.contents);
  s.BindString(6, shortcut.match_core.contents_class);
  s.BindString16(7, shortcut.match_core.description);
  s.BindString(8, shortcut.match_core.description_class);
  s.BindInt(9, base::checked_cast<int>(shortcut.match_core.transition));
  s.BindInt(10, base::checked_cast<int>(shortcut.match_core.type));
  s.BindString16(11, shortcut.match_core.keyword);
  s.BindTime(12, shortcut.last_access_time);
  s.BindInt(13, shortcut.number_of_hits);
}

void DatabaseErrorCallback(sql::Database* db,
                           int extended_error,
                           sql::Statement* stmt) {
  // Attempt to recover a corrupt database, if it is eligible to be recovered.
  if (sql::Recovery::RecoverIfPossible(
          db, extended_error, sql::Recovery::Strategy::kRecoverOrRaze)) {
    // Recovery was attempted. The database handle has been poisoned and the
    // error callback has been reset.

    // Signal the test-expectation framework that the error was handled.
    std::ignore = sql::Database::IsExpectedSqliteError(extended_error);
    return;
  }

  // The default handling is to assert on debug and to ignore on release.
  if (!sql::Database::IsExpectedSqliteError(extended_error))
    DLOG(FATAL) << db->GetErrorMessage();
}

}  // namespace

// ShortcutsDatabase::Shortcut::MatchCore -------------------------------------

ShortcutsDatabase::Shortcut::MatchCore::MatchCore(
    const std::u16string& fill_into_edit,
    const GURL& destination_url,
    AutocompleteMatch::DocumentType document_type,
    const std::u16string& contents,
    const std::string& contents_class,
    const std::u16string& description,
    const std::string& description_class,
    ui::PageTransition transition,
    AutocompleteMatchType::Type type,
    const std::u16string& keyword)
    : fill_into_edit(fill_into_edit),
      destination_url(destination_url),
      document_type(document_type),
      contents(contents),
      contents_class(contents_class),
      description(description),
      description_class(description_class),
      transition(transition),
      type(type),
      keyword(keyword) {}

ShortcutsDatabase::Shortcut::MatchCore::MatchCore(const MatchCore&) = default;

ShortcutsDatabase::Shortcut::MatchCore::~MatchCore() = default;

// ShortcutsDatabase::Shortcut ------------------------------------------------

ShortcutsDatabase::Shortcut::Shortcut(const std::string& id,
                                      const std::u16string& text,
                                      const MatchCore& match_core,
                                      const base::Time& last_access_time,
                                      int number_of_hits)
    : id(id),
      text(text),
      match_core(match_core),
      last_access_time(last_access_time),
      number_of_hits(number_of_hits) {}

ShortcutsDatabase::Shortcut::Shortcut()
    : match_core(std::u16string(),
                 GURL(),
                 AutocompleteMatch::DocumentType::NONE,
                 std::u16string(),
                 std::string(),
                 std::u16string(),
                 std::string(),
                 ui::PageTransition::PAGE_TRANSITION_FIRST,
                 // AutocompleteMatchType doesn't have a sentinel or null value,
                 // so we just use the value equal to 0. This constructor is
                 // only used by STL anyways, so this is harmless.
                 AutocompleteMatchType::Type::URL_WHAT_YOU_TYPED,
                 std::u16string()),
      last_access_time(base::Time::Now()),
      number_of_hits(0) {}

ShortcutsDatabase::Shortcut::Shortcut(const Shortcut&) = default;

ShortcutsDatabase::Shortcut::~Shortcut() = default;

// ShortcutsDatabase ----------------------------------------------------------

ShortcutsDatabase::ShortcutsDatabase(const base::FilePath& database_path)
    : db_(/*tag=*/"Shortcuts"), database_path_(database_path) {}

bool ShortcutsDatabase::Init() {
  if (!db_.has_error_callback()) {
    // The error callback may be reset if recovery was attempted, so ensure the
    // callback is re-set when the database is re-opened.
    db_.set_error_callback(base::BindRepeating(&DatabaseErrorCallback, &db_));
  }

  // Attach the database to our index file.
  return db_.Open(database_path_) && EnsureTable();
}

bool ShortcutsDatabase::AddShortcut(const Shortcut& shortcut) {
  static constexpr char kInsertSql[] =
      // clang-format off
      "INSERT INTO omni_box_shortcuts("
        "id,text,fill_into_edit,url,document_type,contents,contents_class,"
        "description,description_class,transition,type,keyword,"
        "last_access_time,number_of_hits)"
      "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
  // clang-format on

  sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE, kInsertSql));
  BindShortcutToStatement(shortcut, s);
  return s.Run();
}

bool ShortcutsDatabase::UpdateShortcut(const Shortcut& shortcut) {
  static constexpr char kUpdateSql[] =
      // clang-format off
      "UPDATE omni_box_shortcuts "
        "SET "
          "id=?,text=?,fill_into_edit=?,url=?,"
          "document_type=?,contents=?,contents_class=?,description=?,"
          "description_class=?,transition=?,type=?,keyword=?,"
          "last_access_time=?,number_of_hits=? "
        "WHERE id=?";
  // clang-format on

  sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE, kUpdateSql));
  BindShortcutToStatement(shortcut, s);
  s.BindString(14, shortcut.id);
  return s.Run();
}

bool ShortcutsDatabase::DeleteShortcutsWithIDs(
    const ShortcutIDs& shortcut_ids) {
  sql::Transaction transaction(&db_);
  if (!transaction.Begin()) {
    return false;
  }

  sql::Statement s(
      db_.GetUniqueStatement("DELETE FROM omni_box_shortcuts WHERE id=?"));

  for (const std::string& id : shortcut_ids) {
    s.Reset(/*clear_bound_vars=*/true);
    s.BindString(0, id);
    if (!s.Run()) {
      return false;
    }
  }

  return transaction.Commit();
}

bool ShortcutsDatabase::DeleteShortcutsWithURL(
    const std::string& shortcut_url_spec) {
  sql::Statement s(
      db_.GetUniqueStatement("DELETE FROM omni_box_shortcuts WHERE url=?"));
  s.BindString(0, shortcut_url_spec);
  return s.Run();
}

bool ShortcutsDatabase::DeleteAllShortcuts() {
  if (!db_.Execute("DELETE FROM omni_box_shortcuts"))
    return false;

  std::ignore = db_.Vacuum();
  return true;
}

void ShortcutsDatabase::LoadShortcuts(GuidToShortcutMap* shortcuts) {
  DCHECK(shortcuts);
  shortcuts->clear();

  // List of shortcuts that need to be purged from the shortcuts DB (e.g. due to
  // using a deprecated suggestion type).
  ShortcutIDs invalid_shortcuts;

  static constexpr char kSelectSql[] =
      // clang-format off
      "SELECT id,text,fill_into_edit,url,document_type,contents,contents_class,"
        "description,description_class,transition,type,keyword,"
        "last_access_time,number_of_hits "
      "FROM omni_box_shortcuts";
  // clang-format on

  sql::Statement s(db_.GetUniqueStatement(kSelectSql));

  while (s.Step()) {
    // Some users have corrupt data in their SQL database. That causes crashes.
    // Therefore, validate the integral values first. https://crbug.com/1024114
    AutocompleteMatch::DocumentType document_type;
    if (!AutocompleteMatch::DocumentTypeFromInteger(s.ColumnInt(4),
                                                    &document_type)) {
      continue;
    }

    AutocompleteMatchType::Type type;
    if (!AutocompleteMatchType::FromInteger(s.ColumnInt(10), &type))
      continue;

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
    // `HISTORY_KEYWORD` suggestions are no longer recorded to the shortcut DB
    // because a) they're redundant with the keyword provider, and b) we haven't
    // wired in deleting the keyword to delete the shortcut entry. Purge any old
    // `HISTORY_KEYWORD` entries already recorded to the DB.
    if (type == AutocompleteMatchType::HISTORY_KEYWORD) {
      invalid_shortcuts.push_back(s.ColumnString(0));
      continue;
    }
#pragma GCC diagnostic pop

    const int page_transition_integer = s.ColumnInt(9);
    if (!ui::IsValidPageTransitionType(page_transition_integer)) {
      continue;
    }
    ui::PageTransition transition =
        ui::PageTransitionFromInt(page_transition_integer);

    Shortcut::MatchCore match_core =
        Shortcut::MatchCore(s.ColumnString16(2),          // fill_into_edit
                            GURL(s.ColumnStringView(3)),  // destination_url
                            document_type,                // document_type
                            s.ColumnString16(5),          // contents
                            s.ColumnString(6),            // contents_class
                            s.ColumnString16(7),          // description
                            s.ColumnString(8),            // description_class
                            transition,                   // transition
                            type,                         // type
                            s.ColumnString16(11));        // keyword

    std::stringstream debug_stream;
    debug_stream << "Contents: " << match_core.contents;
    debug_stream << ", Description: " << match_core.description;
    debug_stream << ", Type: "
                 << AutocompleteMatchType::ToString(match_core.type);
    debug_stream << ", Provider: Shortcuts";
    DCHECK(match_core.destination_url.is_valid()) << debug_stream.str();
    if (!match_core.destination_url.is_valid()) {
      continue;
    }

    shortcuts->insert(
        std::make_pair(s.ColumnString(0),
                       Shortcut(s.ColumnString(0),      // id
                                s.ColumnString16(1),    // text
                                std::move(match_core),  // match_core
                                s.ColumnTime(12),       // last_access_time
                                s.ColumnInt(13))));     // number_of_hits
  }

  DeleteShortcutsWithIDs(invalid_shortcuts);
}

ShortcutsDatabase::~ShortcutsDatabase() = default;

bool ShortcutsDatabase::EnsureTable() {
  if (!db_.DoesTableExist("omni_box_shortcuts"))
    return DoMigration(-1);

  // The first version of the shortcuts table (pre-v0) lacked the
  // fill_into_edit, transition, type, and keyword columns.
  // Additionally, pre-v0 lacks a MetaTable from which to identify the version,
  // thus requiring checking the existence of those columns.
  if (!db_.DoesColumnExist("omni_box_shortcuts", "fill_into_edit") &&
      !DoMigration(0)) {
    return false;
  }

  // v0 also lacks a MetaTable. Migrating to v1 introduces the MetaTable in
  // addition to other changes handled by |DoMigration|. If the MetaTable
  // exists, lookup |current_version|. Otherwise, leave it at 0, and
  // |DoMigration(1)| will create the MetaTable.
  int current_version = 0;
  if (sql::MetaTable::DoesTableExist(&db_)) {
    if (!(meta_table_.Init(&db_, 1, kCompatibleVersionNumber)))
      return false;
    current_version = meta_table_.GetVersionNumber();
  }

  for (int i = current_version + 1; i <= kCurrentVersionNumber; ++i) {
    if (!DoMigration(i)) {
      return false;
    }
  }

  return true;
}

bool ShortcutsDatabase::DoMigration(int version) {
  // The migration must be performed transactionally with the meta table
  // update, or else one of them can be applied without the other, leading to
  // incorrect logic on subsequent use.
  sql::Transaction transaction(&db_);
  if (!transaction.Begin()) {
    return false;
  }

  switch (version) {
    case -1:
      // When there is no existing table, skip iterative migration; instead,
      // migrate to the latest version.
      return meta_table_.Init(&db_, kCurrentVersionNumber,
                              kCompatibleVersionNumber) &&
             db_.Execute(
                 "CREATE TABLE omni_box_shortcuts(id VARCHAR PRIMARY KEY,"
                 "text VARCHAR,fill_into_edit VARCHAR,url VARCHAR,"
                 "document_type INTEGER,contents VARCHAR,"
                 "contents_class VARCHAR,description VARCHAR,"
                 "description_class VARCHAR,transition INTEGER,type INTEGER,"
                 "keyword VARCHAR,last_access_time INTEGER,"
                 "number_of_hits INTEGER)") &&
             transaction.Commit();
    case 0:
      static_assert(static_cast<int>(ui::PAGE_TRANSITION_TYPED) == 1);
      static_assert(static_cast<int>(AutocompleteMatchType::HISTORY_TITLE) ==
                    2);

      // Version pre-0 of the shortcuts table lacked the fill_into_edit,
      // transition type, and keyword columns.
      return db_.Execute(
                 "ALTER TABLE omni_box_shortcuts "
                 "ADD COLUMN fill_into_edit VARCHAR") &&
             db_.Execute("UPDATE omni_box_shortcuts SET fill_into_edit=url") &&
             db_.Execute(
                 "ALTER TABLE omni_box_shortcuts "
                 "ADD COLUMN transition INTEGER") &&
             db_.Execute("UPDATE omni_box_shortcuts SET transition=1") &&
             db_.Execute(
                 "ALTER TABLE omni_box_shortcuts ADD COLUMN type INTEGER") &&
             db_.Execute("UPDATE omni_box_shortcuts SET type=2") &&
             db_.Execute(
                 "ALTER TABLE omni_box_shortcuts ADD COLUMN keyword VARCHAR") &&
             transaction.Commit();
    case 1:
      return  // Create the MetaTable.
          meta_table_.Init(&db_, 1, kCompatibleVersionNumber) &&
          // Migrate old SEARCH_OTHER_ENGINE values to the new type value.
          db_.Execute("UPDATE omni_box_shortcuts SET type=13 WHERE type=9") &&
          // Migrate old EXTENSION_APP values to the new type value.
          db_.Execute("UPDATE omni_box_shortcuts SET type=14 WHERE type=10") &&
          // Migrate old CONTACT values to the new type value.
          db_.Execute("UPDATE omni_box_shortcuts SET type=15 WHERE type=11") &&
          // Migrate old BOOKMARK_TITLE values to the new type value.
          db_.Execute("UPDATE omni_box_shortcuts SET type=16 WHERE type=12") &&
          transaction.Commit();
    case 2:
      // Version 1 of the shortcuts table lacked the document_type column.
      return db_.Execute(
                 "ALTER TABLE omni_box_shortcuts "
                 "ADD COLUMN document_type INTEGER") &&
             meta_table_.SetVersionNumber(version) && transaction.Commit();
    default:
      return false;
  }
}
