From 504e95b43b0799ad83746bf16e82b0bd492a8592 Mon Sep 17 00:00:00 2001 From: Jan Varga Date: Mon, 25 Mar 2024 10:54:33 +0000 Subject: [PATCH] Bug 934640 - Implement IDBFactory.databases() to enumerate IndexedDB databases; r=dom-storage-reviewers,asuth Differential Revision: https://phabricator.services.mozilla.com/D190053 --- dom/indexedDB/ActorsParent.cpp | 317 +++++++++++++++++- dom/indexedDB/IDBFactory.cpp | 111 ++++-- dom/indexedDB/IDBFactory.h | 4 + dom/indexedDB/PBackgroundIDBFactory.ipdl | 10 + .../database-names-by-origin.html.ini | 13 - .../meta/IndexedDB/get-databases.any.js.ini | 20 -- .../idbfactory-origin-isolation.html.ini | 2 - 7 files changed, 401 insertions(+), 76 deletions(-) delete mode 100644 testing/web-platform/meta/IndexedDB/database-names-by-origin.html.ini diff --git a/dom/indexedDB/ActorsParent.cpp b/dom/indexedDB/ActorsParent.cpp index 1624434aed11..d288b52aec5d 100644 --- a/dom/indexedDB/ActorsParent.cpp +++ b/dom/indexedDB/ActorsParent.cpp @@ -94,6 +94,7 @@ #include "mozilla/dom/FileBlobImpl.h" #include "mozilla/dom/FlippedOnce.h" #include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/IDBFactory.h" #include "mozilla/dom/IPCBlob.h" #include "mozilla/dom/IPCBlobUtils.h" #include "mozilla/dom/IndexedDatabase.h" @@ -2147,6 +2148,14 @@ class Factory final : public PBackgroundIDBFactoryParent, bool DeallocPBackgroundIDBFactoryRequestParent( PBackgroundIDBFactoryRequestParent* aActor) override; + + mozilla::ipc::IPCResult RecvGetDatabases( + const PersistenceType& aPersistenceType, + const PrincipalInfo& aPrincipalInfo, + GetDatabasesResolver&& aResolve) override; + + private: + Maybe GetContentParentId() const; }; class WaitForTransactionsHelper final : public Runnable { @@ -3354,6 +3363,40 @@ class DeleteDatabaseOp::VersionChangeOp final : public DatabaseOperationBase { NS_DECL_NSIRUNNABLE }; +class GetDatabasesOp final : public FactoryOp { + nsTArray mDatabaseMetadataArray; + Factory::GetDatabasesResolver mResolver; + + public: + GetDatabasesOp(SafeRefPtr aFactory, + const Maybe& aContentParentId, + const PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo, + Factory::GetDatabasesResolver&& aResolver) + : FactoryOp(std::move(aFactory), aContentParentId, aPersistenceType, + aPrincipalInfo, Nothing(), /* aDeleting */ false), + mResolver(std::move(aResolver)) {} + + private: + ~GetDatabasesOp() override = default; + + nsresult DatabasesNotAvailable(); + + nsresult DatabaseOpen() override; + + nsresult DoDatabaseWork() override; + + nsresult BeginVersionChange() override; + + bool AreActorsAlive() override; + + void SendBlockedNotification() override; + + nsresult DispatchToWorkThread() override; + + void SendResults() override; +}; + class VersionChangeTransactionOp : public TransactionDatabaseOperationBase { public: void Cleanup() override; @@ -4732,6 +4775,8 @@ class DatabaseLoggingInfo final { }; class QuotaClient final : public mozilla::dom::quota::Client { + friend class GetDatabasesOp; + static QuotaClient* sInstance; nsCOMPtr mBackgroundThread; @@ -4865,8 +4910,9 @@ class QuotaClient final : public mozilla::dom::quota::Client { // checks those unfinished deletion and clean them up after that. template - Result, nsresult> - GetDatabaseFilenames(nsIFile& aDirectory, const AtomicBool& aCanceled); + Result, + nsresult> static GetDatabaseFilenames(nsIFile& aDirectory, + const AtomicBool& aCanceled); nsresult GetUsageForOriginInternal(PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, @@ -9172,18 +9218,7 @@ Factory::AllocPBackgroundIDBFactoryRequestParent( return nullptr; } - Maybe contentParentId; - - uint64_t childID = BackgroundParent::GetChildID(Manager()); - if (childID) { - // If childID is not zero we are dealing with an other-process actor. We - // want to initialize OpenDatabaseOp/DeleteDatabaseOp here with the ID - // (and later also Database) in that case, so Database::IsOwnedByProcess - // can find Databases belonging to a particular content process when - // QuotaClient::AbortOperationsForProcess is called which is currently used - // to abort operations for content processes only. - contentParentId = Some(ContentParentId(childID)); - } + Maybe contentParentId = GetContentParentId(); auto actor = [&]() -> RefPtr { if (aParams.type() == FactoryRequestParams::TOpenDatabaseRequestParams) { @@ -9229,6 +9264,64 @@ bool Factory::DeallocPBackgroundIDBFactoryRequestParent( return true; } +mozilla::ipc::IPCResult Factory::RecvGetDatabases( + const PersistenceType& aPersistenceType, + const PrincipalInfo& aPrincipalInfo, GetDatabasesResolver&& aResolve) { + AssertIsOnBackgroundThread(); + + auto ResolveGetDatabasesAndReturn = [&aResolve](const nsresult rv) { + aResolve(rv); + return IPC_OK(); + }; + + QM_TRY(MOZ_TO_RESULT(!QuotaClient::IsShuttingDownOnBackgroundThread()), + ResolveGetDatabasesAndReturn); + + QM_TRY(MOZ_TO_RESULT(IsValidPersistenceType(aPersistenceType)), + QM_IPC_FAIL(this)); + + QM_TRY(MOZ_TO_RESULT(QuotaManager::IsPrincipalInfoValid(aPrincipalInfo)), + QM_IPC_FAIL(this)); + + MOZ_ASSERT(aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || + aPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + PersistenceType persistenceType = + IDBFactory::GetPersistenceType(aPrincipalInfo); + + QM_TRY(MOZ_TO_RESULT(aPersistenceType == persistenceType), QM_IPC_FAIL(this)); + + Maybe contentParentId = GetContentParentId(); + + auto op = MakeRefPtr(SafeRefPtrFromThis(), contentParentId, + aPersistenceType, aPrincipalInfo, + std::move(aResolve)); + + gFactoryOps->AppendElement(op); + + // Balanced in CleanupMetadata() which is/must always called by SendResults(). + IncreaseBusyCount(); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(op)); + + return IPC_OK(); +} + +Maybe Factory::GetContentParentId() const { + uint64_t childID = BackgroundParent::GetChildID(Manager()); + if (childID) { + // If childID is not zero we are dealing with an other-process actor. We + // want to initialize OpenDatabaseOp/DeleteDatabaseOp here with the ID + // (and later also Database) in that case, so Database::IsOwnedByProcess + // can find Databases belonging to a particular content process when + // QuotaClient::AbortOperationsForProcess is called which is currently used + // to abort operations for content processes only. + return Some(ContentParentId(childID)); + } + + return Nothing(); +} + /******************************************************************************* * WaitForTransactionsHelper ******************************************************************************/ @@ -16626,6 +16719,202 @@ nsresult DeleteDatabaseOp::VersionChangeOp::Run() { return NS_OK; } +nsresult GetDatabasesOp::DatabasesNotAvailable() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + + mState = State::SendingResults; + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +nsresult GetDatabasesOp::DatabaseOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseOpenPending); + + nsresult rv = SendToIOThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult GetDatabasesOp::DoDatabaseWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + + AUTO_PROFILER_LABEL("GetDatabasesOp::DoDatabaseWork", DOM); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + if (mPersistenceType != PERSISTENCE_TYPE_PERSISTENT) { + QM_TRY(MOZ_TO_RESULT( + quotaManager->EnsureTemporaryStorageIsInitializedInternal())); + } + + { + QM_TRY_INSPECT(const bool& exists, + quotaManager->DoesOriginDirectoryExist(mOriginMetadata)); + if (!exists) { + return DatabasesNotAvailable(); + } + } + + QM_TRY((["aManager, this]() + -> mozilla::Result, bool>, nsresult> { + if (mPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + QM_TRY_RETURN( + quotaManager->EnsurePersistentOriginIsInitialized(mOriginMetadata)); + } + + QM_TRY_RETURN(quotaManager->EnsureTemporaryOriginIsInitialized( + mPersistenceType, mOriginMetadata)); + }() + .map([](const auto& res) { return Ok{}; }))); + + { + QM_TRY_INSPECT(const bool& exists, + quotaManager->DoesClientDirectoryExist( + ClientMetadata{mOriginMetadata, Client::IDB})); + if (!exists) { + return DatabasesNotAvailable(); + } + } + + QM_TRY_INSPECT( + const auto& clientDirectory, + (["aManager, this]() + -> mozilla::Result, bool>, nsresult> { + if (mPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + QM_TRY_RETURN(quotaManager->EnsurePersistentClientIsInitialized( + ClientMetadata{mOriginMetadata, Client::IDB})); + } + + QM_TRY_RETURN(quotaManager->EnsureTemporaryClientIsInitialized( + ClientMetadata{mOriginMetadata, Client::IDB})); + }() + .map([](const auto& res) { return res.first; }))); + + QM_TRY_INSPECT( + (const auto& [subdirsToProcess, databaseFilenames]), + QuotaClient::GetDatabaseFilenames(*clientDirectory, + /* aCanceled */ Atomic{false})); + + for (const auto& databaseFilename : databaseFilenames) { + QM_TRY_INSPECT( + const auto& databaseFile, + CloneFileAndAppend(*clientDirectory, databaseFilename + kSQLiteSuffix)); + + nsString path; + databaseFile->GetPath(path); + + IndexedDatabaseManager* const idm = IndexedDatabaseManager::Get(); + MOZ_ASSERT(idm); + + // If the database is already open then there will be a DatabaseFileManager + // which can provide us with the database name and version without needing + // to open the SQLite database. (Also, we are not allowed to open the + // database on this thread if it's already open.) + + SafeRefPtr fileManager = + idm->GetFileManagerByDatabaseFilePath(mPersistenceType, + mOriginMetadata.mOrigin, path); + + if (fileManager) { + mDatabaseMetadataArray.AppendElement( + DatabaseMetadata(nsString(fileManager->DatabaseName()), + fileManager->DatabaseVersion(), mPersistenceType)); + continue; + } + + // Since the database is not already open, it is safe and necessary for us + // to open the database on this thread and retrieve its name and version. + // We do not need to worry about racing a database open because database + // opens can only be processed on this thread and we are performing the + // steps below synchronously. + + QM_TRY_INSPECT( + const auto& fmDirectory, + CloneFileAndAppend(*clientDirectory, + databaseFilename + kFileManagerDirectoryNameSuffix)); + + QM_TRY_UNWRAP( + const NotNull> connection, + CreateStorageConnection(*databaseFile, *fmDirectory, VoidString(), + mOriginMetadata.mOrigin, mDirectoryLockId, + TelemetryIdForFile(databaseFile), Nothing{})); + + { + // Load version information. + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *connection, "SELECT name, version FROM database"_ns)); + + QM_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED); + + QM_TRY_INSPECT( + const auto& databaseName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, stmt, GetString, 0)); + + QM_TRY_INSPECT(const int64_t& version, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 1)); + + mDatabaseMetadataArray.AppendElement( + DatabaseMetadata(databaseName, version, mPersistenceType)); + } + } + + mState = State::SendingResults; + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +nsresult GetDatabasesOp::BeginVersionChange() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +bool GetDatabasesOp::AreActorsAlive() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +void GetDatabasesOp::SendBlockedNotification() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +nsresult GetDatabasesOp::DispatchToWorkThread() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +void GetDatabasesOp::SendResults() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + mResolver(mDatabaseMetadataArray); + + mDirectoryLock = nullptr; + + CleanupMetadata(); + + FinishSendResults(); +} + TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( SafeRefPtr aTransaction, const int64_t aRequestId) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), diff --git a/dom/indexedDB/IDBFactory.cpp b/dom/indexedDB/IDBFactory.cpp index bdc70cfb20e7..51d4c4df239d 100644 --- a/dom/indexedDB/IDBFactory.cpp +++ b/dom/indexedDB/IDBFactory.cpp @@ -16,8 +16,8 @@ #include "mozilla/dom/Document.h" #include "mozilla/dom/IDBFactoryBinding.h" #include "mozilla/dom/Promise.h" -#include "mozilla/dom/quota/PersistenceType.h" #include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/dom/BrowserChild.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/ipc/BackgroundChild.h" @@ -53,30 +53,6 @@ using namespace mozilla::ipc; namespace { -PersistenceType GetPersistenceType(const PrincipalInfo& aPrincipalInfo) { - if (aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { - // Chrome privilege always gets persistent storage. - return PERSISTENCE_TYPE_PERSISTENT; - } - - if (aPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { - nsCString origin = - aPrincipalInfo.get_ContentPrincipalInfo().originNoSuffix(); - - if (QuotaManager::IsOriginInternal(origin)) { - // Internal origins always get persistent storage. - return PERSISTENCE_TYPE_PERSISTENT; - } - - if (aPrincipalInfo.get_ContentPrincipalInfo().attrs().mPrivateBrowsingId > - 0) { - return PERSISTENCE_TYPE_PRIVATE; - } - } - - return PERSISTENCE_TYPE_DEFAULT; -} - Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT IdentifyPrincipalType( const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { switch (aPrincipalInfo.type()) { @@ -403,6 +379,32 @@ bool IDBFactory::AllowedForPrincipal(nsIPrincipal* aPrincipal, return !aPrincipal->GetIsNullPrincipal(); } +// static +PersistenceType IDBFactory::GetPersistenceType( + const PrincipalInfo& aPrincipalInfo) { + if (aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + // Chrome privilege always gets persistent storage. + return PERSISTENCE_TYPE_PERSISTENT; + } + + if (aPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { + nsCString origin = + aPrincipalInfo.get_ContentPrincipalInfo().originNoSuffix(); + + if (QuotaManager::IsOriginInternal(origin)) { + // Internal origins always get persistent storage. + return PERSISTENCE_TYPE_PERSISTENT; + } + + if (aPrincipalInfo.get_ContentPrincipalInfo().attrs().mPrivateBrowsingId > + 0) { + return PERSISTENCE_TYPE_PRIVATE; + } + } + + return PERSISTENCE_TYPE_DEFAULT; +} + void IDBFactory::UpdateActiveTransactionCount(int32_t aDelta) { AssertIsOnOwningThread(); MOZ_DIAGNOSTIC_ASSERT(aDelta > 0 || (mActiveTransactionCount + aDelta) < @@ -471,9 +473,64 @@ RefPtr IDBFactory::DeleteDatabase( already_AddRefed IDBFactory::Databases(JSContext* const aCx) { RefPtr promise = Promise::CreateInfallible(GetOwnerGlobal()); - Sequence databaseInfos; + // Nothing can be done here if we have previously failed to create a + // background actor. + if (mBackgroundActorFailed) { + promise->MaybeReject(NS_ERROR_FAILURE); + return promise.forget(); + } - promise->MaybeResolve(databaseInfos); + PersistenceType persistenceType = GetPersistenceType(*mPrincipalInfo); + + QM_TRY(MOZ_TO_RESULT(EnsureBackgroundActor()), [&promise](const nsresult rv) { + promise->MaybeReject(rv); + return promise.forget(); + }); + + mBackgroundActor->SendGetDatabases(persistenceType, *mPrincipalInfo) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](const PBackgroundIDBFactoryChild::GetDatabasesPromise:: + ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + promise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + const GetDatabasesResponse& response = aValue.ResolveValue(); + + switch (response.type()) { + case GetDatabasesResponse::Tnsresult: + promise->MaybeReject(response.get_nsresult()); + + break; + + case GetDatabasesResponse::TArrayOfDatabaseMetadata: { + const auto& array = response.get_ArrayOfDatabaseMetadata(); + + Sequence databaseInfos; + + for (const auto& databaseMetadata : array) { + IDBDatabaseInfo databaseInfo; + + databaseInfo.mName.Construct(databaseMetadata.name()); + databaseInfo.mVersion.Construct(databaseMetadata.version()); + + if (!databaseInfos.AppendElement(std::move(databaseInfo), + fallible)) { + promise->MaybeRejectWithTypeError("Out of memory"); + return; + } + } + + promise->MaybeResolve(databaseInfos); + + break; + } + default: + MOZ_CRASH("Unknown response type!"); + } + }); return promise.forget(); } diff --git a/dom/indexedDB/IDBFactory.h b/dom/indexedDB/IDBFactory.h index 40f28a9d345a..7139b26f9c9d 100644 --- a/dom/indexedDB/IDBFactory.h +++ b/dom/indexedDB/IDBFactory.h @@ -9,6 +9,7 @@ #include "mozilla/Attributes.h" #include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/quota/PersistenceType.h" #include "mozilla/GlobalTeardownObserver.h" #include "mozilla/UniquePtr.h" #include "nsCOMPtr.h" @@ -97,6 +98,9 @@ class IDBFactory final : public GlobalTeardownObserver, public nsWrapperCache { static bool AllowedForPrincipal(nsIPrincipal* aPrincipal, bool* aIsSystemPrincipal = nullptr); + static quota::PersistenceType GetPersistenceType( + const PrincipalInfo& aPrincipalInfo); + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(IDBFactory); } nsISerialEventTarget* EventTarget() const { diff --git a/dom/indexedDB/PBackgroundIDBFactory.ipdl b/dom/indexedDB/PBackgroundIDBFactory.ipdl index ef2a88e55bc9..c838189b2ba2 100644 --- a/dom/indexedDB/PBackgroundIDBFactory.ipdl +++ b/dom/indexedDB/PBackgroundIDBFactory.ipdl @@ -40,6 +40,12 @@ union FactoryRequestParams DeleteDatabaseRequestParams; }; +union GetDatabasesResponse +{ + nsresult; + DatabaseMetadata[]; +}; + [ChildImpl="indexedDB::BackgroundFactoryChild", ParentImpl=virtual] sync protocol PBackgroundIDBFactory { @@ -53,6 +59,10 @@ parent: async PBackgroundIDBFactoryRequest(FactoryRequestParams params); + async GetDatabases(PersistenceType persistenceType, + PrincipalInfo principalInfo) + returns(GetDatabasesResponse response); + child: async __delete__(); diff --git a/testing/web-platform/meta/IndexedDB/database-names-by-origin.html.ini b/testing/web-platform/meta/IndexedDB/database-names-by-origin.html.ini deleted file mode 100644 index 84b4a7992208..000000000000 --- a/testing/web-platform/meta/IndexedDB/database-names-by-origin.html.ini +++ /dev/null @@ -1,13 +0,0 @@ -[database-names-by-origin.html] - expected: OK - [open database names don't leak to cross-origin iframe] - expected: FAIL - - [open database names don't leak to cross-origin window] - expected: FAIL - - [closed database names don't leak to cross-origin iframe] - expected: FAIL - - [closed database names don't leak to cross-origin window] - expected: FAIL diff --git a/testing/web-platform/meta/IndexedDB/get-databases.any.js.ini b/testing/web-platform/meta/IndexedDB/get-databases.any.js.ini index 0550e5413881..067b297e3dea 100644 --- a/testing/web-platform/meta/IndexedDB/get-databases.any.js.ini +++ b/testing/web-platform/meta/IndexedDB/get-databases.any.js.ini @@ -1,16 +1,6 @@ [get-databases.any.html] expected: if (processor == "x86") and not debug: [OK, TIMEOUT] - [Enumerate multiple databases.] - expected: FAIL - - [Enumerate one database.] - expected: FAIL - - [Make sure an empty list is returned for the case of no databases.] - expected: - if (processor == "x86") and not debug: [FAIL, TIMEOUT] - PASS [Ensure that databases() doesn't pick up changes that haven't commited.] expected: @@ -21,16 +11,6 @@ [get-databases.any.worker.html] expected: if (processor == "x86") and not debug: [OK, TIMEOUT] - [Enumerate multiple databases.] - expected: FAIL - - [Enumerate one database.] - expected: FAIL - - [Make sure an empty list is returned for the case of no databases.] - expected: - if (processor == "x86") and not debug: [FAIL, TIMEOUT] - PASS [Ensure that databases() doesn't pick up changes that haven't commited.] expected: diff --git a/testing/web-platform/meta/IndexedDB/idbfactory-origin-isolation.html.ini b/testing/web-platform/meta/IndexedDB/idbfactory-origin-isolation.html.ini index 8b00a7f7c3cb..ca60986e0b85 100644 --- a/testing/web-platform/meta/IndexedDB/idbfactory-origin-isolation.html.ini +++ b/testing/web-platform/meta/IndexedDB/idbfactory-origin-isolation.html.ini @@ -1,5 +1,3 @@ [idbfactory-origin-isolation.html] expected: if (os == "android") and fission: [TIMEOUT, OK] - [Test to make sure that origins have separate locking schemes] - expected: FAIL