| /* |
| * Copyright (C) 2018 Sony Interactive Entertainment Inc. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
| * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "SearchPopupMenuDB.h" |
| |
| #include "SQLiteFileSystem.h" |
| #include "SQLiteTransaction.h" |
| #include <wtf/FileSystem.h> |
| #include <wtf/Vector.h> |
| #include <wtf/text/StringConcatenateNumbers.h> |
| |
| namespace WebCore { |
| |
| static const int schemaVersion = 1; |
| static constexpr auto createSearchTableSQL { |
| "CREATE TABLE IF NOT EXISTS Search (" |
| " name TEXT NOT NULL," |
| " position INTEGER NOT NULL," |
| " value TEXT," |
| " UNIQUE(name, position)" |
| ");"_s |
| }; |
| static constexpr auto loadSearchTermsForNameSQL { |
| "SELECT value FROM Search " |
| "WHERE name = ? " |
| "ORDER BY position;"_s |
| }; |
| static constexpr auto insertSearchTermSQL { |
| "INSERT INTO Search(name, position, value) " |
| "VALUES(?, ?, ?);"_s |
| }; |
| static constexpr auto removeSearchTermsForNameSQL { |
| "DELETE FROM Search where name = ?;"_s |
| }; |
| |
| SearchPopupMenuDB& SearchPopupMenuDB::singleton() |
| { |
| static SearchPopupMenuDB instance; |
| return instance; |
| } |
| |
| SearchPopupMenuDB::SearchPopupMenuDB() |
| : m_databaseFilename(FileSystem::pathByAppendingComponent(FileSystem::localUserSpecificStorageDirectory(), "autosave-search.db")) |
| { |
| } |
| |
| SearchPopupMenuDB::~SearchPopupMenuDB() |
| { |
| closeDatabase(); |
| } |
| |
| void SearchPopupMenuDB::saveRecentSearches(const String& name, const Vector<RecentSearch>& searches) |
| { |
| if (!m_database.isOpen()) { |
| if (!openDatabase()) |
| return; |
| } |
| |
| bool success = true; |
| |
| SQLiteTransaction transaction(m_database, false); |
| |
| m_removeSearchTermsForNameStatement->bindText(1, name); |
| auto stepRet = m_removeSearchTermsForNameStatement->step(); |
| success = stepRet == SQLITE_DONE; |
| m_removeSearchTermsForNameStatement->reset(); |
| |
| int index = 0; |
| for (const auto& search : searches) { |
| m_insertSearchTermStatement->bindText(1, name); |
| m_insertSearchTermStatement->bindInt(2, index); |
| m_insertSearchTermStatement->bindText(3, search.string); |
| if (success) { |
| stepRet = m_insertSearchTermStatement->step(); |
| success = stepRet == SQLITE_DONE; |
| } |
| m_insertSearchTermStatement->reset(); |
| index++; |
| } |
| |
| if (success) |
| transaction.commit(); |
| checkSQLiteReturnCode(stepRet); |
| } |
| |
| void SearchPopupMenuDB::loadRecentSearches(const String& name, Vector<RecentSearch>& searches) |
| { |
| if (!m_database.isOpen()) { |
| if (!openDatabase()) |
| return; |
| } |
| |
| searches.clear(); |
| |
| m_loadSearchTermsForNameStatement->bindText(1, name); |
| while (m_loadSearchTermsForNameStatement->step() == SQLITE_ROW) { |
| // We are choosing not to use or store search times on Windows at this time, so for now it's OK to use a "distant past" time as a placeholder. |
| searches.append({ m_loadSearchTermsForNameStatement->getColumnText(0), -WallTime::infinity() }); |
| } |
| m_loadSearchTermsForNameStatement->reset(); |
| } |
| |
| |
| bool SearchPopupMenuDB::checkDatabaseValidity() |
| { |
| ASSERT(m_database.isOpen()); |
| |
| if (!m_database.tableExists("Search")) |
| return false; |
| |
| SQLiteStatement integrity(m_database, "PRAGMA quick_check;"); |
| if (integrity.prepare() != SQLITE_OK) { |
| LOG_ERROR("Failed to execute database integrity check"); |
| return false; |
| } |
| |
| int resultCode = integrity.step(); |
| if (resultCode != SQLITE_ROW) { |
| LOG_ERROR("Integrity quick_check step returned %d", resultCode); |
| return false; |
| } |
| |
| int columns = integrity.columnCount(); |
| if (columns != 1) { |
| LOG_ERROR("Received %i columns performing integrity check, should be 1", columns); |
| return false; |
| } |
| |
| String resultText = integrity.getColumnText(0); |
| |
| if (resultText != "ok") { |
| LOG_ERROR("Search autosave database integrity check failed - %s", resultText.ascii().data()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void SearchPopupMenuDB::deleteAllDatabaseFiles() |
| { |
| closeDatabase(); |
| |
| FileSystem::deleteFile(m_databaseFilename); |
| FileSystem::deleteFile(m_databaseFilename + "-shm"); |
| FileSystem::deleteFile(m_databaseFilename + "-wal"); |
| } |
| |
| bool SearchPopupMenuDB::openDatabase() |
| { |
| bool existsDatabaseFile = SQLiteFileSystem::ensureDatabaseFileExists(m_databaseFilename, false); |
| |
| if (existsDatabaseFile) { |
| if (m_database.open(m_databaseFilename)) { |
| if (!checkDatabaseValidity()) { |
| // delete database and try to re-create again |
| LOG_ERROR("Search autosave database validity check failed, attempting to recreate the database"); |
| m_database.close(); |
| deleteAllDatabaseFiles(); |
| existsDatabaseFile = false; |
| } |
| } else { |
| LOG_ERROR("Failed to open search autosave database: %s, attempting to recreate the database", m_databaseFilename.utf8().data()); |
| deleteAllDatabaseFiles(); |
| existsDatabaseFile = false; |
| } |
| } |
| |
| if (!existsDatabaseFile) { |
| if (!FileSystem::makeAllDirectories(FileSystem::directoryName(m_databaseFilename))) |
| LOG_ERROR("Failed to create the search autosave database path %s", m_databaseFilename.utf8().data()); |
| |
| m_database.open(m_databaseFilename); |
| } |
| |
| if (!m_database.isOpen()) |
| return false; |
| |
| if (!m_database.turnOnIncrementalAutoVacuum()) |
| LOG_ERROR("Unable to turn on incremental auto-vacuum (%d %s)", m_database.lastError(), m_database.lastErrorMsg()); |
| |
| verifySchemaVersion(); |
| |
| bool databaseValidity = true; |
| if (!existsDatabaseFile || !m_database.tableExists("Search")) |
| databaseValidity = databaseValidity && (executeSimpleSql(createSearchTableSQL) == SQLITE_DONE); |
| |
| if (!databaseValidity) { |
| // give up create database at this time (search terms will not be saved) |
| m_database.close(); |
| deleteAllDatabaseFiles(); |
| return false; |
| } |
| |
| m_database.setSynchronous(SQLiteDatabase::SyncNormal); |
| |
| m_loadSearchTermsForNameStatement = createPreparedStatement(loadSearchTermsForNameSQL); |
| m_insertSearchTermStatement = createPreparedStatement(insertSearchTermSQL); |
| m_removeSearchTermsForNameStatement = createPreparedStatement(removeSearchTermsForNameSQL); |
| |
| return true; |
| } |
| |
| void SearchPopupMenuDB::closeDatabase() |
| { |
| if (m_database.isOpen()) { |
| m_loadSearchTermsForNameStatement->finalize(); |
| m_insertSearchTermStatement->finalize(); |
| m_removeSearchTermsForNameStatement->finalize(); |
| m_database.close(); |
| } |
| } |
| |
| void SearchPopupMenuDB::verifySchemaVersion() |
| { |
| int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0); |
| if (version == schemaVersion) |
| return; |
| |
| switch (version) { |
| // Placeholder for schema version upgrade logic |
| // Ensure cases fall through to the next version's upgrade logic |
| case 0: |
| m_database.clearAllTables(); |
| break; |
| default: |
| // This case can be reached when downgrading versions |
| LOG_ERROR("Unknown search autosave database version: %d", version); |
| m_database.clearAllTables(); |
| break; |
| } |
| |
| // Update version |
| executeSimpleSql(makeString("PRAGMA user_version=", schemaVersion)); |
| } |
| |
| void SearchPopupMenuDB::checkSQLiteReturnCode(int actual) |
| { |
| switch (actual) { |
| case SQLITE_CORRUPT: |
| case SQLITE_SCHEMA: |
| case SQLITE_FORMAT: |
| case SQLITE_NOTADB: |
| // Database has been corrupted during the run |
| // so we'll recreate the db. |
| deleteAllDatabaseFiles(); |
| openDatabase(); |
| } |
| } |
| |
| int SearchPopupMenuDB::executeSimpleSql(const String& sql, bool ignoreError) |
| { |
| SQLiteStatement statement(m_database, sql); |
| int ret = statement.prepareAndStep(); |
| statement.finalize(); |
| |
| checkSQLiteReturnCode(ret); |
| if (ret != SQLITE_OK && ret != SQLITE_DONE && ret != SQLITE_ROW && !ignoreError) |
| LOG_ERROR("Failed to execute %s error: %s", sql.ascii().data(), m_database.lastErrorMsg()); |
| |
| return ret; |
| } |
| |
| std::unique_ptr<SQLiteStatement> SearchPopupMenuDB::createPreparedStatement(const String& sql) |
| { |
| auto statement = makeUnique<SQLiteStatement>(m_database, sql); |
| int ret = statement->prepare(); |
| ASSERT_UNUSED(ret, ret == SQLITE_OK); |
| return statement; |
| } |
| |
| } |