/* * Copyright (C) 2008, 2009, 2010, 2013 Apple Inc. All rights reserved. * * 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 "LocalStorageDatabase.h" #include "LocalStorageDatabaseTracker.h" #include #include #include #include #include #include #include #include #include #include using namespace WebCore; static const auto databaseUpdateInterval = std::chrono::seconds(1); static const int maximumItemsToUpdate = 100; namespace WebKit { Ref LocalStorageDatabase::create(Ref&& queue, Ref&& tracker, const SecurityOriginData& securityOrigin) { return adoptRef(*new LocalStorageDatabase(WTFMove(queue), WTFMove(tracker), securityOrigin)); } LocalStorageDatabase::LocalStorageDatabase(Ref&& queue, Ref&& tracker, const SecurityOriginData& securityOrigin) : m_queue(WTFMove(queue)) , m_tracker(WTFMove(tracker)) , m_securityOrigin(securityOrigin) , m_databasePath(m_tracker->databasePath(m_securityOrigin)) , m_failedToOpenDatabase(false) , m_didImportItems(false) , m_isClosed(false) , m_didScheduleDatabaseUpdate(false) , m_shouldClearItems(false) { } LocalStorageDatabase::~LocalStorageDatabase() { ASSERT(m_isClosed); } void LocalStorageDatabase::openDatabase(DatabaseOpeningStrategy openingStrategy) { ASSERT(!m_database.isOpen()); ASSERT(!m_failedToOpenDatabase); if (!tryToOpenDatabase(openingStrategy)) { m_failedToOpenDatabase = true; return; } if (m_database.isOpen()) m_tracker->didOpenDatabaseWithOrigin(m_securityOrigin); } bool LocalStorageDatabase::tryToOpenDatabase(DatabaseOpeningStrategy openingStrategy) { if (!fileExists(m_databasePath) && openingStrategy == SkipIfNonExistent) return true; if (m_databasePath.isEmpty()) { LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage"); return false; } if (!m_database.open(m_databasePath)) { LOG_ERROR("Failed to open database file %s for local storage", m_databasePath.utf8().data()); return false; } // Since a WorkQueue isn't bound to a specific thread, we have to disable threading checks // even though we never access the database from different threads simultaneously. m_database.disableThreadingChecks(); if (!migrateItemTableIfNeeded()) { // We failed to migrate the item table. In order to avoid trying to migrate the table over and over, // just delete it and start from scratch. if (!m_database.executeCommand("DROP TABLE ItemTable")) LOG_ERROR("Failed to delete table ItemTable for local storage"); } if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)")) { LOG_ERROR("Failed to create table ItemTable for local storage"); return false; } return true; } bool LocalStorageDatabase::migrateItemTableIfNeeded() { if (!m_database.tableExists("ItemTable")) return true; SQLiteStatement query(m_database, "SELECT value FROM ItemTable LIMIT 1"); // This query isn't ever executed, it's just used to check the column type. if (query.isColumnDeclaredAsBlob(0)) return true; // Create a new table with the right type, copy all the data over to it and then replace the new table with the old table. static const char* commands[] = { "DROP TABLE IF EXISTS ItemTable2", "CREATE TABLE ItemTable2 (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)", "INSERT INTO ItemTable2 SELECT * from ItemTable", "DROP TABLE ItemTable", "ALTER TABLE ItemTable2 RENAME TO ItemTable", 0, }; SQLiteTransaction transaction(m_database, false); transaction.begin(); for (size_t i = 0; commands[i]; ++i) { if (m_database.executeCommand(commands[i])) continue; LOG_ERROR("Failed to migrate table ItemTable for local storage when executing: %s", commands[i]); transaction.rollback(); return false; } transaction.commit(); return true; } void LocalStorageDatabase::importItems(StorageMap& storageMap) { if (m_didImportItems) return; // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing, // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894 // We set this to true even if we don't end up importing any items due to failure because // there's really no good way to recover other than not importing anything. m_didImportItems = true; openDatabase(SkipIfNonExistent); if (!m_database.isOpen()) return; SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable"); if (query.prepare() != SQLITE_OK) { LOG_ERROR("Unable to select items from ItemTable for local storage"); return; } HashMap items; int result = query.step(); while (result == SQLITE_ROW) { String key = query.getColumnText(0); String value = query.getColumnBlobAsString(1); if (!key.isNull() && !value.isNull()) items.set(key, value); result = query.step(); } if (result != SQLITE_DONE) { LOG_ERROR("Error reading items from ItemTable for local storage"); return; } storageMap.importItems(items); } void LocalStorageDatabase::setItem(const String& key, const String& value) { itemDidChange(key, value); } void LocalStorageDatabase::removeItem(const String& key) { itemDidChange(key, String()); } void LocalStorageDatabase::clear() { m_changedItems.clear(); m_shouldClearItems = true; scheduleDatabaseUpdate(); } void LocalStorageDatabase::close() { ASSERT(!m_isClosed); m_isClosed = true; if (m_didScheduleDatabaseUpdate) { updateDatabaseWithChangedItems(m_changedItems); m_changedItems.clear(); } bool isEmpty = databaseIsEmpty(); if (m_database.isOpen()) m_database.close(); if (isEmpty) m_tracker->deleteDatabaseWithOrigin(m_securityOrigin); } void LocalStorageDatabase::itemDidChange(const String& key, const String& value) { m_changedItems.set(key, value); scheduleDatabaseUpdate(); } void LocalStorageDatabase::scheduleDatabaseUpdate() { if (m_didScheduleDatabaseUpdate) return; if (!m_disableSuddenTerminationWhileWritingToLocalStorage) m_disableSuddenTerminationWhileWritingToLocalStorage = std::make_unique(); m_didScheduleDatabaseUpdate = true; RefPtr localStorageDatabase(this); m_queue->dispatchAfter(databaseUpdateInterval, [localStorageDatabase] { localStorageDatabase->updateDatabase(); }); } void LocalStorageDatabase::updateDatabase() { if (m_isClosed) return; ASSERT(m_didScheduleDatabaseUpdate); m_didScheduleDatabaseUpdate = false; HashMap changedItems; if (m_changedItems.size() <= maximumItemsToUpdate) { // There are few enough changed items that we can just always write all of them. m_changedItems.swap(changedItems); updateDatabaseWithChangedItems(changedItems); m_disableSuddenTerminationWhileWritingToLocalStorage = nullptr; } else { for (int i = 0; i < maximumItemsToUpdate; ++i) { auto it = m_changedItems.begin(); changedItems.add(it->key, it->value); m_changedItems.remove(it); } ASSERT(changedItems.size() <= maximumItemsToUpdate); // Reschedule the update for the remaining items. scheduleDatabaseUpdate(); updateDatabaseWithChangedItems(changedItems); } } void LocalStorageDatabase::updateDatabaseWithChangedItems(const HashMap& changedItems) { if (!m_database.isOpen()) openDatabase(CreateIfNonExistent); if (!m_database.isOpen()) return; if (m_shouldClearItems) { m_shouldClearItems = false; SQLiteStatement clearStatement(m_database, "DELETE FROM ItemTable"); if (clearStatement.prepare() != SQLITE_OK) { LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database"); return; } int result = clearStatement.step(); if (result != SQLITE_DONE) { LOG_ERROR("Failed to clear all items in the local storage database - %i", result); return; } } SQLiteStatement insertStatement(m_database, "INSERT INTO ItemTable VALUES (?, ?)"); if (insertStatement.prepare() != SQLITE_OK) { LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database"); return; } SQLiteStatement deleteStatement(m_database, "DELETE FROM ItemTable WHERE key=?"); if (deleteStatement.prepare() != SQLITE_OK) { LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database"); return; } SQLiteTransaction transaction(m_database); transaction.begin(); for (auto it = changedItems.begin(), end = changedItems.end(); it != end; ++it) { // A null value means that the key/value pair should be deleted. SQLiteStatement& statement = it->value.isNull() ? deleteStatement : insertStatement; statement.bindText(1, it->key); // If we're inserting a key/value pair, bind the value as well. if (!it->value.isNull()) statement.bindBlob(2, it->value); int result = statement.step(); if (result != SQLITE_DONE) { LOG_ERROR("Failed to update item in the local storage database - %i", result); break; } statement.reset(); } transaction.commit(); } bool LocalStorageDatabase::databaseIsEmpty() { if (!m_database.isOpen()) return false; SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable"); if (query.prepare() != SQLITE_OK) { LOG_ERROR("Unable to count number of rows in ItemTable for local storage"); return false; } int result = query.step(); if (result != SQLITE_ROW) { LOG_ERROR("No results when counting number of rows in ItemTable for local storage"); return false; } return !query.getColumnInt(0); } } // namespace WebKit