| /* |
| * Copyright (C) 2015, 2016 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 "IDBDatabase.h" |
| |
| #if ENABLE(INDEXED_DATABASE) |
| |
| #include "DOMStringList.h" |
| #include "EventNames.h" |
| #include "EventQueue.h" |
| #include "IDBConnectionProxy.h" |
| #include "IDBConnectionToServer.h" |
| #include "IDBIndex.h" |
| #include "IDBObjectStore.h" |
| #include "IDBOpenDBRequest.h" |
| #include "IDBResultData.h" |
| #include "IDBTransaction.h" |
| #include "IDBVersionChangeEvent.h" |
| #include "Logging.h" |
| #include "ScriptExecutionContext.h" |
| #include <JavaScriptCore/HeapInlines.h> |
| #include <wtf/IsoMallocInlines.h> |
| |
| namespace WebCore { |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(IDBDatabase); |
| |
| Ref<IDBDatabase> IDBDatabase::create(ScriptExecutionContext& context, IDBClient::IDBConnectionProxy& connectionProxy, const IDBResultData& resultData) |
| { |
| return adoptRef(*new IDBDatabase(context, connectionProxy, resultData)); |
| } |
| |
| IDBDatabase::IDBDatabase(ScriptExecutionContext& context, IDBClient::IDBConnectionProxy& connectionProxy, const IDBResultData& resultData) |
| : IDBActiveDOMObject(&context) |
| , m_connectionProxy(connectionProxy) |
| , m_info(resultData.databaseInfo()) |
| , m_databaseConnectionIdentifier(resultData.databaseConnectionIdentifier()) |
| , m_eventNames(eventNames()) |
| { |
| LOG(IndexedDB, "IDBDatabase::IDBDatabase - Creating database %s with version %" PRIu64 " connection %" PRIu64 " (%p)", m_info.name().utf8().data(), m_info.version(), m_databaseConnectionIdentifier, this); |
| suspendIfNeeded(); |
| m_connectionProxy->registerDatabaseConnection(*this); |
| } |
| |
| IDBDatabase::~IDBDatabase() |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (!m_closedInServer) |
| m_connectionProxy->databaseConnectionClosed(*this); |
| |
| m_connectionProxy->unregisterDatabaseConnection(*this); |
| } |
| |
| bool IDBDatabase::hasPendingActivity() const |
| { |
| ASSERT(&originThread() == &Thread::current() || Thread::mayBeGCThread()); |
| |
| if (m_closedInServer || isContextStopped()) |
| return false; |
| |
| if (!m_activeTransactions.isEmpty() || !m_committingTransactions.isEmpty() || !m_abortingTransactions.isEmpty()) |
| return true; |
| |
| return hasEventListeners(m_eventNames.abortEvent) || hasEventListeners(m_eventNames.errorEvent) || hasEventListeners(m_eventNames.versionchangeEvent); |
| } |
| |
| const String IDBDatabase::name() const |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| return m_info.name(); |
| } |
| |
| uint64_t IDBDatabase::version() const |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| return m_info.version(); |
| } |
| |
| Ref<DOMStringList> IDBDatabase::objectStoreNames() const |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| |
| auto objectStoreNames = DOMStringList::create(); |
| for (auto& name : m_info.objectStoreNames()) |
| objectStoreNames->append(name); |
| objectStoreNames->sort(); |
| return objectStoreNames; |
| } |
| |
| void IDBDatabase::renameObjectStore(IDBObjectStore& objectStore, const String& newName) |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| ASSERT(m_versionChangeTransaction); |
| ASSERT(m_info.hasObjectStore(objectStore.info().name())); |
| |
| m_info.renameObjectStore(objectStore.info().identifier(), newName); |
| |
| m_versionChangeTransaction->renameObjectStore(objectStore, newName); |
| } |
| |
| void IDBDatabase::renameIndex(IDBIndex& index, const String& newName) |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| ASSERT(m_versionChangeTransaction); |
| ASSERT(m_info.hasObjectStore(index.objectStore().info().name())); |
| ASSERT(m_info.infoForExistingObjectStore(index.objectStore().info().name())->hasIndex(index.info().name())); |
| |
| m_info.infoForExistingObjectStore(index.objectStore().info().name())->infoForExistingIndex(index.info().identifier())->rename(newName); |
| |
| m_versionChangeTransaction->renameIndex(index, newName); |
| } |
| |
| ExceptionOr<Ref<IDBObjectStore>> IDBDatabase::createObjectStore(const String& name, ObjectStoreParameters&& parameters) |
| { |
| LOG(IndexedDB, "IDBDatabase::createObjectStore - (%s %s)", m_info.name().utf8().data(), name.utf8().data()); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| ASSERT(!m_versionChangeTransaction || m_versionChangeTransaction->isVersionChange()); |
| |
| if (!m_versionChangeTransaction) |
| return Exception { InvalidStateError, "Failed to execute 'createObjectStore' on 'IDBDatabase': The database is not running a version change transaction."_s }; |
| |
| if (!m_versionChangeTransaction->isActive()) |
| return Exception { TransactionInactiveError }; |
| |
| auto& keyPath = parameters.keyPath; |
| if (keyPath && !isIDBKeyPathValid(keyPath.value())) |
| return Exception { SyntaxError, "Failed to execute 'createObjectStore' on 'IDBDatabase': The keyPath option is not a valid key path."_s }; |
| |
| if (m_info.hasObjectStore(name)) |
| return Exception { ConstraintError, "Failed to execute 'createObjectStore' on 'IDBDatabase': An object store with the specified name already exists."_s }; |
| |
| if (keyPath && parameters.autoIncrement && ((WTF::holds_alternative<String>(keyPath.value()) && WTF::get<String>(keyPath.value()).isEmpty()) || WTF::holds_alternative<Vector<String>>(keyPath.value()))) |
| return Exception { InvalidAccessError, "Failed to execute 'createObjectStore' on 'IDBDatabase': The autoIncrement option was set but the keyPath option was empty or an array."_s }; |
| |
| // Install the new ObjectStore into the connection's metadata. |
| auto info = m_info.createNewObjectStore(name, WTFMove(keyPath), parameters.autoIncrement); |
| |
| // Create the actual IDBObjectStore from the transaction, which also schedules the operation server side. |
| return m_versionChangeTransaction->createObjectStore(info); |
| } |
| |
| ExceptionOr<Ref<IDBTransaction>> IDBDatabase::transaction(StringOrVectorOfStrings&& storeNames, IDBTransactionMode mode) |
| { |
| LOG(IndexedDB, "IDBDatabase::transaction"); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (m_versionChangeTransaction && !m_versionChangeTransaction->isFinishedOrFinishing()) |
| return Exception { InvalidStateError, "Failed to execute 'transaction' on 'IDBDatabase': A version change transaction is running."_s }; |
| |
| if (m_closePending) |
| return Exception { InvalidStateError, "Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing."_s }; |
| |
| Vector<String> objectStores; |
| if (WTF::holds_alternative<Vector<String>>(storeNames)) |
| objectStores = WTFMove(WTF::get<Vector<String>>(storeNames)); |
| else |
| objectStores.append(WTFMove(WTF::get<String>(storeNames))); |
| |
| // It is valid for javascript to pass in a list of object store names with the same name listed twice, |
| // so we need to put them all in a set to get a unique list. |
| HashSet<String> objectStoreSet; |
| for (auto& objectStore : objectStores) |
| objectStoreSet.add(objectStore); |
| |
| objectStores = copyToVector(objectStoreSet); |
| |
| for (auto& objectStoreName : objectStores) { |
| if (m_info.hasObjectStore(objectStoreName)) |
| continue; |
| return Exception { NotFoundError, "Failed to execute 'transaction' on 'IDBDatabase': One of the specified object stores was not found."_s }; |
| } |
| |
| if (objectStores.isEmpty()) |
| return Exception { InvalidAccessError, "Failed to execute 'transaction' on 'IDBDatabase': The storeNames parameter was empty."_s }; |
| |
| if (mode != IDBTransactionMode::Readonly && mode != IDBTransactionMode::Readwrite) |
| return Exception { TypeError }; |
| |
| auto info = IDBTransactionInfo::clientTransaction(m_connectionProxy.get(), objectStores, mode); |
| |
| LOG(IndexedDBOperations, "IDB creating transaction: %s", info.loggingString().utf8().data()); |
| auto transaction = IDBTransaction::create(*this, info); |
| |
| LOG(IndexedDB, "IDBDatabase::transaction - Added active transaction %s", info.identifier().loggingString().utf8().data()); |
| |
| m_activeTransactions.set(info.identifier(), transaction.ptr()); |
| |
| return transaction; |
| } |
| |
| ExceptionOr<void> IDBDatabase::deleteObjectStore(const String& objectStoreName) |
| { |
| LOG(IndexedDB, "IDBDatabase::deleteObjectStore"); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (!m_versionChangeTransaction) |
| return Exception { InvalidStateError, "Failed to execute 'deleteObjectStore' on 'IDBDatabase': The database is not running a version change transaction."_s }; |
| |
| if (!m_versionChangeTransaction->isActive()) |
| return Exception { TransactionInactiveError }; |
| |
| if (!m_info.hasObjectStore(objectStoreName)) |
| return Exception { NotFoundError, "Failed to execute 'deleteObjectStore' on 'IDBDatabase': The specified object store was not found."_s }; |
| |
| m_info.deleteObjectStore(objectStoreName); |
| m_versionChangeTransaction->deleteObjectStore(objectStoreName); |
| |
| return { }; |
| } |
| |
| void IDBDatabase::close() |
| { |
| LOG(IndexedDB, "IDBDatabase::close - %" PRIu64, m_databaseConnectionIdentifier); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (!m_closePending) { |
| m_closePending = true; |
| m_connectionProxy->databaseConnectionPendingClose(*this); |
| } |
| |
| maybeCloseInServer(); |
| } |
| |
| void IDBDatabase::didCloseFromServer(const IDBError& error) |
| { |
| LOG(IndexedDB, "IDBDatabase::didCloseFromServer - %" PRIu64, m_databaseConnectionIdentifier); |
| |
| connectionToServerLost(error); |
| |
| m_connectionProxy->confirmDidCloseFromServer(*this); |
| } |
| |
| void IDBDatabase::connectionToServerLost(const IDBError& error) |
| { |
| LOG(IndexedDB, "IDBDatabase::connectionToServerLost - %" PRIu64, m_databaseConnectionIdentifier); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| m_closePending = true; |
| m_closedInServer = true; |
| |
| auto activeTransactions = copyToVector(m_activeTransactions.values()); |
| for (auto& transaction : activeTransactions) |
| transaction->connectionClosedFromServer(error); |
| |
| auto committingTransactions = copyToVector(m_committingTransactions.values()); |
| for (auto& transaction : committingTransactions) |
| transaction->connectionClosedFromServer(error); |
| |
| auto errorEvent = Event::create(m_eventNames.errorEvent, Event::CanBubble::Yes, Event::IsCancelable::No); |
| errorEvent->setTarget(this); |
| |
| if (auto* context = scriptExecutionContext()) |
| context->eventQueue().enqueueEvent(WTFMove(errorEvent)); |
| |
| auto closeEvent = Event::create(m_eventNames.closeEvent, Event::CanBubble::Yes, Event::IsCancelable::No); |
| closeEvent->setTarget(this); |
| |
| if (auto* context = scriptExecutionContext()) |
| context->eventQueue().enqueueEvent(WTFMove(closeEvent)); |
| } |
| |
| void IDBDatabase::maybeCloseInServer() |
| { |
| LOG(IndexedDB, "IDBDatabase::maybeCloseInServer - %" PRIu64, m_databaseConnectionIdentifier); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (m_closedInServer) |
| return; |
| |
| // 3.3.9 Database closing steps |
| // Wait for all transactions created using this connection to complete. |
| // Once they are complete, this connection is closed. |
| if (!m_activeTransactions.isEmpty() || !m_committingTransactions.isEmpty()) |
| return; |
| |
| m_closedInServer = true; |
| m_connectionProxy->databaseConnectionClosed(*this); |
| } |
| |
| const char* IDBDatabase::activeDOMObjectName() const |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| return "IDBDatabase"; |
| } |
| |
| void IDBDatabase::stop() |
| { |
| LOG(IndexedDB, "IDBDatabase::stop - %" PRIu64, m_databaseConnectionIdentifier); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| removeAllEventListeners(); |
| |
| Vector<IDBResourceIdentifier> transactionIdentifiers; |
| transactionIdentifiers.reserveInitialCapacity(m_activeTransactions.size()); |
| |
| for (auto& id : m_activeTransactions.keys()) |
| transactionIdentifiers.uncheckedAppend(id); |
| |
| for (auto& id : transactionIdentifiers) { |
| IDBTransaction* transaction = m_activeTransactions.get(id); |
| if (transaction) |
| transaction->stop(); |
| } |
| |
| close(); |
| } |
| |
| Ref<IDBTransaction> IDBDatabase::startVersionChangeTransaction(const IDBTransactionInfo& info, IDBOpenDBRequest& request) |
| { |
| LOG(IndexedDB, "IDBDatabase::startVersionChangeTransaction %s", info.identifier().loggingString().utf8().data()); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| ASSERT(!m_versionChangeTransaction); |
| ASSERT(info.mode() == IDBTransactionMode::Versionchange); |
| ASSERT(!m_closePending); |
| ASSERT(scriptExecutionContext()); |
| |
| Ref<IDBTransaction> transaction = IDBTransaction::create(*this, info, request); |
| m_versionChangeTransaction = &transaction.get(); |
| |
| m_activeTransactions.set(transaction->info().identifier(), &transaction.get()); |
| |
| return transaction; |
| } |
| |
| void IDBDatabase::didStartTransaction(IDBTransaction& transaction) |
| { |
| LOG(IndexedDB, "IDBDatabase::didStartTransaction %s", transaction.info().identifier().loggingString().utf8().data()); |
| ASSERT(!m_versionChangeTransaction); |
| ASSERT(&originThread() == &Thread::current()); |
| |
| // It is possible for the client to have aborted a transaction before the server replies back that it has started. |
| if (m_abortingTransactions.contains(transaction.info().identifier())) |
| return; |
| |
| m_activeTransactions.set(transaction.info().identifier(), &transaction); |
| } |
| |
| void IDBDatabase::willCommitTransaction(IDBTransaction& transaction) |
| { |
| LOG(IndexedDB, "IDBDatabase::willCommitTransaction %s", transaction.info().identifier().loggingString().utf8().data()); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| auto refTransaction = m_activeTransactions.take(transaction.info().identifier()); |
| ASSERT(refTransaction); |
| m_committingTransactions.set(transaction.info().identifier(), WTFMove(refTransaction)); |
| } |
| |
| void IDBDatabase::didCommitTransaction(IDBTransaction& transaction) |
| { |
| LOG(IndexedDB, "IDBDatabase::didCommitTransaction %s", transaction.info().identifier().loggingString().utf8().data()); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (m_versionChangeTransaction == &transaction) |
| m_info.setVersion(transaction.info().newVersion()); |
| |
| didCommitOrAbortTransaction(transaction); |
| } |
| |
| void IDBDatabase::willAbortTransaction(IDBTransaction& transaction) |
| { |
| LOG(IndexedDB, "IDBDatabase::willAbortTransaction %s", transaction.info().identifier().loggingString().utf8().data()); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| auto refTransaction = m_activeTransactions.take(transaction.info().identifier()); |
| if (!refTransaction) |
| refTransaction = m_committingTransactions.take(transaction.info().identifier()); |
| |
| ASSERT(refTransaction); |
| m_abortingTransactions.set(transaction.info().identifier(), WTFMove(refTransaction)); |
| |
| if (transaction.isVersionChange()) { |
| ASSERT(transaction.originalDatabaseInfo()); |
| m_info = *transaction.originalDatabaseInfo(); |
| m_closePending = true; |
| } |
| } |
| |
| void IDBDatabase::didAbortTransaction(IDBTransaction& transaction) |
| { |
| LOG(IndexedDB, "IDBDatabase::didAbortTransaction %s", transaction.info().identifier().loggingString().utf8().data()); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (transaction.isVersionChange()) { |
| ASSERT(transaction.originalDatabaseInfo()); |
| ASSERT(m_info.version() == transaction.originalDatabaseInfo()->version()); |
| m_closePending = true; |
| maybeCloseInServer(); |
| } |
| |
| didCommitOrAbortTransaction(transaction); |
| } |
| |
| void IDBDatabase::didCommitOrAbortTransaction(IDBTransaction& transaction) |
| { |
| LOG(IndexedDB, "IDBDatabase::didCommitOrAbortTransaction %s", transaction.info().identifier().loggingString().utf8().data()); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (m_versionChangeTransaction == &transaction) |
| m_versionChangeTransaction = nullptr; |
| |
| #ifndef NDBEBUG |
| unsigned count = 0; |
| if (m_activeTransactions.contains(transaction.info().identifier())) |
| ++count; |
| if (m_committingTransactions.contains(transaction.info().identifier())) |
| ++count; |
| if (m_abortingTransactions.contains(transaction.info().identifier())) |
| ++count; |
| |
| ASSERT(count == 1); |
| #endif |
| |
| m_activeTransactions.remove(transaction.info().identifier()); |
| m_committingTransactions.remove(transaction.info().identifier()); |
| m_abortingTransactions.remove(transaction.info().identifier()); |
| |
| if (m_closePending) |
| maybeCloseInServer(); |
| } |
| |
| void IDBDatabase::fireVersionChangeEvent(const IDBResourceIdentifier& requestIdentifier, uint64_t requestedVersion) |
| { |
| uint64_t currentVersion = m_info.version(); |
| LOG(IndexedDB, "IDBDatabase::fireVersionChangeEvent - current version %" PRIu64 ", requested version %" PRIu64 ", connection %" PRIu64 " (%p)", currentVersion, requestedVersion, m_databaseConnectionIdentifier, this); |
| |
| ASSERT(&originThread() == &Thread::current()); |
| |
| if (!scriptExecutionContext() || m_closePending) { |
| connectionProxy().didFireVersionChangeEvent(m_databaseConnectionIdentifier, requestIdentifier); |
| return; |
| } |
| |
| Ref<Event> event = IDBVersionChangeEvent::create(requestIdentifier, currentVersion, requestedVersion, m_eventNames.versionchangeEvent); |
| event->setTarget(this); |
| scriptExecutionContext()->eventQueue().enqueueEvent(WTFMove(event)); |
| } |
| |
| void IDBDatabase::dispatchEvent(Event& event) |
| { |
| LOG(IndexedDB, "IDBDatabase::dispatchEvent (%" PRIu64 ") (%p)", m_databaseConnectionIdentifier, this); |
| ASSERT(&originThread() == &Thread::current()); |
| |
| auto protectedThis = makeRef(*this); |
| |
| EventTargetWithInlineData::dispatchEvent(event); |
| |
| if (event.isVersionChangeEvent() && event.type() == m_eventNames.versionchangeEvent) |
| m_connectionProxy->didFireVersionChangeEvent(m_databaseConnectionIdentifier, downcast<IDBVersionChangeEvent>(event).requestIdentifier()); |
| } |
| |
| void IDBDatabase::didCreateIndexInfo(const IDBIndexInfo& info) |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| |
| auto* objectStore = m_info.infoForExistingObjectStore(info.objectStoreIdentifier()); |
| ASSERT(objectStore); |
| objectStore->addExistingIndex(info); |
| } |
| |
| void IDBDatabase::didDeleteIndexInfo(const IDBIndexInfo& info) |
| { |
| ASSERT(&originThread() == &Thread::current()); |
| |
| auto* objectStore = m_info.infoForExistingObjectStore(info.objectStoreIdentifier()); |
| ASSERT(objectStore); |
| objectStore->deleteIndex(info.name()); |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(INDEXED_DATABASE) |