LocalStorageDatabase.cpp   [plain text]


/*
 * Copyright (C) 2008, 2009, 2010, 2013, 2019 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 <WebCore/SQLiteStatement.h>
#include <WebCore/SQLiteTransaction.h>
#include <WebCore/SecurityOrigin.h>
#include <WebCore/StorageMap.h>
#include <WebCore/SuddenTermination.h>
#include <wtf/FileSystem.h>
#include <wtf/RefPtr.h>
#include <wtf/RunLoop.h>
#include <wtf/WorkQueue.h>
#include <wtf/text/StringHash.h>
#include <wtf/text/WTFString.h>

static const auto databaseUpdateInterval = 1_s;

static const int maximumItemsToUpdate = 100;

namespace WebKit {
using namespace WebCore;

Ref<LocalStorageDatabase> LocalStorageDatabase::create(Ref<WorkQueue>&& queue, Ref<LocalStorageDatabaseTracker>&& tracker, const SecurityOriginData& securityOrigin)
{
    return adoptRef(*new LocalStorageDatabase(WTFMove(queue), WTFMove(tracker), securityOrigin));
}

LocalStorageDatabase::LocalStorageDatabase(Ref<WorkQueue>&& queue, Ref<LocalStorageDatabaseTracker>&& tracker, const SecurityOriginData& securityOrigin)
    : m_queue(WTFMove(queue))
    , m_tracker(WTFMove(tracker))
    , m_securityOrigin(securityOrigin)
    , m_databasePath(m_tracker->databasePath(m_securityOrigin))
{
    ASSERT(!RunLoop::isMain());
}

LocalStorageDatabase::~LocalStorageDatabase()
{
    ASSERT(!RunLoop::isMain());
    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)
{
    ASSERT(!RunLoop::isMain());
    if (!FileSystem::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"_str);
    if (query.prepare() != SQLITE_OK) {
        LOG_ERROR("Unable to select items from ItemTable for local storage");
        return;
    }

    HashMap<String, String> 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.add(WTFMove(key), WTFMove(value));
        result = query.step();
    }

    if (result != SQLITE_DONE) {
        LOG_ERROR("Error reading items from ItemTable for local storage");
        return;
    }

    storageMap.importItems(WTFMove(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 = makeUnique<SuddenTerminationDisabler>();

    m_didScheduleDatabaseUpdate = true;

    m_queue->dispatch([protectedThis = makeRef(*this)] {
        protectedThis->updateDatabase();
    });
}

void LocalStorageDatabase::updateDatabase()
{
    if (m_isClosed)
        return;

    m_didScheduleDatabaseUpdate = false;

    HashMap<String, String> 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<String, String>& 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