| // 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_.Execute("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" |
| // Given that support for HISTORY_KEYWORD suggestions has been deprecated, |
| // this code is necessary in order to purge any old HISTORY_KEYWORD |
| // entries that might still be present in the shortcuts DB, thereby |
| // preventing ShortcutsProvider from surfacing these invalid suggestions. |
| 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; |
| } |
| } |