/* * Copyright (C) 2007, 2008, 2013 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 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 "Database.h" #include "DatabaseAuthorizer.h" #include "DatabaseContext.h" #include "DatabaseThread.h" #include "DatabaseTracker.h" #include "ExceptionCode.h" #include "Logging.h" #include "OriginLock.h" #include "SQLError.h" #include "SQLStatement.h" #include "SQLStatementCallback.h" #include "SQLStatementErrorCallback.h" #include "SQLTransactionBackend.h" #include "SQLTransactionCallback.h" #include "SQLTransactionCoordinator.h" #include "SQLTransactionErrorCallback.h" #include "SQLiteTransaction.h" #include "VoidCallback.h" #include #include namespace WebCore { Ref SQLTransaction::create(Ref&& database, RefPtr&& callback, RefPtr&& successCallback, RefPtr&& errorCallback, RefPtr&& wrapper, bool readOnly) { return adoptRef(*new SQLTransaction(WTFMove(database), WTFMove(callback), WTFMove(successCallback), WTFMove(errorCallback), WTFMove(wrapper), readOnly)); } SQLTransaction::SQLTransaction(Ref&& database, RefPtr&& callback, RefPtr&& successCallback, RefPtr&& errorCallback, RefPtr&& wrapper, bool readOnly) : m_database(WTFMove(database)) , m_callbackWrapper(WTFMove(callback), &m_database->scriptExecutionContext()) , m_successCallbackWrapper(WTFMove(successCallback), &m_database->scriptExecutionContext()) , m_errorCallbackWrapper(WTFMove(errorCallback), &m_database->scriptExecutionContext()) , m_wrapper(WTFMove(wrapper)) , m_nextStep(&SQLTransaction::acquireLock) , m_readOnly(readOnly) , m_backend(*this) { } SQLTransaction::~SQLTransaction() { } ExceptionOr SQLTransaction::executeSql(const String& sqlStatement, std::optional>&& arguments, RefPtr&& callback, RefPtr&& callbackError) { if (!m_executeSqlAllowed || !m_database->opened()) return Exception { INVALID_STATE_ERR }; int permissions = DatabaseAuthorizer::ReadWriteMask; if (!m_database->databaseContext().allowDatabaseAccess()) permissions |= DatabaseAuthorizer::NoAccessMask; else if (m_readOnly) permissions |= DatabaseAuthorizer::ReadOnlyMask; auto statement = std::make_unique(m_database, sqlStatement, arguments.value_or(Vector { }), WTFMove(callback), WTFMove(callbackError), permissions); if (m_database->deleted()) statement->setDatabaseDeletedError(); enqueueStatement(WTFMove(statement)); return { }; } void SQLTransaction::lockAcquired() { m_lockAcquired = true; m_backend.m_requestedState = SQLTransactionState::OpenTransactionAndPreflight; m_database->scheduleTransactionStep(*this); } void SQLTransaction::performNextStep() { m_backend.computeNextStateAndCleanupIfNeeded(); m_backend.runStateMachine(); } 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::notifyDatabaseThreadIsShuttingDown() { m_backend.notifyDatabaseThreadIsShuttingDown(); } void SQLTransaction::enqueueStatement(std::unique_ptr statement) { LockHolder locker(m_statementMutex); m_statementQueue.append(WTFMove(statement)); } SQLTransaction::StateFunction SQLTransaction::stateFunctionFor(SQLTransactionState state) { static const StateFunction stateFunctions[] = { &SQLTransaction::unreachableState, // 0. illegal &SQLTransaction::unreachableState, // 1. idle &SQLTransaction::unreachableState, // 2. acquireLock &SQLTransaction::unreachableState, // 3. openTransactionAndPreflight &SQLTransaction::unreachableState, // 4. runStatements &SQLTransaction::unreachableState, // 5. postflightAndCommit &SQLTransaction::unreachableState, // 6. cleanupAndTerminate &SQLTransaction::unreachableState, // 7. cleanupAfterTransactionErrorCallback &SQLTransaction::deliverTransactionCallback, // 8. &SQLTransaction::deliverTransactionErrorCallback, // 9. &SQLTransaction::deliverStatementCallback, // 10. &SQLTransaction::deliverQuotaIncreaseCallback, // 11. &SQLTransaction::deliverSuccessCallback // 12. }; ASSERT(WTF_ARRAY_LENGTH(stateFunctions) == static_cast(SQLTransactionState::NumberOfStates)); ASSERT(state < SQLTransactionState::NumberOfStates); return stateFunctions[static_cast(state)]; } // requestTransitToState() can be called from the backend. Hence, it should // NOT be modifying SQLTransactionBackend in general. The only safe field to // modify is m_requestedState which is meant for this purpose. void SQLTransaction::requestTransitToState(SQLTransactionState nextState) { LOG(StorageAPI, "Scheduling %s for transaction %p\n", nameForSQLTransactionState(nextState), this); m_requestedState = nextState; m_database->scheduleTransactionCallback(this); } void SQLTransaction::checkAndHandleClosedDatabase() { if (m_database->opened()) return; // If the database was stopped, don't do anything and cancel queued work LOG(StorageAPI, "Database was stopped or interrupted - cancelling work for this transaction"); LockHolder locker(m_statementMutex); m_statementQueue.clear(); m_nextStep = nullptr; // Release the unneeded callbacks, to break reference cycles. m_callbackWrapper.clear(); m_successCallbackWrapper.clear(); m_errorCallbackWrapper.clear(); // The next steps should be executed only if we're on the DB thread. if (currentThread() != m_database->databaseThread().getThreadID()) return; // The current SQLite transaction should be stopped, as well if (m_sqliteTransaction) { m_sqliteTransaction->stop(); m_sqliteTransaction = nullptr; } if (m_lockAcquired) m_database->transactionCoordinator()->releaseLock(*this); } void SQLTransaction::scheduleCallback(void (SQLTransaction::*step)()) { m_nextStep = step; LOG(StorageAPI, "Scheduling %s for transaction %p\n", debugStepName(step), this); m_database->scheduleTransactionCallback(this); } void SQLTransaction::acquireLock() { m_database->transactionCoordinator()->acquireLock(*this); } void SQLTransaction::openTransactionAndPreflight() { ASSERT(!m_database->sqliteDatabase().transactionInProgress()); ASSERT(m_lockAcquired); 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(SQLError::UNKNOWN_ERR, "unable to open a transaction, because the user deleted the database"); handleTransactionError(); return; } // Set the maximum usage for this transaction if this transactions is not read-only if (!m_readOnly) { acquireOriginLock(); m_database->sqliteDatabase().setMaximumSize(m_database->maximumSize()); } ASSERT(!m_sqliteTransaction); m_sqliteTransaction = std::make_unique(m_database->sqliteDatabase(), m_readOnly); m_database->resetDeletes(); m_database->disableAuthorizer(); m_sqliteTransaction->begin(); m_database->enableAuthorizer(); // Spec 4.3.2.1+2: Open a transaction to the database, jumping to the error callback if that fails if (!m_sqliteTransaction->inProgress()) { ASSERT(!m_database->sqliteDatabase().transactionInProgress()); m_transactionError = SQLError::create(SQLError::DATABASE_ERR, "unable to begin transaction", m_database->sqliteDatabase().lastError(), m_database->sqliteDatabase().lastErrorMsg()); m_sqliteTransaction = nullptr; handleTransactionError(); return; } // Note: We intentionally retrieve the actual version even with an empty expected version. // In multi-process browsers, we take this opportinutiy to update the cached value for // the actual version. In single-process browsers, this is just a map lookup. String actualVersion; if (!m_database->getActualVersionForTransaction(actualVersion)) { m_transactionError = SQLError::create(SQLError::DATABASE_ERR, "unable to read version", m_database->sqliteDatabase().lastError(), m_database->sqliteDatabase().lastErrorMsg()); m_database->disableAuthorizer(); m_sqliteTransaction = nullptr; m_database->enableAuthorizer(); handleTransactionError(); return; } m_hasVersionMismatch = !m_database->expectedVersion().isEmpty() && (m_database->expectedVersion() != actualVersion); // Spec 4.3.2.3: Perform preflight steps, jumping to the error callback if they fail if (m_wrapper && !m_wrapper->performPreflight(*this)) { m_database->disableAuthorizer(); m_sqliteTransaction = nullptr; m_database->enableAuthorizer(); m_transactionError = m_wrapper->sqlError(); if (!m_transactionError) m_transactionError = SQLError::create(SQLError::UNKNOWN_ERR, "unknown error occurred during transaction preflight"); handleTransactionError(); return; } // Spec 4.3.2.4: Invoke the transaction callback with the new SQLTransaction object if (m_callbackWrapper.hasCallback()) { scheduleCallback(&SQLTransaction::deliverTransactionCallback); return; } // If we have no callback to make, skip pass to the state after: runStatements(); } void SQLTransaction::runStatements() { ASSERT(m_lockAcquired); // 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_sqliteTransaction->wasRolledBackBySqlite()) { 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_shouldRetryCurrentStatement is set to true only when a statement exceeds // the quota, which can happen only in a read-write transaction. Therefore, there // is no need to check here if the transaction is read-write. m_database->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::cleanupAndTerminate() { ASSERT(m_lockAcquired); // Spec 4.3.2.9: End transaction steps. There is no next step. LOG(StorageAPI, "Transaction %p is complete\n", this); ASSERT(!m_database->sqliteDatabase().transactionInProgress()); // Phase 5 cleanup. See comment on the SQLTransaction life-cycle above. m_backend.doCleanup(); m_database->inProgressTransactionCompleted(); } void SQLTransaction::cleanupAfterTransactionErrorCallback() { ASSERT(m_lockAcquired); LOG(StorageAPI, "Transaction %p is complete with an error\n", this); m_database->disableAuthorizer(); if (m_sqliteTransaction) { // Spec 4.3.2.10: Rollback the transaction. m_sqliteTransaction->rollback(); ASSERT(!m_database->sqliteDatabase().transactionInProgress()); m_sqliteTransaction = nullptr; } m_database->enableAuthorizer(); releaseOriginLockIfNeeded(); ASSERT(!m_database->sqliteDatabase().transactionInProgress()); cleanupAndTerminate(); } void SQLTransaction::deliverTransactionCallback() { bool shouldDeliverErrorCallback = false; // Spec 4.3.2 4: Invoke the transaction callback with the new SQLTransaction object RefPtr callback = m_callbackWrapper.unwrap(); if (callback) { m_executeSqlAllowed = true; shouldDeliverErrorCallback = !callback->handleEvent(this); m_executeSqlAllowed = false; } // Spec 4.3.2 5: If the transaction callback was null or raised an exception, jump to the error callback if (shouldDeliverErrorCallback) { m_transactionError = SQLError::create(SQLError::UNKNOWN_ERR, "the SQLTransactionCallback was null or threw an exception"); return deliverTransactionErrorCallback(); } m_backend.requestTransitToState(SQLTransactionState::RunStatements); } void SQLTransaction::deliverTransactionErrorCallback() { ASSERT(m_transactionError); // Spec 4.3.2.10: If exists, invoke error callback with the last // error to have occurred in this transaction. RefPtr errorCallback = m_errorCallbackWrapper.unwrap(); if (errorCallback) errorCallback->handleEvent(m_transactionError.get()); clearCallbackWrappers(); // Spec 4.3.2.10: Rollback the transaction. m_backend.requestTransitToState(SQLTransactionState::CleanupAfterTransactionErrorCallback); } void SQLTransaction::deliverStatementCallback() { ASSERT(m_currentStatement); // Spec 4.3.2.6.6 and 4.3.2.6.3: 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(SQLError::UNKNOWN_ERR, "the statement callback raised an exception or statement error callback did not return false"); if (m_errorCallbackWrapper.hasCallback()) return deliverTransactionErrorCallback(); // No error callback, so fast-forward to: // Transaction Step 11 - Rollback the transaction. m_backend.requestTransitToState(SQLTransactionState::CleanupAfterTransactionErrorCallback); return; } m_backend.requestTransitToState(SQLTransactionState::RunStatements); } void SQLTransaction::deliverQuotaIncreaseCallback() { ASSERT(m_currentStatement); ASSERT(!m_shouldRetryCurrentStatement); m_shouldRetryCurrentStatement = m_database->didExceedQuota(); m_backend.requestTransitToState(SQLTransactionState::RunStatements); } void SQLTransaction::deliverSuccessCallback() { // Spec 4.3.2.8: Deliver success callback. RefPtr successCallback = m_successCallbackWrapper.unwrap(); if (successCallback) successCallback->handleEvent(); clearCallbackWrappers(); // 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_backend.requestTransitToState(SQLTransactionState::CleanupAndTerminate); } // This state function is used as a stub function to plug unimplemented states // in the state dispatch table. They are unimplemented because they should // never be reached in the course of correct execution. void SQLTransaction::unreachableState() { ASSERT_NOT_REACHED(); } void SQLTransaction::computeNextStateAndCleanupIfNeeded() { // Only honor the requested state transition if we're not supposed to be // cleaning up and shutting down: if (m_database->opened()) { setStateToRequestedState(); ASSERT(m_nextState == SQLTransactionState::End || m_nextState == SQLTransactionState::DeliverTransactionCallback || m_nextState == SQLTransactionState::DeliverTransactionErrorCallback || m_nextState == SQLTransactionState::DeliverStatementCallback || m_nextState == SQLTransactionState::DeliverQuotaIncreaseCallback || m_nextState == SQLTransactionState::DeliverSuccessCallback); LOG(StorageAPI, "Callback %s\n", nameForSQLTransactionState(m_nextState)); return; } clearCallbackWrappers(); m_backend.requestTransitToState(SQLTransactionState::CleanupAndTerminate); } void SQLTransaction::clearCallbackWrappers() { // Release the unneeded callbacks, to break reference cycles. m_callbackWrapper.clear(); m_successCallbackWrapper.clear(); m_errorCallbackWrapper.clear(); } void SQLTransaction::getNextStatement() { m_currentStatement = nullptr; LockHolder locker(m_statementMutex); if (!m_statementQueue.isEmpty()) m_currentStatement = m_statementQueue.takeFirst(); } bool SQLTransaction::runCurrentStatement() { if (!m_currentStatement) { // No more statements to run. So move on to the next state. return false; } m_database->resetAuthorizer(); if (m_hasVersionMismatch) m_currentStatement->setVersionMismatchedError(); if (m_currentStatement->execute(m_database)) { if (m_database->lastActionChangedDatabase()) { // Flag this transaction as having changed the database for later delegate notification m_modifiedDatabase = true; } if (m_currentStatement->hasStatementCallback()) { scheduleCallback(&SQLTransaction::deliverStatementCallback); return false; } // If we get here, then the statement doesn't have a callback to invoke. // We can move on to the next statement. Hence, stay in this state. return true; } if (m_currentStatement->lastExecutionFailedDueToQuota()) { scheduleCallback(&SQLTransaction::deliverQuotaIncreaseCallback); return false; } handleCurrentStatementError(); return false; } void SQLTransaction::handleCurrentStatementError() { // Spec 4.3.2.6.6: error - Call the statement's error callback, but if there was no error callback, // or the transaction was rolled back, jump to the transaction error callback if (m_currentStatement->hasStatementErrorCallback() && !m_sqliteTransaction->wasRolledBackBySqlite()) { scheduleCallback(&SQLTransaction::deliverStatementCallback); return; } m_transactionError = m_currentStatement->sqlError(); if (!m_transactionError) m_transactionError = SQLError::create(SQLError::DATABASE_ERR, "the statement failed to execute"); handleTransactionError(); } void SQLTransaction::handleTransactionError() { ASSERT(m_transactionError); if (m_errorCallbackWrapper.hasCallback()) { scheduleCallback(&SQLTransaction::deliverTransactionErrorCallback); return; } // No error callback, so fast-forward to the next state and rollback the // transaction. m_backend.cleanupAfterTransactionErrorCallback(); } void SQLTransaction::postflightAndCommit() { ASSERT(m_lockAcquired); // Spec 4.3.2.7: Perform 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(SQLError::UNKNOWN_ERR, "unknown error occurred during transaction postflight"); handleTransactionError(); return; } // Spec 4.3.2.7: Commit the transaction, jumping to the error callback if that fails. ASSERT(m_sqliteTransaction); m_database->disableAuthorizer(); m_sqliteTransaction->commit(); m_database->enableAuthorizer(); releaseOriginLockIfNeeded(); // If the commit failed, the transaction will still be marked as "in progress" if (m_sqliteTransaction->inProgress()) { if (m_wrapper) m_wrapper->handleCommitFailedAfterPostflight(*this); m_transactionError = SQLError::create(SQLError::DATABASE_ERR, "unable to commit transaction", m_database->sqliteDatabase().lastError(), m_database->sqliteDatabase().lastErrorMsg()); handleTransactionError(); return; } // Vacuum the database if anything was deleted. if (m_database->hadDeletes()) m_database->incrementalVacuumIfNeeded(); // The commit was successful. If the transaction modified this database, notify the delegates. if (m_modifiedDatabase) m_database->didCommitWriteTransaction(); // Spec 4.3.2.8: Deliver success callback, if there is one. scheduleCallback(&SQLTransaction::deliverSuccessCallback); } void SQLTransaction::acquireOriginLock() { ASSERT(!m_originLock); m_originLock = DatabaseTracker::singleton().originLockFor(m_database->securityOrigin()); m_originLock->lock(); } void SQLTransaction::releaseOriginLockIfNeeded() { if (m_originLock) { m_originLock->unlock(); m_originLock = nullptr; } } #if !LOG_DISABLED const char* SQLTransaction::debugStepName(void (SQLTransaction::*step)()) { if (step == &SQLTransaction::acquireLock) return "acquireLock"; if (step == &SQLTransaction::openTransactionAndPreflight) return "openTransactionAndPreflight"; if (step == &SQLTransaction::runStatements) return "runStatements"; if (step == &SQLTransaction::postflightAndCommit) return "postflightAndCommit"; if (step == &SQLTransaction::cleanupAfterTransactionErrorCallback) return "cleanupAfterTransactionErrorCallback"; if (step == &SQLTransaction::deliverTransactionCallback) return "deliverTransactionCallback"; if (step == &SQLTransaction::deliverTransactionErrorCallback) return "deliverTransactionErrorCallback"; if (step == &SQLTransaction::deliverStatementCallback) return "deliverStatementCallback"; if (step == &SQLTransaction::deliverQuotaIncreaseCallback) return "deliverQuotaIncreaseCallback"; if (step == &SQLTransaction::deliverSuccessCallback) return "deliverSuccessCallback"; ASSERT_NOT_REACHED(); return "UNKNOWN"; } #endif } // namespace WebCore