blob: 5e1fab094e37315dea34601b17941a59eefa37cb [file] [log] [blame]
/*
* Copyright (C) 2008-2021 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 <WebCore/SQLiteFileSystem.h>
#include <WebCore/SQLiteStatement.h>
#include <WebCore/SQLiteStatementAutoResetScope.h>
#include <WebCore/SQLiteTransaction.h>
#include <WebCore/StorageMap.h>
#include <wtf/FileSystem.h>
#include <wtf/HashMap.h>
#include <wtf/RefPtr.h>
#include <wtf/RunLoop.h>
#include <wtf/SuspendableWorkQueue.h>
namespace WebKit {
using namespace WebCore;
constexpr auto getItemsQueryString { "SELECT key, value FROM ItemTable"_s };
constexpr unsigned maximumSizeForValuesKeptInMemory { 1024 }; // 1 KB
constexpr Seconds transactionDuration { 500_ms };
Ref<LocalStorageDatabase> LocalStorageDatabase::create(Ref<SuspendableWorkQueue>&& workQueue, String&& databasePath, unsigned quotaInBytes)
{
return adoptRef(*new LocalStorageDatabase(WTFMove(workQueue), WTFMove(databasePath), quotaInBytes));
}
LocalStorageDatabase::LocalStorageDatabase(Ref<SuspendableWorkQueue>&& workQueue, String&& databasePath, unsigned quotaInBytes)
: m_workQueue(WTFMove(workQueue))
, m_databasePath(WTFMove(databasePath))
, m_quotaInBytes(quotaInBytes)
{
ASSERT(!RunLoop::isMain());
}
LocalStorageDatabase::~LocalStorageDatabase()
{
ASSERT(!RunLoop::isMain());
ASSERT(m_isClosed);
}
bool LocalStorageDatabase::openDatabase(ShouldCreateDatabase shouldCreateDatabase)
{
ASSERT(!RunLoop::isMain());
if (!FileSystem::fileExists(m_databasePath)) {
if (shouldCreateDatabase == ShouldCreateDatabase::No)
return true;
m_items = HashMap<String, String> { };
}
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"_s))
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)"_s)) {
LOG_ERROR("Failed to create table ItemTable for local storage");
return false;
}
if (m_quotaInBytes != WebCore::StorageMap::noQuota)
m_database.setMaximumSize(m_quotaInBytes);
return true;
}
void LocalStorageDatabase::startTransactionIfNecessary()
{
if (!m_transaction)
m_transaction = makeUnique<SQLiteTransaction>(m_database);
if (m_transaction->inProgress())
return;
m_transaction->begin();
m_workQueue->dispatchAfter(transactionDuration, [weakThis = WeakPtr { *this }] {
if (weakThis)
weakThis->m_transaction->commit();
});
}
bool LocalStorageDatabase::migrateItemTableIfNeeded()
{
ASSERT(!RunLoop::isMain());
if (!m_database.tableExists("ItemTable"))
return true;
auto query = m_database.prepareStatement("SELECT value FROM ItemTable LIMIT 1"_s);
// This query isn't ever executed, it's just used to check the column type.
if (query && 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 ASCIILiteral commands[] = {
"DROP TABLE IF EXISTS ItemTable2"_s,
"CREATE TABLE ItemTable2 (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)"_s,
"INSERT INTO ItemTable2 SELECT * from ItemTable"_s,
"DROP TABLE ItemTable"_s,
"ALTER TABLE ItemTable2 RENAME TO ItemTable"_s,
ASCIILiteral::null(),
};
SQLiteTransaction transaction(m_database);
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].characters());
return false;
}
transaction.commit();
return true;
}
HashMap<String, String> LocalStorageDatabase::items() const
{
ASSERT(!RunLoop::isMain());
if (!m_database.isOpen())
return { };
HashMap<String, String> items;
if (m_items) {
items.reserveInitialCapacity(m_items->size());
for (auto& entry : *m_items) {
auto value = entry.value.isNull() ? itemBypassingCache(entry.key) : entry.value;
items.add(entry.key, WTFMove(value));
}
return items;
}
auto query = scopedStatement(m_getItemsStatement, getItemsQueryString);
if (!query) {
LOG_ERROR("Unable to select items from ItemTable for local storage");
return { };
}
m_items = HashMap<String, String> { };
int result = query->step();
while (result == SQLITE_ROW) {
String key = query->columnText(0);
String value = query->columnBlobAsString(1);
if (!key.isNull() && !value.isNull()) {
m_items->add(key, value.sizeInBytes() > maximumSizeForValuesKeptInMemory ? String() : value);
items.add(WTFMove(key), WTFMove(value));
}
result = query->step();
}
if (result != SQLITE_DONE)
LOG_ERROR("Error reading items from ItemTable for local storage");
return items;
}
void LocalStorageDatabase::removeItem(const String& key, String& oldValue)
{
ASSERT(!RunLoop::isMain());
if (!m_database.isOpen())
return;
startTransactionIfNecessary();
oldValue = item(key);
if (oldValue.isNull())
return;
auto deleteStatement = scopedStatement(m_deleteItemStatement, "DELETE FROM ItemTable WHERE key=?"_s);
if (!deleteStatement) {
LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database");
return;
}
deleteStatement->bindText(1, key);
int result = deleteStatement->step();
if (result != SQLITE_DONE) {
LOG_ERROR("Failed to delete item in the local storage database - %i", result);
return;
}
if (m_items)
m_items->remove(key);
}
String LocalStorageDatabase::item(const String& key) const
{
ASSERT(!RunLoop::isMain());
if (!m_database.isOpen())
return { };
if (m_items) {
// Use find() instead of get() since a null String is a valid value here.
auto it = m_items->find(key);
if (it == m_items->end())
return { };
if (!it->value.isNull())
return it->value;
// The value is too large and needs to be fetched from the database.
}
return itemBypassingCache(key);
}
String LocalStorageDatabase::itemBypassingCache(const String& key) const
{
auto query = scopedStatement(m_getItemStatement, "SELECT value FROM ItemTable WHERE key=?"_s);
if (!query) {
LOG_ERROR("Unable to get item from ItemTable for local storage");
return { };
}
query->bindText(1, key);
int result = query->step();
if (result == SQLITE_ROW)
return query->columnBlobAsString(0);
if (result != SQLITE_DONE)
LOG_ERROR("Error get item from ItemTable for local storage");
return { };
}
void LocalStorageDatabase::setItem(const String& key, const String& value, String& oldValue, bool& quotaException)
{
ASSERT(!RunLoop::isMain());
if (!m_database.isOpen())
openDatabase(ShouldCreateDatabase::Yes);
if (!m_database.isOpen())
return;
startTransactionIfNecessary();
oldValue = item(key);
auto insertStatement = scopedStatement(m_insertStatement, "INSERT INTO ItemTable VALUES (?, ?)"_s);
if (!insertStatement) {
LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database");
return;
}
insertStatement->bindText(1, key);
insertStatement->bindBlob(2, value);
int result = insertStatement->step();
if (result != SQLITE_DONE) {
LOG_ERROR("Failed to update item in the local storage database - %i", result);
if (result == SQLITE_FULL)
quotaException = true;
return;
}
if (m_items)
m_items->set(key, value.sizeInBytes() > maximumSizeForValuesKeptInMemory ? String() : value);
}
bool LocalStorageDatabase::clear()
{
ASSERT(!RunLoop::isMain());
if (!m_database.isOpen())
return false;
if (m_items && m_items->isEmpty())
return false;
startTransactionIfNecessary();
auto clearStatement = scopedStatement(m_clearStatement, "DELETE FROM ItemTable"_s);
if (!clearStatement) {
LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database");
return false;
}
int result = clearStatement->step();
if (result != SQLITE_DONE) {
LOG_ERROR("Failed to clear all items in the local storage database - %i", result);
return false;
}
if (m_items) {
m_items->clear();
return true;
}
return m_database.lastChanges() > 0;
}
void LocalStorageDatabase::flushToDisk()
{
if (m_transaction)
m_transaction->commit();
}
void LocalStorageDatabase::close()
{
ASSERT(!RunLoop::isMain());
if (m_isClosed)
return;
m_isClosed = true;
bool isEmpty = databaseIsEmpty();
m_clearStatement = nullptr;
m_insertStatement = nullptr;
m_getItemStatement = nullptr;
m_getItemsStatement = nullptr;
m_deleteItemStatement = nullptr;
m_items = std::nullopt;
if (auto transaction = std::exchange(m_transaction, nullptr))
transaction->commit();
if (m_database.isOpen())
m_database.close();
if (isEmpty)
SQLiteFileSystem::deleteDatabaseFile(m_databasePath);
}
bool LocalStorageDatabase::databaseIsEmpty() const
{
ASSERT(!RunLoop::isMain());
if (!m_database.isOpen())
return false;
if (m_items)
return m_items->isEmpty();
auto query = m_database.prepareStatement("SELECT COUNT(*) FROM ItemTable"_s);
if (!query) {
LOG_ERROR("Unable to count number of rows in ItemTable for local storage");
return false;
}
if (query->step() != SQLITE_ROW) {
LOG_ERROR("No results when counting number of rows in ItemTable for local storage");
return false;
}
return !query->columnInt(0);
}
void LocalStorageDatabase::openIfExisting()
{
ASSERT(!RunLoop::isMain());
if (m_database.isOpen())
return;
openDatabase(ShouldCreateDatabase::No);
// Prewarm the getItems statement for performance since the pages block synchronously on retrieving the items.
if (m_database.isOpen())
scopedStatement(m_getItemsStatement, getItemsQueryString);
}
SQLiteStatementAutoResetScope LocalStorageDatabase::scopedStatement(std::unique_ptr<SQLiteStatement>& statement, ASCIILiteral query) const
{
ASSERT(!RunLoop::isMain());
if (!statement) {
auto statementOrError = m_database.prepareHeapStatement(query);
if (!statementOrError)
return SQLiteStatementAutoResetScope { };
statement = statementOrError.value().moveToUniquePtr();
ASSERT(m_database.isOpen());
}
return SQLiteStatementAutoResetScope { statement.get() };
}
void LocalStorageDatabase::handleLowMemoryWarning()
{
if (m_database.isOpen())
m_database.releaseMemory();
}
} // namespace WebKit