From d8543bb6618c17b12da906afa77d216f58cf4058 Mon Sep 17 00:00:00 2001 From: Upstream Date: Mon, 12 Jan 1970 13:46:40 +0000 Subject: external/webkit r30707 --- WebCore/storage/Database.cpp | 581 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 WebCore/storage/Database.cpp (limited to 'WebCore/storage/Database.cpp') diff --git a/WebCore/storage/Database.cpp b/WebCore/storage/Database.cpp new file mode 100644 index 0000000..927d199 --- /dev/null +++ b/WebCore/storage/Database.cpp @@ -0,0 +1,581 @@ +/* + * Copyright (C) 2007, 2008 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "Database.h" + +#include "ChangeVersionWrapper.h" +#include "CString.h" +#include "DatabaseAuthorizer.h" +#include "DatabaseTask.h" +#include "DatabaseThread.h" +#include "DatabaseTracker.h" +#include "Document.h" +#include "ExceptionCode.h" +#include "FileSystem.h" +#include "Frame.h" +#include "InspectorController.h" +#include "Logging.h" +#include "MainThread.h" +#include "NotImplemented.h" +#include "Page.h" +#include "OriginQuotaManager.h" +#include "SQLiteDatabase.h" +#include "SQLiteStatement.h" +#include "SQLResultSet.h" + +namespace WebCore { + +static Mutex& guidMutex() +{ + // FIXME: this is not a thread-safe way to initialize a shared global. + static Mutex mutex; + return mutex; +} + +static HashMap& guidToVersionMap() +{ + static HashMap map; + return map; +} + +static HashMap*>& guidToDatabaseMap() +{ + static HashMap*> map; + return map; +} + +const String& Database::databaseInfoTableName() +{ + static String name = "__WebKitDatabaseInfoTable__"; + return name; +} + +static const String& databaseVersionKey() +{ + static String key = "WebKitDatabaseVersionKey"; + return key; +} + +static int guidForOriginAndName(const String& origin, const String& name); + +PassRefPtr Database::openDatabase(Document* document, const String& name, const String& expectedVersion, const String& displayName, unsigned long estimatedSize, ExceptionCode& e) +{ + if (!DatabaseTracker::tracker().canEstablishDatabase(document, name, displayName, estimatedSize)) { + // FIXME: There should be an exception raised here in addition to returning a null Database object. The question has been raised with the WHATWG. + LOG(StorageAPI, "Database %s for origin %s not allowed to be established", name.ascii().data(), document->securityOrigin()->toString().ascii().data()); + return 0; + } + + RefPtr database = new Database(document, name, expectedVersion); + + if (!database->openAndVerifyVersion(e)) { + LOG(StorageAPI, "Failed to open and verify version (expected %s) of database %s", expectedVersion.ascii().data(), database->databaseDebugName().ascii().data()); + return 0; + } + + DatabaseTracker::tracker().setDatabaseDetails(document->securityOrigin(), name, displayName, estimatedSize); + + document->setHasOpenDatabases(); + + if (Page* page = document->frame()->page()) + page->inspectorController()->didOpenDatabase(database.get(), document->domain(), name, expectedVersion); + + return database; +} + +Database::Database(Document* document, const String& name, const String& expectedVersion) + : m_transactionInProgress(false) + , m_document(document) + , m_name(name.copy()) + , m_guid(0) + , m_expectedVersion(expectedVersion) + , m_deleted(false) + , m_stopped(false) +{ + ASSERT(document); + m_securityOrigin = document->securityOrigin(); + + if (m_name.isNull()) + m_name = ""; + + initializeThreadingAndMainThread(); + + m_guid = guidForOriginAndName(m_securityOrigin->toString(), name); + + { + MutexLocker locker(guidMutex()); + + HashSet* hashSet = guidToDatabaseMap().get(m_guid); + if (!hashSet) { + hashSet = new HashSet; + guidToDatabaseMap().set(m_guid, hashSet); + } + + hashSet->add(this); + } + + ASSERT(m_document->databaseThread()); + + m_filename = DatabaseTracker::tracker().fullPathForDatabase(m_securityOrigin.get(), m_name); + + DatabaseTracker::tracker().addOpenDatabase(this); + m_document->addOpenDatabase(this); +} + +Database::~Database() +{ + { + MutexLocker locker(guidMutex()); + + HashSet* hashSet = guidToDatabaseMap().get(m_guid); + ASSERT(hashSet); + ASSERT(hashSet->contains(this)); + hashSet->remove(this); + if (hashSet->isEmpty()) { + guidToDatabaseMap().remove(m_guid); + delete hashSet; + guidToVersionMap().remove(m_guid); + } + } + + if (m_document->databaseThread()) + m_document->databaseThread()->unscheduleDatabaseTasks(this); + + DatabaseTracker::tracker().removeOpenDatabase(this); + m_document->removeOpenDatabase(this); +} + +bool Database::openAndVerifyVersion(ExceptionCode& e) +{ + m_databaseAuthorizer = new DatabaseAuthorizer(); + + RefPtr task = new DatabaseOpenTask(this); + + task->lockForSynchronousScheduling(); + m_document->databaseThread()->scheduleImmediateTask(task.get()); + task->waitForSynchronousCompletion(); + + ASSERT(task->isComplete()); + e = task->exceptionCode(); + return task->openSuccessful(); +} + + +static bool retrieveTextResultFromDatabase(SQLiteDatabase& db, const String& query, String& resultString) +{ + SQLiteStatement statement(db, query); + int result = statement.prepare(); + + if (result != SQLResultOk) { + LOG_ERROR("Error (%i) preparing statement to read text result from database (%s)", result, query.ascii().data()); + return false; + } + + result = statement.step(); + if (result == SQLResultRow) { + resultString = statement.getColumnText(0); + return true; + } else if (result == SQLResultDone) { + resultString = String(); + return true; + } else { + LOG_ERROR("Error (%i) reading text result from database (%s)", result, query.ascii().data()); + return false; + } +} + +bool Database::getVersionFromDatabase(String& version) +{ + static String getVersionQuery = "SELECT value FROM " + databaseInfoTableName() + " WHERE key = '" + databaseVersionKey() + "';"; + + m_databaseAuthorizer->disable(); + + bool result = retrieveTextResultFromDatabase(m_sqliteDatabase, getVersionQuery.copy(), version); + if (!result) + LOG_ERROR("Failed to retrieve version from database %s", databaseDebugName().ascii().data()); + + m_databaseAuthorizer->enable(); + + return result; +} + +static bool setTextValueInDatabase(SQLiteDatabase& db, const String& query, const String& value) +{ + SQLiteStatement statement(db, query); + int result = statement.prepare(); + + if (result != SQLResultOk) { + LOG_ERROR("Failed to prepare statement to set value in database (%s)", query.ascii().data()); + return false; + } + + statement.bindText(1, value); + + result = statement.step(); + if (result != SQLResultDone) { + LOG_ERROR("Failed to step statement to set value in database (%s)", query.ascii().data()); + return false; + } + + return true; +} + +bool Database::setVersionInDatabase(const String& version) +{ + static String setVersionQuery = "INSERT INTO " + databaseInfoTableName() + " (key, value) VALUES ('" + databaseVersionKey() + "', ?);"; + + m_databaseAuthorizer->disable(); + + bool result = setTextValueInDatabase(m_sqliteDatabase, setVersionQuery.copy(), version); + if (!result) + LOG_ERROR("Failed to set version %s in database (%s)", version.ascii().data(), setVersionQuery.ascii().data()); + + m_databaseAuthorizer->enable(); + + return result; +} + +bool Database::versionMatchesExpected() const +{ + if (!m_expectedVersion.isEmpty()) { + MutexLocker locker(guidMutex()); + return m_expectedVersion == guidToVersionMap().get(m_guid); + } + + return true; +} + +void Database::markAsDeletedAndClose() +{ + if (m_deleted) + return; + + LOG(StorageAPI, "Marking %s (%p) as deleted", stringIdentifier().ascii().data(), this); + m_deleted = true; + + if (m_document->databaseThread()->terminationRequested()) { + LOG(StorageAPI, "Database handle %p is on a terminated DatabaseThread, cannot be marked for normal closure\n", this); + return; + } + + document()->databaseThread()->unscheduleDatabaseTasks(this); + + RefPtr task = new DatabaseCloseTask(this); + + task->lockForSynchronousScheduling(); + m_document->databaseThread()->scheduleImmediateTask(task.get()); + task->waitForSynchronousCompletion(); +} + +void Database::close() +{ + m_sqliteDatabase.close(); +} + +void Database::stop() +{ + // FIXME: The net effect of the following code is to remove all pending transactions and statements, but allow the current statement + // to run to completion. In the future we can use the sqlite3_progress_handler or sqlite3_interrupt interfaces to cancel the current + // statement in response to close(), as well. + + // This method is meant to be used as an analog to cancelling a loader, and is used when a document is shut down as the result of + // a page load or closing the page + m_stopped = true; + + { + MutexLocker locker(m_transactionInProgressMutex); + m_transactionQueue.kill(); + m_transactionInProgress = false; + } +} + +unsigned long long Database::databaseSize() const +{ + long long size; + if (!getFileSize(m_filename, size)) + size = 0; + return size; +} + +unsigned long long Database::maximumSize() const +{ + // The maximum size for this database is the full quota for this origin, minus the current usage within this origin, + // except for the current usage of this database + + OriginQuotaManager& manager(DatabaseTracker::tracker().originQuotaManager()); + Locker locker(manager); + + return DatabaseTracker::tracker().quotaForOrigin(m_securityOrigin.get()) - manager.diskUsage(m_securityOrigin.get()) + databaseSize(); +} + +void Database::disableAuthorizer() +{ + ASSERT(m_databaseAuthorizer); + m_databaseAuthorizer->disable(); +} + +void Database::enableAuthorizer() +{ + ASSERT(m_databaseAuthorizer); + m_databaseAuthorizer->enable(); +} + +static int guidForOriginAndName(const String& origin, const String& name) +{ + static int currentNewGUID = 1; + static Mutex stringIdentifierMutex; + static HashMap stringIdentifierToGUIDMap; + + String stringID; + if (origin.endsWith("/")) + stringID = origin + name; + else + stringID = origin + "/" + name; + + MutexLocker locker(stringIdentifierMutex); + int guid = stringIdentifierToGUIDMap.get(stringID); + if (!guid) { + guid = currentNewGUID++; + stringIdentifierToGUIDMap.set(stringID, guid); + } + + return guid; +} + +void Database::resetAuthorizer() +{ + if (m_databaseAuthorizer) + m_databaseAuthorizer->reset(); +} + +void Database::performPolicyChecks() +{ + // FIXME: Code similar to the following will need to be run to enforce the per-origin size limit the spec mandates. + // Additionally, we might need a way to pause the database thread while the UA prompts the user for permission to + // increase the size limit + + /* + if (m_databaseAuthorizer->lastActionIncreasedSize()) + DatabaseTracker::scheduleFileSizeCheckOnMainThread(this); + */ + + notImplemented(); +} + +bool Database::performOpenAndVerify(ExceptionCode& e) +{ + if (!m_sqliteDatabase.open(m_filename)) { + LOG_ERROR("Unable to open database at path %s", m_filename.ascii().data()); + e = INVALID_STATE_ERR; + return false; + } + + ASSERT(m_databaseAuthorizer); + m_sqliteDatabase.setAuthorizer(m_databaseAuthorizer); + + if (!m_sqliteDatabase.tableExists(databaseInfoTableName())) { + if (!m_sqliteDatabase.executeCommand("CREATE TABLE " + databaseInfoTableName() + " (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) { + LOG_ERROR("Unable to create table %s in database %s", databaseInfoTableName().ascii().data(), databaseDebugName().ascii().data()); + e = INVALID_STATE_ERR; + return false; + } + } + + + String currentVersion; + { + MutexLocker locker(guidMutex()); + currentVersion = guidToVersionMap().get(m_guid); + + if (currentVersion.isNull()) + LOG(StorageAPI, "Current cached version for guid %i is null", m_guid); + else + LOG(StorageAPI, "Current cached version for guid %i is %s", m_guid, currentVersion.ascii().data()); + + if (currentVersion.isNull()) { + if (!getVersionFromDatabase(currentVersion)) { + LOG_ERROR("Failed to get current version from database %s", databaseDebugName().ascii().data()); + e = INVALID_STATE_ERR; + return false; + } + if (currentVersion.length()) { + LOG(StorageAPI, "Retrieved current version %s from database %s", currentVersion.ascii().data(), databaseDebugName().ascii().data()); + } else { + LOG(StorageAPI, "Setting version %s in database %s that was just created", m_expectedVersion.ascii().data(), databaseDebugName().ascii().data()); + if (!setVersionInDatabase(m_expectedVersion)) { + LOG_ERROR("Failed to set version %s in database %s", m_expectedVersion.ascii().data(), databaseDebugName().ascii().data()); + e = INVALID_STATE_ERR; + return false; + } + + currentVersion = m_expectedVersion; + } + + guidToVersionMap().set(m_guid, currentVersion.copy()); + } + } + + if (currentVersion.isNull()) { + LOG(StorageAPI, "Database %s does not have its version set", databaseDebugName().ascii().data()); + currentVersion = ""; + } + + // FIXME: For now, the spec says that if the database has no version, it is valid for any "Expected version" string. That seems silly and I think it should be + // changed, and here's where we would change it + if (m_expectedVersion.length()) { + if (currentVersion.length() && m_expectedVersion != currentVersion) { + LOG(StorageAPI, "page expects version %s from database %s, which actually has version name %s - openDatabase() call will fail", m_expectedVersion.ascii().data(), + databaseDebugName().ascii().data(), currentVersion.ascii().data()); + e = INVALID_STATE_ERR; + return false; + } + } + + return true; +} + +void Database::changeVersion(const String& oldVersion, const String& newVersion, + PassRefPtr callback, PassRefPtr errorCallback, + PassRefPtr successCallback) +{ + m_transactionQueue.append(new SQLTransaction(this, callback, errorCallback, successCallback, new ChangeVersionWrapper(oldVersion, newVersion))); + MutexLocker locker(m_transactionInProgressMutex); + if (!m_transactionInProgress) + scheduleTransaction(); +} + +void Database::transaction(PassRefPtr callback, PassRefPtr errorCallback, + PassRefPtr successCallback) +{ + m_transactionQueue.append(new SQLTransaction(this, callback, errorCallback, successCallback, 0)); + MutexLocker locker(m_transactionInProgressMutex); + if (!m_transactionInProgress) + scheduleTransaction(); +} + +void Database::scheduleTransaction() +{ + ASSERT(!m_transactionInProgressMutex.tryLock()); // Locked by caller. + RefPtr transaction; + if (m_transactionQueue.tryGetMessage(transaction) && m_document->databaseThread()) { + DatabaseTransactionTask* task = new DatabaseTransactionTask(transaction); + LOG(StorageAPI, "Scheduling DatabaseTransactionTask %p for transaction %p\n", task, task->transaction()); + m_transactionInProgress = true; + m_document->databaseThread()->scheduleTask(task); + } else + m_transactionInProgress = false; +} + +void Database::scheduleTransactionStep(SQLTransaction* transaction) +{ + if (m_document->databaseThread()) { + DatabaseTransactionTask* task = new DatabaseTransactionTask(transaction); + LOG(StorageAPI, "Scheduling DatabaseTransactionTask %p for the transaction step\n", task); + m_document->databaseThread()->scheduleTask(task); + } +} + +void Database::scheduleTransactionCallback(SQLTransaction* transaction) +{ + transaction->ref(); + callOnMainThread(deliverPendingCallback, transaction); +} + +Vector Database::performGetTableNames() +{ + disableAuthorizer(); + + SQLiteStatement statement(m_sqliteDatabase, "SELECT name FROM sqlite_master WHERE type='table';"); + if (statement.prepare() != SQLResultOk) { + LOG_ERROR("Unable to retrieve list of tables for database %s", databaseDebugName().ascii().data()); + enableAuthorizer(); + return Vector(); + } + + Vector tableNames; + int result; + while ((result = statement.step()) == SQLResultRow) { + String name = statement.getColumnText(0); + if (name != databaseInfoTableName()) + tableNames.append(name); + } + + enableAuthorizer(); + + if (result != SQLResultDone) { + LOG_ERROR("Error getting tables for database %s", databaseDebugName().ascii().data()); + return Vector(); + } + + return tableNames; +} + +String Database::version() const +{ + if (m_deleted) + return String(); + MutexLocker locker(guidMutex()); + return guidToVersionMap().get(m_guid).copy(); +} + +void Database::deliverPendingCallback(void* context) +{ + SQLTransaction* transaction = static_cast(context); + transaction->performPendingCallback(); + transaction->deref(); // Was ref'd in scheduleTransactionCallback(). +} + +Vector Database::tableNames() +{ + RefPtr task = new DatabaseTableNamesTask(this); + + task->lockForSynchronousScheduling(); + m_document->databaseThread()->scheduleImmediateTask(task.get()); + task->waitForSynchronousCompletion(); + + return task->tableNames(); +} + +void Database::setExpectedVersion(const String& version) +{ + m_expectedVersion = version.copy(); +} + +PassRefPtr Database::securityOriginCopy() const +{ + return m_securityOrigin->copy(); +} + +String Database::stringIdentifier() const +{ + // Return a deep copy for ref counting thread safety + return m_name.copy(); +} + +} -- cgit v1.1