/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "LocalStorageManager2.h" // Local includes #include "ActorsChild.h" #include "LSObject.h" // Global includes #include #include "MainThreadUtils.h" #include "jsapi.h" #include "mozilla/Assertions.h" #include "mozilla/ErrorResult.h" #include "mozilla/MacroForEach.h" #include "mozilla/OriginAttributes.h" #include "mozilla/RefPtr.h" #include "mozilla/RemoteLazyInputStreamThread.h" #include "mozilla/dom/LocalStorageCommon.h" #include "mozilla/dom/PBackgroundLSRequest.h" #include "mozilla/dom/PBackgroundLSSharedTypes.h" #include "mozilla/dom/PBackgroundLSSimpleRequest.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "nsCOMPtr.h" #include "nsDebug.h" #include "nsError.h" #include "nsIEventTarget.h" #include "nsILocalStorageManager.h" #include "nsIPrincipal.h" #include "nsIRunnable.h" #include "nsPIDOMWindow.h" #include "nsStringFwd.h" #include "nsThreadUtils.h" #include "nscore.h" #include "xpcpublic.h" namespace mozilla::dom { namespace { class AsyncRequestHelper final : public Runnable, public LSRequestChildCallback { enum class State { /** * The AsyncRequestHelper has been created and dispatched to the * RemoteLazyInputStream Thread. */ Initial, /** * Start() has been invoked on the RemoteLazyInputStream Thread and * LocalStorageManager2::StartRequest has been invoked from there, sending * an IPC message to PBackground to service the request. We stay in this * state until a response is received. */ ResponsePending, /** * A response has been received and AsyncRequestHelper has been dispatched * back to the owning event target to call Finish(). */ Finishing, /** * Finish() has been called on the main thread. The promise will be resolved * according to the received response. */ Complete }; // The object we are issuing a request on behalf of. Present because of the // need to invoke LocalStorageManager2::StartRequest off the main thread. // Dropped on return to the main-thread in Finish(). RefPtr mManager; // The thread the AsyncRequestHelper was created on. This should be the main // thread. nsCOMPtr mOwningEventTarget; // The IPC actor handling the request with standard IPC allocation rules. // Our reference is nulled in OnResponse which corresponds to the actor's // __destroy__ method. LSRequestChild* mActor; RefPtr mPromise; const LSRequestParams mParams; LSRequestResponse mResponse; nsresult mResultCode; State mState; public: AsyncRequestHelper(LocalStorageManager2* aManager, Promise* aPromise, const LSRequestParams& aParams) : Runnable("dom::LocalStorageManager2::AsyncRequestHelper"), mManager(aManager), mOwningEventTarget(GetCurrentSerialEventTarget()), mActor(nullptr), mPromise(aPromise), mParams(aParams), mResultCode(NS_OK), mState(State::Initial) {} bool IsOnOwningThread() const { MOZ_ASSERT(mOwningEventTarget); bool current; return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && current; } void AssertIsOnOwningThread() const { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(IsOnOwningThread()); } nsresult Dispatch(); private: ~AsyncRequestHelper() = default; nsresult Start(); void Finish(); NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSIRUNNABLE // LSRequestChildCallback void OnResponse(const LSRequestResponse& aResponse) override; }; class SimpleRequestResolver final : public LSSimpleRequestChildCallback { RefPtr mPromise; public: explicit SimpleRequestResolver(Promise* aPromise) : mPromise(aPromise) {} NS_INLINE_DECL_REFCOUNTING(SimpleRequestResolver, override); private: ~SimpleRequestResolver() = default; void HandleResponse(nsresult aResponse); void HandleResponse(bool aResponse); void HandleResponse(const nsTArray& aResponse); // LSRequestChildCallback void OnResponse(const LSSimpleRequestResponse& aResponse) override; }; nsresult CreatePromise(JSContext* aContext, Promise** aPromise) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aContext); nsIGlobalObject* global = xpc::NativeGlobal(JS::CurrentGlobalOrNull(aContext)); if (NS_WARN_IF(!global)) { return NS_ERROR_FAILURE; } ErrorResult result; RefPtr promise = Promise::Create(global, result); if (result.Failed()) { return result.StealNSResult(); } promise.forget(aPromise); return NS_OK; } nsresult CheckedPrincipalToPrincipalInfo( nsIPrincipal* aPrincipal, mozilla::ipc::PrincipalInfo& aPrincipalInfo) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aPrincipalInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (NS_WARN_IF(!quota::QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) { return NS_ERROR_FAILURE; } if (aPrincipalInfo.type() != mozilla::ipc::PrincipalInfo::TContentPrincipalInfo && aPrincipalInfo.type() != mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo) { return NS_ERROR_UNEXPECTED; } return NS_OK; } } // namespace LocalStorageManager2::LocalStorageManager2() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NextGenLocalStorageEnabled()); } LocalStorageManager2::~LocalStorageManager2() { MOZ_ASSERT(NS_IsMainThread()); } NS_IMPL_ISUPPORTS(LocalStorageManager2, nsIDOMStorageManager, nsILocalStorageManager) NS_IMETHODIMP LocalStorageManager2::PrecacheStorage(nsIPrincipal* aPrincipal, nsIPrincipal* aStoragePrincipal, Storage** _retval) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aStoragePrincipal); MOZ_ASSERT(_retval); // This method was created as part of the e10s-ification of the old LS // implementation to perform a preload in the content/current process. That's // not how things work in LSNG. Instead everything happens in the parent // process, triggered by the official preloading spot, // ContentParent::AboutToLoadHttpDocumentForChild. return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP LocalStorageManager2::CreateStorage(mozIDOMWindow* aWindow, nsIPrincipal* aPrincipal, nsIPrincipal* aStoragePrincipal, const nsAString& aDocumentURI, bool aPrivate, Storage** _retval) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aStoragePrincipal); MOZ_ASSERT(_retval); nsCOMPtr inner = nsPIDOMWindowInner::From(aWindow); RefPtr object; nsresult rv = LSObject::CreateForPrincipal(inner, aPrincipal, aStoragePrincipal, aDocumentURI, aPrivate, getter_AddRefs(object)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } object.forget(_retval); return NS_OK; } NS_IMETHODIMP LocalStorageManager2::GetStorage(mozIDOMWindow* aWindow, nsIPrincipal* aPrincipal, nsIPrincipal* aStoragePrincipal, bool aPrivate, Storage** _retval) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aStoragePrincipal); MOZ_ASSERT(_retval); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP LocalStorageManager2::CloneStorage(Storage* aStorageToCloneFrom) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aStorageToCloneFrom); // Cloning is specific to sessionStorage; state is forked when a new tab is // opened from an existing tab. return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP LocalStorageManager2::CheckStorage(nsIPrincipal* aPrincipal, Storage* aStorage, bool* _retval) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aStorage); MOZ_ASSERT(_retval); // Only used by sessionStorage. return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP LocalStorageManager2::GetNextGenLocalStorageEnabled(bool* aResult) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aResult); *aResult = NextGenLocalStorageEnabled(); return NS_OK; } NS_IMETHODIMP LocalStorageManager2::Preload(nsIPrincipal* aPrincipal, JSContext* aContext, Promise** _retval) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(_retval); nsCString originAttrSuffix; nsCString originKey; nsresult rv = aPrincipal->GetStorageOriginKey(originKey); aPrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); if (NS_FAILED(rv)) { return NS_ERROR_NOT_AVAILABLE; } mozilla::ipc::PrincipalInfo principalInfo; rv = CheckedPrincipalToPrincipalInfo(aPrincipal, principalInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } RefPtr promise; if (aContext) { rv = CreatePromise(aContext, getter_AddRefs(promise)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } LSRequestCommonParams commonParams; commonParams.principalInfo() = principalInfo; commonParams.storagePrincipalInfo() = principalInfo; commonParams.originKey() = originKey; LSRequestPreloadDatastoreParams params(commonParams); RefPtr helper = new AsyncRequestHelper(this, promise, params); // This will start and finish the async request on the RemoteLazyInputStream // thread. // This must be done on RemoteLazyInputStream Thread because it's very likely // that a content process will issue a prepare datastore request for the same // principal while blocking the content process on the main thread. // There would be a potential for deadlock if the preloading was initialized // from the main thread of the parent process and a11y issued a synchronous // message from the parent process to the content process (approximately at // the same time) because the preload request wouldn't be able to respond // to the Ready message by sending the Finish message which is needed to // finish the preload request and unblock the prepare datastore request. rv = helper->Dispatch(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } promise.forget(_retval); return NS_OK; } NS_IMETHODIMP LocalStorageManager2::IsPreloaded(nsIPrincipal* aPrincipal, JSContext* aContext, Promise** _retval) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(_retval); RefPtr promise; nsresult rv = CreatePromise(aContext, getter_AddRefs(promise)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } LSSimpleRequestPreloadedParams params; rv = CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } params.storagePrincipalInfo() = params.principalInfo(); rv = StartSimpleRequest(promise, params); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } promise.forget(_retval); return NS_OK; } NS_IMETHODIMP LocalStorageManager2::GetState(nsIPrincipal* aPrincipal, JSContext* aContext, Promise** _retval) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(_retval); RefPtr promise; nsresult rv = CreatePromise(aContext, getter_AddRefs(promise)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } LSSimpleRequestGetStateParams params; rv = CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } params.storagePrincipalInfo() = params.principalInfo(); rv = StartSimpleRequest(promise, params); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } promise.forget(_retval); return NS_OK; } LSRequestChild* LocalStorageManager2::StartRequest( const LSRequestParams& aParams, LSRequestChildCallback* aCallback) { AssertIsOnDOMFileThread(); mozilla::ipc::PBackgroundChild* backgroundActor = mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!backgroundActor)) { return nullptr; } auto actor = new LSRequestChild(); if (!backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams)) { return nullptr; } // Must set callback after calling SendPBackgroundLSRequestConstructor since // it can be called synchronously when SendPBackgroundLSRequestConstructor // fails. actor->SetCallback(aCallback); return actor; } nsresult LocalStorageManager2::StartSimpleRequest( Promise* aPromise, const LSSimpleRequestParams& aParams) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPromise); mozilla::ipc::PBackgroundChild* backgroundActor = mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!backgroundActor)) { return NS_ERROR_FAILURE; } auto actor = new LSSimpleRequestChild(); if (!backgroundActor->SendPBackgroundLSSimpleRequestConstructor(actor, aParams)) { return NS_ERROR_FAILURE; } RefPtr resolver = new SimpleRequestResolver(aPromise); // Must set callback after calling SendPBackgroundLSRequestConstructor since // it can be called synchronously when SendPBackgroundLSRequestConstructor // fails. actor->SetCallback(resolver); return NS_OK; } nsresult AsyncRequestHelper::Dispatch() { AssertIsOnOwningThread(); nsCOMPtr domFileThread = RemoteLazyInputStreamThread::GetOrCreate(); if (NS_WARN_IF(!domFileThread)) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } nsresult rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult AsyncRequestHelper::Start() { AssertIsOnDOMFileThread(); MOZ_ASSERT(mState == State::Initial); mState = State::ResponsePending; LSRequestChild* actor = mManager->StartRequest(mParams, this); if (NS_WARN_IF(!actor)) { return NS_ERROR_FAILURE; } mActor = actor; return NS_OK; } void AsyncRequestHelper::Finish() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::Finishing); if (NS_WARN_IF(NS_FAILED(mResultCode))) { if (mPromise) { mPromise->MaybeReject(mResultCode); } } else { switch (mResponse.type()) { case LSRequestResponse::Tnsresult: if (mPromise) { mPromise->MaybeReject(mResponse.get_nsresult()); } break; case LSRequestResponse::TLSRequestPreloadDatastoreResponse: if (mPromise) { mPromise->MaybeResolveWithUndefined(); } break; default: MOZ_CRASH("Unknown response type!"); } } mManager = nullptr; mState = State::Complete; } NS_IMPL_ISUPPORTS_INHERITED0(AsyncRequestHelper, Runnable) NS_IMETHODIMP AsyncRequestHelper::Run() { nsresult rv; switch (mState) { case State::Initial: rv = Start(); break; case State::Finishing: Finish(); return NS_OK; default: MOZ_CRASH("Bad state!"); } if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::Finishing) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } mState = State::Finishing; if (IsOnOwningThread()) { Finish(); } else { MOZ_ALWAYS_SUCCEEDS( mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); } } return NS_OK; } void AsyncRequestHelper::OnResponse(const LSRequestResponse& aResponse) { AssertIsOnDOMFileThread(); MOZ_ASSERT(mState == State::ResponsePending); mActor = nullptr; mResponse = aResponse; mState = State::Finishing; MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); } void SimpleRequestResolver::HandleResponse(nsresult aResponse) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mPromise); mPromise->MaybeReject(aResponse); } void SimpleRequestResolver::HandleResponse(bool aResponse) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mPromise); mPromise->MaybeResolve(aResponse); } [[nodiscard]] static bool ToJSValue(JSContext* aCx, const nsTArray& aArgument, JS::MutableHandle aValue) { JS::Rooted obj(aCx, JS_NewPlainObject(aCx)); if (!obj) { return false; } for (size_t i = 0; i < aArgument.Length(); ++i) { const LSItemInfo& itemInfo = aArgument[i]; const nsString& key = itemInfo.key(); JS::Rooted value(aCx); if (!ToJSValue(aCx, itemInfo.value().AsString(), &value)) { return false; } if (!JS_DefineUCProperty(aCx, obj, key.BeginReading(), key.Length(), value, JSPROP_ENUMERATE)) { return false; } } aValue.setObject(*obj); return true; } void SimpleRequestResolver::HandleResponse( const nsTArray& aResponse) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mPromise); mPromise->MaybeResolve(aResponse); } void SimpleRequestResolver::OnResponse( const LSSimpleRequestResponse& aResponse) { MOZ_ASSERT(NS_IsMainThread()); switch (aResponse.type()) { case LSSimpleRequestResponse::Tnsresult: HandleResponse(aResponse.get_nsresult()); break; case LSSimpleRequestResponse::TLSSimpleRequestPreloadedResponse: HandleResponse( aResponse.get_LSSimpleRequestPreloadedResponse().preloaded()); break; case LSSimpleRequestResponse::TLSSimpleRequestGetStateResponse: HandleResponse( aResponse.get_LSSimpleRequestGetStateResponse().itemInfos()); break; default: MOZ_CRASH("Unknown response type!"); } } } // namespace mozilla::dom