/* * 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 "SQLTransaction.h" #include "ChromeClient.h" #include "Database.h" #include "DatabaseAuthorizer.h" #include "DatabaseDetails.h" #include "DatabaseTracker.h" #include "Document.h" #include "ExceptionCode.h" #include "Logging.h" #include "OriginQuotaManager.h" #include "Page.h" #include "PlatformString.h" #include "SecurityOrigin.h" #include "SQLError.h" #include "SQLiteTransaction.h" #include "SQLResultSet.h" #include "SQLStatement.h" #include "SQLStatementCallback.h" #include "SQLStatementErrorCallback.h" #include "SQLValue.h" // There's no way of knowing exactly how much more space will be required when a statement hits the quota limit. // For now, we'll arbitrarily choose currentQuota + 1mb. // In the future we decide to track if a size increase wasn't enough, and ask for larger-and-larger increases until its enough. static const int DefaultQuotaSizeIncrease = 1048576; namespace WebCore { PassRefPtr SQLTransaction::create(Database* db, PassRefPtr callback, PassRefPtr errorCallback, PassRefPtr successCallback, PassRefPtr wrapper) { return adoptRef(new SQLTransaction(db, callback, errorCallback, successCallback, wrapper)); } SQLTransaction::SQLTransaction(Database* db, PassRefPtr callback, PassRefPtr errorCallback, PassRefPtr successCallback, PassRefPtr wrapper) : m_nextStep(&SQLTransaction::openTransactionAndPreflight) , m_executeSqlAllowed(false) , m_database(db) , m_wrapper(wrapper) , m_callback(callback) , m_successCallback(successCallback) , m_errorCallback(errorCallback) , m_shouldRetryCurrentStatement(false) , m_shouldCommitAfterErrorCallback(true) , m_modifiedDatabase(false) { ASSERT(m_database); } SQLTransaction::~SQLTransaction() { } void SQLTransaction::executeSQL(const String& sqlStatement, const Vector& arguments, PassRefPtr callback, PassRefPtr callbackError, ExceptionCode& e) { if (!m_executeSqlAllowed || m_database->stopped()) { e = INVALID_STATE_ERR; return; } RefPtr statement = SQLStatement::create(sqlStatement.copy(), arguments, callback, callbackError); if (m_database->deleted()) statement->setDatabaseDeletedError(); if (!m_database->versionMatchesExpected()) statement->setVersionMismatchedError(); enqueueStatement(statement); } void SQLTransaction::enqueueStatement(PassRefPtr statement) { MutexLocker locker(m_statementMutex); m_statementQueue.append(statement); } #ifndef NDEBUG const char* SQLTransaction::debugStepName(SQLTransaction::TransactionStepMethod step) { if (step == &SQLTransaction::openTransactionAndPreflight) return "openTransactionAndPreflight"; else if (step == &SQLTransaction::runStatements) return "runStatements"; else if (step == &SQLTransaction::postflightAndCommit) return "postflightAndCommit"; else if (step == &SQLTransaction::cleanupAfterTransactionErrorCallback) return "cleanupAfterTransactionErrorCallback"; else if (step == &SQLTransaction::deliverTransactionCallback) return "deliverTransactionCallback"; else if (step == &SQLTransaction::deliverTransactionErrorCallback) return "deliverTransactionErrorCallback"; else if (step == &SQLTransaction::deliverStatementCallback) return "deliverStatementCallback"; else if (step == &SQLTransaction::deliverQuotaIncreaseCallback) return "deliverQuotaIncreaseCallback"; else if (step == &SQLTransaction::deliverSuccessCallback) return "deliverSuccessCallback"; else if (step == &SQLTransaction::cleanupAfterSuccessCallback) return "cleanupAfterSuccessCallback"; else return "UNKNOWN"; } #endif void SQLTransaction::checkAndHandleClosedDatabase() { if (!m_database->stopped()) return; // If the database was stopped, don't do anything and cancel queued work LOG(StorageAPI, "Database was stopped - cancelling work for this transaction"); MutexLocker locker(m_statementMutex); m_statementQueue.clear(); m_nextStep = 0; // The current SQLite transaction should be stopped, as well if (m_sqliteTransaction) { m_sqliteTransaction->stop(); m_sqliteTransaction.clear(); } } bool SQLTransaction::performNextStep() { LOG(StorageAPI, "Step %s\n", debugStepName(m_nextStep)); ASSERT(m_nextStep == &SQLTransaction::openTransactionAndPreflight || m_nextStep == &SQLTransaction::runStatements || m_nextStep == &SQLTransaction::postflightAndCommit || m_nextStep == &SQLTransaction::cleanupAfterSuccessCallback || m_nextStep == &SQLTransaction::cleanupAfterTransactionErrorCallback); checkAndHandleClosedDatabase(); if (m_nextStep) (this->*m_nextStep)(); // If there is no nextStep after performing the above step, the transaction is complete return !m_nextStep; } void SQLTransaction::performPendingCallback() { LOG(StorageAPI, "Callback %s\n", debugStepName(m_nextStep)); ASSERT(m_nextStep == &SQLTransaction::deliverTransactionCallback || m_nextStep == &SQLTransaction::deliverTransactionErrorCallback || m_nextStep == &SQLTransaction::deliverStatementCallback || m_nextStep == &SQLTransaction::deliverQuotaIncreaseCallback || m_nextStep == &SQLTransaction::deliverSuccessCallback); checkAndHandleClosedDatabase(); if (m_nextStep) (this->*m_nextStep)(); } void SQLTransaction::openTransactionAndPreflight() { ASSERT(!m_database->m_sqliteDatabase.transactionInProgress()); LOG(StorageAPI, "Opening and preflighting transaction %p", this); // If the database was deleted, jump to the error callback if (m_database->deleted()) { m_transactionError = SQLError::create(0, "unable to open a transaction, because the user deleted the database"); handleTransactionError(false); return; } // Set the maximum usage for this transaction m_database->m_sqliteDatabase.setMaximumSize(m_database->maximumSize()); ASSERT(!m_sqliteTransaction); m_sqliteTransaction.set(new SQLiteTransaction(m_database->m_sqliteDatabase)); m_database->m_databaseAuthorizer->disable(); m_sqliteTransaction->begin(); m_database->m_databaseAuthorizer->enable(); // Transaction Steps 1+2 - Open a transaction to the database, jumping to the error callback if that fails if (!m_sqliteTransaction->inProgress()) { ASSERT(!m_database->m_sqliteDatabase.transactionInProgress()); m_sqliteTransaction.clear(); m_transactionError = SQLError::create(0, "unable to open a transaction to the database"); handleTransactionError(false); return; } // Transaction Steps 3 - Peform preflight steps, jumping to the error callback if they fail if (m_wrapper && !m_wrapper->performPreflight(this)) { ASSERT(!m_database->m_sqliteDatabase.transactionInProgress()); m_sqliteTransaction.clear(); m_transactionError = m_wrapper->sqlError(); if (!m_transactionError) m_transactionError = SQLError::create(0, "unknown error occured setting up transaction"); handleTransactionError(false); return; } // Transaction Step 4 - Invoke the transaction callback with the new SQLTransaction object m_nextStep = &SQLTransaction::deliverTransactionCallback; LOG(StorageAPI, "Scheduling deliverTransactionCallback for transaction %p\n", this); m_database->scheduleTransactionCallback(this); } void SQLTransaction::deliverTransactionCallback() { bool shouldDeliverErrorCallback = false; if (m_callback) { m_executeSqlAllowed = true; m_callback->handleEvent(this, shouldDeliverErrorCallback); m_executeSqlAllowed = false; } else shouldDeliverErrorCallback = true; // Transaction Step 5 - If the transaction callback was null or raised an exception, jump to the error callback if (shouldDeliverErrorCallback) { m_transactionError = SQLError::create(0, "the SQLTransactionCallback was null or threw an exception"); deliverTransactionErrorCallback(); } else scheduleToRunStatements(); } void SQLTransaction::scheduleToRunStatements() { m_nextStep = &SQLTransaction::runStatements; LOG(StorageAPI, "Scheduling runStatements for transaction %p\n", this); m_database->scheduleTransactionStep(this); } void SQLTransaction::runStatements() { // If there is a series of statements queued up that are all successful and have no associated // SQLStatementCallback objects, then we can burn through the queue do { if (m_shouldRetryCurrentStatement) { m_shouldRetryCurrentStatement = false; // FIXME - Another place that needs fixing up after is addressed. // See ::openTransactionAndPreflight() for discussion // Reset the maximum size here, as it was increased to allow us to retry this statement m_database->m_sqliteDatabase.setMaximumSize(m_database->maximumSize()); } else { // If the current statement has already been run, failed due to quota constraints, and we're not retrying it, // that means it ended in an error. Handle it now if (m_currentStatement && m_currentStatement->lastExecutionFailedDueToQuota()) { handleCurrentStatementError(); break; } // Otherwise, advance to the next statement getNextStatement(); } } while (runCurrentStatement()); // If runCurrentStatement() returned false, that means either there was no current statement to run, // or the current statement requires a callback to complete. In the later case, it also scheduled // the callback or performed any other additional work so we can return if (!m_currentStatement) postflightAndCommit(); } void SQLTransaction::getNextStatement() { m_currentStatement = 0; MutexLocker locker(m_statementMutex); if (!m_statementQueue.isEmpty()) { m_currentStatement = m_statementQueue.first(); m_statementQueue.removeFirst(); } } bool SQLTransaction::runCurrentStatement() { if (!m_currentStatement) return false; m_database->m_databaseAuthorizer->reset(); if (m_currentStatement->execute(m_database.get())) { if (m_database->m_databaseAuthorizer->lastActionChangedDatabase()) { // Flag this transaction as having changed the database for later delegate notification m_modifiedDatabase = true; // Also dirty the size of this database file for calculating quota usage OriginQuotaManager& manager(DatabaseTracker::tracker().originQuotaManager()); Locker locker(manager); manager.markDatabase(m_database.get()); } if (m_currentStatement->hasStatementCallback()) { m_nextStep = &SQLTransaction::deliverStatementCallback; LOG(StorageAPI, "Scheduling deliverStatementCallback for transaction %p\n", this); m_database->scheduleTransactionCallback(this); return false; } return true; } if (m_currentStatement->lastExecutionFailedDueToQuota()) { m_nextStep = &SQLTransaction::deliverQuotaIncreaseCallback; LOG(StorageAPI, "Scheduling deliverQuotaIncreaseCallback for transaction %p\n", this); m_database->scheduleTransactionCallback(this); return false; } handleCurrentStatementError(); return false; } void SQLTransaction::handleCurrentStatementError() { // Transaction Steps 6.error - Call the statement's error callback, but if there was no error callback, // jump to the transaction error callback if (m_currentStatement->hasStatementErrorCallback()) { m_nextStep = &SQLTransaction::deliverStatementCallback; LOG(StorageAPI, "Scheduling deliverStatementCallback for transaction %p\n", this); m_database->scheduleTransactionCallback(this); } else { m_transactionError = m_currentStatement->sqlError(); if (!m_transactionError) m_transactionError = SQLError::create(1, "the statement failed to execute"); handleTransactionError(false); } } void SQLTransaction::deliverStatementCallback() { ASSERT(m_currentStatement); // Transaction Step 6.6 and 6.3(error) - If the statement callback went wrong, jump to the transaction error callback // Otherwise, continue to loop through the statement queue m_executeSqlAllowed = true; bool result = m_currentStatement->performCallback(this); m_executeSqlAllowed = false; if (result) { m_transactionError = SQLError::create(0, "the statement callback raised an exception or statement error callback did not return false"); handleTransactionError(true); } else scheduleToRunStatements(); } void SQLTransaction::deliverQuotaIncreaseCallback() { ASSERT(m_currentStatement); ASSERT(!m_shouldRetryCurrentStatement); Page* page = m_database->document()->page(); ASSERT(page); RefPtr origin = m_database->securityOriginCopy(); unsigned long long currentQuota = DatabaseTracker::tracker().quotaForOrigin(origin.get()); page->chrome()->client()->exceededDatabaseQuota(m_database->document()->frame(), m_database->stringIdentifier()); unsigned long long newQuota = DatabaseTracker::tracker().quotaForOrigin(origin.get()); // If the new quota ended up being larger than the old quota, we will retry the statement. if (newQuota > currentQuota) m_shouldRetryCurrentStatement = true; m_nextStep = &SQLTransaction::runStatements; LOG(StorageAPI, "Scheduling runStatements for transaction %p\n", this); m_database->scheduleTransactionStep(this); } void SQLTransaction::postflightAndCommit() { // Transaction Step 7 - Peform postflight steps, jumping to the error callback if they fail if (m_wrapper && !m_wrapper->performPostflight(this)) { m_transactionError = m_wrapper->sqlError(); if (!m_transactionError) m_transactionError = SQLError::create(0, "unknown error occured setting up transaction"); handleTransactionError(false); return; } // Transacton Step 8+9 - Commit the transaction, jumping to the error callback if that fails ASSERT(m_sqliteTransaction); m_database->m_databaseAuthorizer->disable(); m_sqliteTransaction->commit(); m_database->m_databaseAuthorizer->enable(); // If the commit failed, the transaction will still be marked as "in progress" if (m_sqliteTransaction->inProgress()) { m_shouldCommitAfterErrorCallback = false; m_transactionError = SQLError::create(0, "failed to commit the transaction"); handleTransactionError(false); return; } // The commit was successful, notify the delegates if the transaction modified this database if (m_modifiedDatabase) DatabaseTracker::tracker().scheduleNotifyDatabaseChanged(m_database->m_securityOrigin.get(), m_database->m_name); // Now release our unneeded callbacks, to break reference cycles. m_callback = 0; m_errorCallback = 0; // Transaction Step 10 - Deliver success callback, if there is one if (m_successCallback) { m_nextStep = &SQLTransaction::deliverSuccessCallback; LOG(StorageAPI, "Scheduling deliverSuccessCallback for transaction %p\n", this); m_database->scheduleTransactionCallback(this); } else cleanupAfterSuccessCallback(); } void SQLTransaction::deliverSuccessCallback() { // Transaction Step 10 - Deliver success callback ASSERT(m_successCallback); m_successCallback->handleEvent(); // Release the last callback to break reference cycle m_successCallback = 0; // Schedule a "post-success callback" step to return control to the database thread in case there // are further transactions queued up for this Database m_nextStep = &SQLTransaction::cleanupAfterSuccessCallback; LOG(StorageAPI, "Scheduling cleanupAfterSuccessCallback for transaction %p\n", this); m_database->scheduleTransactionStep(this); } void SQLTransaction::cleanupAfterSuccessCallback() { // Transaction Step 11 - End transaction steps // There is no next step LOG(StorageAPI, "Transaction %p is complete\n", this); ASSERT(!m_database->m_sqliteDatabase.transactionInProgress()); m_nextStep = 0; } void SQLTransaction::handleTransactionError(bool inCallback) { if (m_errorCallback) { if (inCallback) deliverTransactionErrorCallback(); else { m_nextStep = &SQLTransaction::deliverTransactionErrorCallback; LOG(StorageAPI, "Scheduling deliverTransactionErrorCallback for transaction %p\n", this); m_database->scheduleTransactionCallback(this); } return; } // Transaction Step 12 - If the callback couldn't be called, then rollback the transaction. m_shouldCommitAfterErrorCallback = false; if (inCallback) { m_nextStep = &SQLTransaction::cleanupAfterTransactionErrorCallback; LOG(StorageAPI, "Scheduling cleanupAfterTransactionErrorCallback for transaction %p\n", this); m_database->scheduleTransactionStep(this); } else { cleanupAfterTransactionErrorCallback(); } } void SQLTransaction::deliverTransactionErrorCallback() { ASSERT(m_transactionError); // Transaction Step 12 - If the callback didn't return false, then rollback the transaction. // This includes the callback not existing, returning true, or throwing an exception if (!m_errorCallback || m_errorCallback->handleEvent(m_transactionError.get())) m_shouldCommitAfterErrorCallback = false; m_nextStep = &SQLTransaction::cleanupAfterTransactionErrorCallback; LOG(StorageAPI, "Scheduling cleanupAfterTransactionErrorCallback for transaction %p\n", this); m_database->scheduleTransactionStep(this); } void SQLTransaction::cleanupAfterTransactionErrorCallback() { m_database->m_databaseAuthorizer->disable(); if (m_sqliteTransaction) { // Transaction Step 12 -If the error callback returned false, and the last error wasn't itself a // failure when committing the transaction, then try to commit the transaction if (m_shouldCommitAfterErrorCallback) m_sqliteTransaction->commit(); if (m_sqliteTransaction->inProgress()) { // Transaction Step 12 - If that fails, or if the callback couldn't be called // or if it didn't return false, then rollback the transaction. m_sqliteTransaction->rollback(); } else if (m_modifiedDatabase) { // But if the commit was successful, notify the delegates if the transaction modified this database DatabaseTracker::tracker().scheduleNotifyDatabaseChanged(m_database->m_securityOrigin.get(), m_database->m_name); } ASSERT(!m_database->m_sqliteDatabase.transactionInProgress()); m_sqliteTransaction.clear(); } m_database->m_databaseAuthorizer->enable(); // Transaction Step 12 - Any still-pending statements in the transaction are discarded. { MutexLocker locker(m_statementMutex); m_statementQueue.clear(); } // Transaction is complete! There is no next step LOG(StorageAPI, "Transaction %p is complete with an error\n", this); ASSERT(!m_database->m_sqliteDatabase.transactionInProgress()); m_nextStep = 0; // Now release our callbacks, to break reference cycles. m_callback = 0; m_errorCallback = 0; } } // namespace WebCore