summaryrefslogtreecommitdiffstats
path: root/WebCore/storage/Database.cpp
diff options
context:
space:
mode:
authorUpstream <upstream-import@none>1970-01-12 13:46:40 +0000
committerUpstream <upstream-import@none>1970-01-12 13:46:40 +0000
commitd8543bb6618c17b12da906afa77d216f58cf4058 (patch)
treec58dc05ed86825bd0ef8d305d58c8205106b540f /WebCore/storage/Database.cpp
downloadexternal_webkit-d8543bb6618c17b12da906afa77d216f58cf4058.zip
external_webkit-d8543bb6618c17b12da906afa77d216f58cf4058.tar.gz
external_webkit-d8543bb6618c17b12da906afa77d216f58cf4058.tar.bz2
external/webkit r30707
Diffstat (limited to 'WebCore/storage/Database.cpp')
-rw-r--r--WebCore/storage/Database.cpp581
1 files changed, 581 insertions, 0 deletions
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<int, String>& guidToVersionMap()
+{
+ static HashMap<int, String> map;
+ return map;
+}
+
+static HashMap<int, HashSet<Database*>*>& guidToDatabaseMap()
+{
+ static HashMap<int, HashSet<Database*>*> 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> 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> 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<Database*>* hashSet = guidToDatabaseMap().get(m_guid);
+ if (!hashSet) {
+ hashSet = new HashSet<Database*>;
+ 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<Database*>* 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<DatabaseOpenTask> 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<DatabaseCloseTask> 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<OriginQuotaManager> 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<String, int> 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<SQLTransactionCallback> callback, PassRefPtr<SQLTransactionErrorCallback> errorCallback,
+ PassRefPtr<VoidCallback> 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<SQLTransactionCallback> callback, PassRefPtr<SQLTransactionErrorCallback> errorCallback,
+ PassRefPtr<VoidCallback> 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<SQLTransaction> 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<String> 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<String>();
+ }
+
+ Vector<String> 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<String>();
+ }
+
+ 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<SQLTransaction*>(context);
+ transaction->performPendingCallback();
+ transaction->deref(); // Was ref'd in scheduleTransactionCallback().
+}
+
+Vector<String> Database::tableNames()
+{
+ RefPtr<DatabaseTableNamesTask> 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<SecurityOrigin> Database::securityOriginCopy() const
+{
+ return m_securityOrigin->copy();
+}
+
+String Database::stringIdentifier() const
+{
+ // Return a deep copy for ref counting thread safety
+ return m_name.copy();
+}
+
+}