blob: 283245e594d24b971e8c22fbe2604a2550742e54 [file] [log] [blame]
/*
* Copyright (C) 2010 Google 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 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 "third_party/blink/renderer/modules/indexeddb/idb_database.h"
#include <limits>
#include <memory>
#include <utility>
#include "base/atomic_sequence_num.h"
#include "base/optional.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/common/indexeddb/web_idb_types.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.h"
#include "third_party/blink/renderer/core/dom/events/event_queue.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/modules/indexed_db_names.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_any.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_event_dispatcher.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_index.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_key_path.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_tracing.h"
#include "third_party/blink/renderer/modules/indexeddb/idb_version_change_event.h"
#include "third_party/blink/renderer/modules/indexeddb/web_idb_database_callbacks.h"
#include "third_party/blink/renderer/modules/indexeddb/web_idb_database_callbacks_impl.h"
#include "third_party/blink/renderer/modules/indexeddb/web_idb_transaction_impl.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/wtf/assertions.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
namespace blink {
const char IDBDatabase::kIndexDeletedErrorMessage[] =
"The index or its object store has been deleted.";
const char IDBDatabase::kIndexNameTakenErrorMessage[] =
"An index with the specified name already exists.";
const char IDBDatabase::kIsKeyCursorErrorMessage[] =
"The cursor is a key cursor.";
const char IDBDatabase::kNoKeyOrKeyRangeErrorMessage[] =
"No key or key range specified.";
const char IDBDatabase::kNoSuchIndexErrorMessage[] =
"The specified index was not found.";
const char IDBDatabase::kNoSuchObjectStoreErrorMessage[] =
"The specified object store was not found.";
const char IDBDatabase::kNoValueErrorMessage[] =
"The cursor is being iterated or has iterated past its end.";
const char IDBDatabase::kNotValidKeyErrorMessage[] =
"The parameter is not a valid key.";
const char IDBDatabase::kNotVersionChangeTransactionErrorMessage[] =
"The database is not running a version change transaction.";
const char IDBDatabase::kObjectStoreDeletedErrorMessage[] =
"The object store has been deleted.";
const char IDBDatabase::kObjectStoreNameTakenErrorMessage[] =
"An object store with the specified name already exists.";
const char IDBDatabase::kRequestNotFinishedErrorMessage[] =
"The request has not finished.";
const char IDBDatabase::kSourceDeletedErrorMessage[] =
"The cursor's source or effective object store has been deleted.";
const char IDBDatabase::kTransactionInactiveErrorMessage[] =
"The transaction is not active.";
const char IDBDatabase::kTransactionFinishedErrorMessage[] =
"The transaction has finished.";
const char IDBDatabase::kTransactionReadOnlyErrorMessage[] =
"The transaction is read-only.";
const char IDBDatabase::kDatabaseClosedErrorMessage[] =
"The database connection is closed.";
IDBDatabase::IDBDatabase(
ExecutionContext* context,
std::unique_ptr<WebIDBDatabase> backend,
IDBDatabaseCallbacks* callbacks,
mojo::PendingRemote<mojom::blink::ObservedFeature> connection_lifetime)
: ExecutionContextLifecycleObserver(context),
backend_(std::move(backend)),
connection_lifetime_(std::move(connection_lifetime)),
event_queue_(
MakeGarbageCollected<EventQueue>(context, TaskType::kDatabaseAccess)),
database_callbacks_(callbacks),
feature_handle_for_scheduler_(
context
? context->GetScheduler()->RegisterFeature(
SchedulingPolicy::Feature::kIndexedDBConnection,
{SchedulingPolicy::DisableBackForwardCache()})
: FrameOrWorkerScheduler::SchedulingAffectingFeatureHandle()) {
database_callbacks_->Connect(this);
}
IDBDatabase::~IDBDatabase() {
if (!close_pending_ && backend_)
backend_->Close();
}
void IDBDatabase::Trace(Visitor* visitor) const {
visitor->Trace(version_change_transaction_);
visitor->Trace(transactions_);
visitor->Trace(event_queue_);
visitor->Trace(database_callbacks_);
EventTargetWithInlineData::Trace(visitor);
ExecutionContextLifecycleObserver::Trace(visitor);
}
int64_t IDBDatabase::NextTransactionId() {
// Starts at 1, unlike AtomicSequenceNumber.
// Only keep a 32-bit counter to allow ports to use the other 32
// bits of the id.
static base::AtomicSequenceNumber current_transaction_id;
return current_transaction_id.GetNext() + 1;
}
void IDBDatabase::SetMetadata(const IDBDatabaseMetadata& metadata) {
metadata_ = metadata;
}
void IDBDatabase::SetDatabaseMetadata(const IDBDatabaseMetadata& metadata) {
metadata_.CopyFrom(metadata);
}
void IDBDatabase::TransactionCreated(IDBTransaction* transaction) {
DCHECK(transaction);
DCHECK(!transactions_.Contains(transaction->Id()));
transactions_.insert(transaction->Id(), transaction);
if (transaction->IsVersionChange()) {
DCHECK(!version_change_transaction_);
version_change_transaction_ = transaction;
}
}
void IDBDatabase::TransactionFinished(const IDBTransaction* transaction) {
DCHECK(transaction);
DCHECK(transactions_.Contains(transaction->Id()));
DCHECK_EQ(transactions_.at(transaction->Id()), transaction);
transactions_.erase(transaction->Id());
if (transaction->IsVersionChange()) {
DCHECK_EQ(version_change_transaction_, transaction);
version_change_transaction_ = nullptr;
}
if (close_pending_ && transactions_.IsEmpty())
CloseConnection();
}
void IDBDatabase::OnAbort(int64_t transaction_id, DOMException* error) {
DCHECK(transactions_.Contains(transaction_id));
transactions_.at(transaction_id)->OnAbort(error);
}
void IDBDatabase::OnComplete(int64_t transaction_id) {
DCHECK(transactions_.Contains(transaction_id));
transactions_.at(transaction_id)->OnComplete();
}
DOMStringList* IDBDatabase::objectStoreNames() const {
auto* object_store_names = MakeGarbageCollected<DOMStringList>();
for (const auto& it : metadata_.object_stores)
object_store_names->Append(it.value->name);
object_store_names->Sort();
return object_store_names;
}
const String& IDBDatabase::GetObjectStoreName(int64_t object_store_id) const {
const auto& it = metadata_.object_stores.find(object_store_id);
DCHECK(it != metadata_.object_stores.end());
return it->value->name;
}
IDBObjectStore* IDBDatabase::createObjectStore(
const String& name,
const IDBKeyPath& key_path,
bool auto_increment,
ExceptionState& exception_state) {
IDB_TRACE("IDBDatabase::createObjectStore");
if (!version_change_transaction_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
IDBDatabase::kNotVersionChangeTransactionErrorMessage);
return nullptr;
}
if (!version_change_transaction_->IsActive()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kTransactionInactiveError,
version_change_transaction_->InactiveErrorMessage());
return nullptr;
}
if (!key_path.IsNull() && !key_path.IsValid()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"The keyPath option is not a valid key path.");
return nullptr;
}
if (ContainsObjectStore(name)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kConstraintError,
IDBDatabase::kObjectStoreNameTakenErrorMessage);
return nullptr;
}
if (auto_increment && ((key_path.GetType() == mojom::IDBKeyPathType::String &&
key_path.GetString().IsEmpty()) ||
key_path.GetType() == mojom::IDBKeyPathType::Array)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidAccessError,
"The autoIncrement option was set but the "
"keyPath option was empty or an array.");
return nullptr;
}
if (!backend_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
IDBDatabase::kDatabaseClosedErrorMessage);
return nullptr;
}
int64_t object_store_id = metadata_.max_object_store_id + 1;
DCHECK_NE(object_store_id, IDBObjectStoreMetadata::kInvalidId);
version_change_transaction_->transaction_backend()->CreateObjectStore(
object_store_id, name, key_path, auto_increment);
scoped_refptr<IDBObjectStoreMetadata> store_metadata =
base::AdoptRef(new IDBObjectStoreMetadata(
name, object_store_id, key_path, auto_increment,
WebIDBDatabase::kMinimumIndexId));
auto* object_store = MakeGarbageCollected<IDBObjectStore>(
store_metadata, version_change_transaction_.Get());
version_change_transaction_->ObjectStoreCreated(name, object_store);
metadata_.object_stores.Set(object_store_id, std::move(store_metadata));
++metadata_.max_object_store_id;
return object_store;
}
void IDBDatabase::deleteObjectStore(const String& name,
ExceptionState& exception_state) {
IDB_TRACE("IDBDatabase::deleteObjectStore");
if (!version_change_transaction_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
IDBDatabase::kNotVersionChangeTransactionErrorMessage);
return;
}
if (!version_change_transaction_->IsActive()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kTransactionInactiveError,
version_change_transaction_->InactiveErrorMessage());
return;
}
int64_t object_store_id = FindObjectStoreId(name);
if (object_store_id == IDBObjectStoreMetadata::kInvalidId) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotFoundError,
"The specified object store was not found.");
return;
}
if (!backend_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
IDBDatabase::kDatabaseClosedErrorMessage);
return;
}
version_change_transaction_->transaction_backend()->DeleteObjectStore(
object_store_id);
version_change_transaction_->ObjectStoreDeleted(object_store_id, name);
metadata_.object_stores.erase(object_store_id);
}
IDBTransaction* IDBDatabase::transaction(
ScriptState* script_state,
const StringOrStringSequence& store_names,
const String& mode,
ExceptionState& exception_state) {
return transaction(script_state, store_names, mode, nullptr, exception_state);
}
IDBTransaction* IDBDatabase::transaction(
ScriptState* script_state,
const StringOrStringSequence& store_names,
const String& mode_string,
const IDBTransactionOptions* options,
ExceptionState& exception_state) {
IDB_TRACE("IDBDatabase::transaction");
HashSet<String> scope;
if (store_names.IsString()) {
scope.insert(store_names.GetAsString());
} else if (store_names.IsStringSequence()) {
for (const String& name : store_names.GetAsStringSequence())
scope.insert(name);
} else {
NOTREACHED();
}
if (version_change_transaction_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"A version change transaction is running.");
return nullptr;
}
if (close_pending_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"The database connection is closing.");
return nullptr;
}
if (!backend_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
IDBDatabase::kDatabaseClosedErrorMessage);
return nullptr;
}
if (scope.IsEmpty()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidAccessError,
"The storeNames parameter was empty.");
return nullptr;
}
Vector<int64_t> object_store_ids;
for (const String& name : scope) {
int64_t object_store_id = FindObjectStoreId(name);
if (object_store_id == IDBObjectStoreMetadata::kInvalidId) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotFoundError,
"One of the specified object stores was not found.");
return nullptr;
}
object_store_ids.push_back(object_store_id);
}
mojom::IDBTransactionMode mode = IDBTransaction::StringToMode(mode_string);
if (mode != mojom::IDBTransactionMode::ReadOnly &&
mode != mojom::IDBTransactionMode::ReadWrite) {
exception_state.ThrowTypeError(
"The mode provided ('" + mode_string +
"') is not one of 'readonly' or 'readwrite'.");
return nullptr;
}
// TODO(cmp): Delete |transaction_id| once all users are removed.
int64_t transaction_id = NextTransactionId();
auto transaction_backend = std::make_unique<WebIDBTransactionImpl>(
ExecutionContext::From(script_state)
->GetTaskRunner(TaskType::kDatabaseAccess),
transaction_id);
mojom::IDBTransactionDurability durability =
mojom::IDBTransactionDurability::Default;
if (options) {
DCHECK(RuntimeEnabledFeatures::IDBRelaxedDurabilityEnabled());
if (options->durability() == indexed_db_names::kRelaxed) {
durability = mojom::IDBTransactionDurability::Relaxed;
} else if (options->durability() == indexed_db_names::kStrict) {
durability = mojom::IDBTransactionDurability::Strict;
}
}
backend_->CreateTransaction(transaction_backend->CreateReceiver(),
transaction_id, object_store_ids, mode,
durability);
return IDBTransaction::CreateNonVersionChange(
script_state, std::move(transaction_backend), transaction_id, scope, mode,
durability, this);
}
void IDBDatabase::ForceClose() {
for (const auto& it : transactions_)
it.value->abort(IGNORE_EXCEPTION_FOR_TESTING);
this->close();
EnqueueEvent(Event::Create(event_type_names::kClose));
}
void IDBDatabase::close() {
IDB_TRACE("IDBDatabase::close");
if (close_pending_)
return;
connection_lifetime_.reset();
close_pending_ = true;
feature_handle_for_scheduler_.reset();
if (transactions_.IsEmpty())
CloseConnection();
}
void IDBDatabase::CloseConnection() {
DCHECK(close_pending_);
DCHECK(transactions_.IsEmpty());
if (backend_) {
backend_->Close();
backend_.reset();
}
if (database_callbacks_)
database_callbacks_->DetachWebCallbacks();
if (!GetExecutionContext())
return;
// Remove any pending versionchange events scheduled to fire on this
// connection. They would have been scheduled by the backend when another
// connection attempted an upgrade, but the frontend connection is being
// closed before they could fire.
event_queue_->CancelAllEvents();
}
void IDBDatabase::OnVersionChange(int64_t old_version, int64_t new_version) {
IDB_TRACE("IDBDatabase::onVersionChange");
if (!GetExecutionContext())
return;
if (close_pending_) {
// If we're pending, that means there's a busy transaction. We won't
// fire 'versionchange' but since we're not closing immediately the
// back-end should still send out 'blocked'.
backend_->VersionChangeIgnored();
return;
}
base::Optional<uint64_t> new_version_nullable;
if (new_version != IDBDatabaseMetadata::kNoVersion) {
new_version_nullable = new_version;
}
EnqueueEvent(MakeGarbageCollected<IDBVersionChangeEvent>(
event_type_names::kVersionchange, old_version, new_version_nullable));
}
void IDBDatabase::EnqueueEvent(Event* event) {
DCHECK(GetExecutionContext());
event->SetTarget(this);
event_queue_->EnqueueEvent(FROM_HERE, *event);
}
DispatchEventResult IDBDatabase::DispatchEventInternal(Event& event) {
IDB_TRACE("IDBDatabase::dispatchEvent");
event.SetTarget(this);
// If this event originated from script, it should have no side effects.
if (!event.isTrusted())
return EventTarget::DispatchEventInternal(event);
DCHECK(event.type() == event_type_names::kVersionchange ||
event.type() == event_type_names::kClose);
if (!GetExecutionContext())
return DispatchEventResult::kCanceledBeforeDispatch;
DispatchEventResult dispatch_result =
EventTarget::DispatchEventInternal(event);
if (event.type() == event_type_names::kVersionchange && !close_pending_ &&
backend_)
backend_->VersionChangeIgnored();
return dispatch_result;
}
int64_t IDBDatabase::FindObjectStoreId(const String& name) const {
for (const auto& it : metadata_.object_stores) {
if (it.value->name == name) {
DCHECK_NE(it.key, IDBObjectStoreMetadata::kInvalidId);
return it.key;
}
}
return IDBObjectStoreMetadata::kInvalidId;
}
void IDBDatabase::RenameObjectStore(int64_t object_store_id,
const String& new_name) {
DCHECK(version_change_transaction_)
<< "Object store renamed on database without a versionchange transaction";
DCHECK(version_change_transaction_->IsActive())
<< "Object store renamed when versionchange transaction is not active";
DCHECK(backend_) << "Object store renamed after database connection closed";
DCHECK(metadata_.object_stores.Contains(object_store_id));
backend_->RenameObjectStore(version_change_transaction_->Id(),
object_store_id, new_name);
IDBObjectStoreMetadata* object_store_metadata =
metadata_.object_stores.at(object_store_id);
version_change_transaction_->ObjectStoreRenamed(object_store_metadata->name,
new_name);
object_store_metadata->name = new_name;
}
void IDBDatabase::RevertObjectStoreCreation(int64_t object_store_id) {
DCHECK(version_change_transaction_) << "Object store metadata reverted on "
"database without a versionchange "
"transaction";
DCHECK(!version_change_transaction_->IsActive())
<< "Object store metadata reverted when versionchange transaction is "
"still active";
DCHECK(metadata_.object_stores.Contains(object_store_id));
metadata_.object_stores.erase(object_store_id);
}
void IDBDatabase::RevertObjectStoreMetadata(
scoped_refptr<IDBObjectStoreMetadata> old_metadata) {
DCHECK(version_change_transaction_) << "Object store metadata reverted on "
"database without a versionchange "
"transaction";
DCHECK(!version_change_transaction_->IsActive())
<< "Object store metadata reverted when versionchange transaction is "
"still active";
DCHECK(old_metadata.get());
metadata_.object_stores.Set(old_metadata->id, std::move(old_metadata));
}
bool IDBDatabase::HasPendingActivity() const {
// The script wrapper must not be collected before the object is closed or
// we can't fire a "versionchange" event to let script manually close the
// connection.
return !close_pending_ && GetExecutionContext() && HasEventListeners();
}
void IDBDatabase::ContextDestroyed() {
// Immediately close the connection to the back end. Don't attempt a
// normal close() since that may wait on transactions which require a
// round trip to the back-end to abort.
if (backend_) {
backend_->Close();
backend_.reset();
}
connection_lifetime_.reset();
if (database_callbacks_)
database_callbacks_->DetachWebCallbacks();
}
const AtomicString& IDBDatabase::InterfaceName() const {
return event_target_names::kIDBDatabase;
}
ExecutionContext* IDBDatabase::GetExecutionContext() const {
return ExecutionContextLifecycleObserver::GetExecutionContext();
}
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kNoError,
DOMExceptionCode::kNoError);
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kUnknownError,
DOMExceptionCode::kUnknownError);
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kConstraintError,
DOMExceptionCode::kConstraintError);
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kDataError,
DOMExceptionCode::kDataError);
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kVersionError,
DOMExceptionCode::kVersionError);
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kAbortError,
DOMExceptionCode::kAbortError);
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kQuotaError,
DOMExceptionCode::kQuotaExceededError);
STATIC_ASSERT_ENUM(mojom::blink::IDBException::kTimeoutError,
DOMExceptionCode::kTimeoutError);
} // namespace blink