diff options
Diffstat (limited to 'dom/promise/Promise.cpp')
-rw-r--r-- | dom/promise/Promise.cpp | 1129 |
1 files changed, 1129 insertions, 0 deletions
diff --git a/dom/promise/Promise.cpp b/dom/promise/Promise.cpp new file mode 100644 index 0000000000..fb4989c43d --- /dev/null +++ b/dom/promise/Promise.cpp @@ -0,0 +1,1129 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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 "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" + +#include "js/Debug.h" + +#include "mozilla/Atomics.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/Preferences.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Unused.h" + +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/MediaStreamError.h" +#include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkletImpl.h" +#include "mozilla/dom/WorkletGlobalScope.h" + +#include "jsfriendapi.h" +#include "js/Exception.h" // JS::ExceptionStack +#include "js/Object.h" // JS::GetCompartment +#include "js/StructuredClone.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDebug.h" +#include "nsGlobalWindowInner.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsJSEnvironment.h" +#include "nsJSPrincipals.h" +#include "nsJSUtils.h" +#include "nsPIDOMWindow.h" +#include "PromiseDebugging.h" +#include "PromiseNativeHandler.h" +#include "PromiseWorkerProxy.h" +#include "WrapperFactory.h" +#include "xpcpublic.h" +#include "xpcprivate.h" + +namespace mozilla::dom { + +// Promise + +NS_IMPL_CYCLE_COLLECTION_SINGLE_ZONE_SCRIPT_HOLDER_CLASS(Promise) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR + tmp->mPromiseObj = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Promise) + // If you add new JS member variables, you may need to stop using + // NS_IMPL_CYCLE_COLLECTION_SINGLE_ZONE_SCRIPT_HOLDER_CLASS. + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPromiseObj); +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +Promise::Promise(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal), mPromiseObj(nullptr) { + MOZ_ASSERT(mGlobal); + + mozilla::HoldJSObjects(this); +} + +Promise::~Promise() { mozilla::DropJSObjects(this); } + +// static +already_AddRefed<Promise> Promise::Create( + nsIGlobalObject* aGlobal, ErrorResult& aRv, + PropagateUserInteraction aPropagateUserInteraction) { + if (!aGlobal) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + RefPtr<Promise> p = new Promise(aGlobal); + p->CreateWrapper(aRv, aPropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + return p.forget(); +} + +// static +already_AddRefed<Promise> Promise::CreateInfallible( + nsIGlobalObject* aGlobal, + PropagateUserInteraction aPropagateUserInteraction) { + MOZ_ASSERT(aGlobal); + RefPtr<Promise> p = new Promise(aGlobal); + IgnoredErrorResult rv; + p->CreateWrapper(rv, aPropagateUserInteraction); + if (rv.Failed() && rv.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY)) { + MOZ_CRASH("Out of memory"); + } + + // We may have failed to init the wrapper here, because nsIGlobalObject had + // null GlobalJSObject. In that case we consider the JS realm is dead, which + // means: + // 1. This promise can't be settled. + // 2. Nothing can subscribe this promise anymore from that realm. + // Such condition makes this promise a no-op object. + (void)NS_WARN_IF(!p->PromiseObj()); + + return p.forget(); +} + +bool Promise::MaybePropagateUserInputEventHandling() { + MOZ_ASSERT(mPromiseObj, + "Should be called only if the wrapper is successfully created"); + JS::PromiseUserInputEventHandlingState state = + UserActivation::IsHandlingUserInput() + ? JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation + : JS::PromiseUserInputEventHandlingState:: + DidntHaveUserInteractionAtCreation; + JS::Rooted<JSObject*> p(RootingCx(), mPromiseObj); + return JS::SetPromiseUserInputEventHandlingState(p, state); +} + +// static +already_AddRefed<Promise> Promise::Resolve( + nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv, PropagateUserInteraction aPropagateUserInteraction) { + JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject()); + JS::Rooted<JSObject*> p(aCx, JS::CallOriginalPromiseResolve(aCx, aValue)); + if (!p) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + + return CreateFromExisting(aGlobal, p, aPropagateUserInteraction); +} + +// static +already_AddRefed<Promise> Promise::Reject(nsIGlobalObject* aGlobal, + JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject()); + JS::Rooted<JSObject*> p(aCx, JS::CallOriginalPromiseReject(aCx, aValue)); + if (!p) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + + // This promise will never be resolved, so we pass + // eDontPropagateUserInteraction for aPropagateUserInteraction + // unconditionally. + return CreateFromExisting(aGlobal, p, eDontPropagateUserInteraction); +} + +// static +already_AddRefed<Promise> Promise::All( + JSContext* aCx, const nsTArray<RefPtr<Promise>>& aPromiseList, + ErrorResult& aRv, PropagateUserInteraction aPropagateUserInteraction) { + JS::Rooted<JSObject*> globalObj(aCx, JS::CurrentGlobalOrNull(aCx)); + if (!globalObj) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(globalObj); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + JS::RootedVector<JSObject*> promises(aCx); + if (!promises.reserve(aPromiseList.Length())) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + + for (const auto& promise : aPromiseList) { + JS::Rooted<JSObject*> promiseObj(aCx, promise->PromiseObj()); + if (!promiseObj) { + // No-op object will never settle, so we return a no-op Promise here, + // which is equivalent of returning the existing no-op one. + return do_AddRef(promise); + } + // Just in case, make sure these are all in the context compartment. + if (!JS_WrapObject(aCx, &promiseObj)) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + promises.infallibleAppend(promiseObj); + } + + JS::Rooted<JSObject*> result(aCx, JS::GetWaitForAllPromise(aCx, promises)); + if (!result) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + + return CreateFromExisting(global, result, aPropagateUserInteraction); +} + +void Promise::Then(JSContext* aCx, + // aCalleeGlobal may not be in the compartment of aCx, when + // called over Xrays. + JS::Handle<JSObject*> aCalleeGlobal, + AnyCallback* aResolveCallback, AnyCallback* aRejectCallback, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) { + NS_ASSERT_OWNINGTHREAD(Promise); + + // Let's hope this does the right thing with Xrays... Ensure everything is + // just in the caller compartment; that ought to do the trick. In theory we + // should consider aCalleeGlobal, but in practice our only caller is + // DOMRequest::Then, which is not working with a Promise subclass, so things + // should be OK. + JS::Rooted<JSObject*> promise(aCx, PromiseObj()); + if (!promise) { + // This promise is no-op, so do nothing. + return; + } + + if (!JS_WrapObject(aCx, &promise)) { + aRv.NoteJSContextException(aCx); + return; + } + + JS::Rooted<JSObject*> resolveCallback(aCx); + if (aResolveCallback) { + resolveCallback = aResolveCallback->CallbackOrNull(); + if (!JS_WrapObject(aCx, &resolveCallback)) { + aRv.NoteJSContextException(aCx); + return; + } + } + + JS::Rooted<JSObject*> rejectCallback(aCx); + if (aRejectCallback) { + rejectCallback = aRejectCallback->CallbackOrNull(); + if (!JS_WrapObject(aCx, &rejectCallback)) { + aRv.NoteJSContextException(aCx); + return; + } + } + + JS::Rooted<JSObject*> retval(aCx); + retval = JS::CallOriginalPromiseThen(aCx, promise, resolveCallback, + rejectCallback); + if (!retval) { + aRv.NoteJSContextException(aCx); + return; + } + + aRetval.setObject(*retval); +} + +static void SettlePromise(Promise* aSettlingPromise, Promise* aCallbackPromise, + ErrorResult& aRv) { + if (!aSettlingPromise) { + return; + } + if (aRv.IsUncatchableException()) { + return; + } + if (aRv.Failed()) { + aSettlingPromise->MaybeReject(std::move(aRv)); + return; + } + if (aCallbackPromise) { + aSettlingPromise->MaybeResolve(aCallbackPromise); + } else { + aSettlingPromise->MaybeResolveWithUndefined(); + } +} + +void PromiseNativeThenHandlerBase::ResolvedCallback( + JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + if (!HasResolvedCallback()) { + mPromise->MaybeResolve(aValue); + return; + } + RefPtr<Promise> promise = CallResolveCallback(aCx, aValue, aRv); + SettlePromise(mPromise, promise, aRv); +} + +void PromiseNativeThenHandlerBase::RejectedCallback( + JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + if (!HasRejectedCallback()) { + mPromise->MaybeReject(aValue); + return; + } + RefPtr<Promise> promise = CallRejectCallback(aCx, aValue, aRv); + SettlePromise(mPromise, promise, aRv); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(PromiseNativeThenHandlerBase) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PromiseNativeThenHandlerBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) + tmp->Traverse(cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PromiseNativeThenHandlerBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) + tmp->Unlink(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseNativeThenHandlerBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(PromiseNativeThenHandlerBase) + tmp->Trace(aCallbacks, aClosure); +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseNativeThenHandlerBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseNativeThenHandlerBase) + +Result<RefPtr<Promise>, nsresult> Promise::ThenWithoutCycleCollection( + const std::function<already_AddRefed<Promise>( + JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv)>& + aCallback) { + return ThenWithCycleCollectedArgs(aCallback); +} + +void Promise::CreateWrapper( + ErrorResult& aRv, PropagateUserInteraction aPropagateUserInteraction) { + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + JSContext* cx = jsapi.cx(); + mPromiseObj = JS::NewPromiseObject(cx, nullptr); + if (!mPromiseObj) { + JS_ClearPendingException(cx); + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + if (aPropagateUserInteraction == ePropagateUserInteraction) { + Unused << MaybePropagateUserInputEventHandling(); + } +} + +void Promise::MaybeResolve(JSContext* aCx, JS::Handle<JS::Value> aValue) { + NS_ASSERT_OWNINGTHREAD(Promise); + + JS::Rooted<JSObject*> p(aCx, PromiseObj()); + if (!p || !JS::ResolvePromise(aCx, p, aValue)) { + // Now what? There's nothing sane to do here. + JS_ClearPendingException(aCx); + } +} + +void Promise::MaybeReject(JSContext* aCx, JS::Handle<JS::Value> aValue) { + NS_ASSERT_OWNINGTHREAD(Promise); + + JS::Rooted<JSObject*> p(aCx, PromiseObj()); + if (!p || !JS::RejectPromise(aCx, p, aValue)) { + // Now what? There's nothing sane to do here. + JS_ClearPendingException(aCx); + } +} + +#define SLOT_NATIVEHANDLER 0 +#define SLOT_NATIVEHANDLER_TASK 1 + +enum class NativeHandlerTask : int32_t { Resolve, Reject }; + +MOZ_CAN_RUN_SCRIPT +static bool NativeHandlerCallback(JSContext* aCx, unsigned aArgc, + JS::Value* aVp) { + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + + JS::Value v = + js::GetFunctionNativeReserved(&args.callee(), SLOT_NATIVEHANDLER); + MOZ_ASSERT(v.isObject()); + + JS::Rooted<JSObject*> obj(aCx, &v.toObject()); + PromiseNativeHandler* handler = nullptr; + if (NS_FAILED(UNWRAP_OBJECT(PromiseNativeHandler, &obj, handler))) { + return Throw(aCx, NS_ERROR_UNEXPECTED); + } + + v = js::GetFunctionNativeReserved(&args.callee(), SLOT_NATIVEHANDLER_TASK); + NativeHandlerTask task = static_cast<NativeHandlerTask>(v.toInt32()); + + ErrorResult rv; + if (task == NativeHandlerTask::Resolve) { + // handler is kept alive by "obj" on the stack. + MOZ_KnownLive(handler)->ResolvedCallback(aCx, args.get(0), rv); + } else { + MOZ_ASSERT(task == NativeHandlerTask::Reject); + // handler is kept alive by "obj" on the stack. + MOZ_KnownLive(handler)->RejectedCallback(aCx, args.get(0), rv); + } + + return !rv.MaybeSetPendingException(aCx); +} + +static JSObject* CreateNativeHandlerFunction(JSContext* aCx, + JS::Handle<JSObject*> aHolder, + NativeHandlerTask aTask) { + JSFunction* func = js::NewFunctionWithReserved(aCx, NativeHandlerCallback, + /* nargs = */ 1, + /* flags = */ 0, nullptr); + if (!func) { + return nullptr; + } + + JS::Rooted<JSObject*> obj(aCx, JS_GetFunctionObject(func)); + + JS::AssertObjectIsNotGray(aHolder); + js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER, + JS::ObjectValue(*aHolder)); + js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER_TASK, + JS::Int32Value(static_cast<int32_t>(aTask))); + + return obj; +} + +namespace { + +class PromiseNativeHandlerShim final : public PromiseNativeHandler { + RefPtr<PromiseNativeHandler> mInner; +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + enum InnerState{ + NotCleared, + ClearedFromResolve, + ClearedFromReject, + ClearedFromCC, + }; + InnerState mState = NotCleared; +#endif + + ~PromiseNativeHandlerShim() = default; + + public: + explicit PromiseNativeHandlerShim(PromiseNativeHandler* aInner) + : mInner(aInner) { + MOZ_DIAGNOSTIC_ASSERT(mInner); + } + + MOZ_CAN_RUN_SCRIPT + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromResolve); + MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromReject); + MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromCC); +#else + if (!mInner) { + return; + } +#endif + RefPtr<PromiseNativeHandler> inner = std::move(mInner); + inner->ResolvedCallback(aCx, aValue, aRv); + MOZ_ASSERT(!mInner); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mState = ClearedFromResolve; +#endif + } + + MOZ_CAN_RUN_SCRIPT + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromResolve); + MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromReject); + MOZ_DIAGNOSTIC_ASSERT(mState != ClearedFromCC); +#else + if (!mInner) { + return; + } +#endif + RefPtr<PromiseNativeHandler> inner = std::move(mInner); + inner->RejectedCallback(aCx, aValue, aRv); + MOZ_ASSERT(!mInner); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mState = ClearedFromReject; +#endif + } + + bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aWrapper) { + return PromiseNativeHandler_Binding::Wrap(aCx, this, aGivenProto, aWrapper); + } + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(PromiseNativeHandlerShim) +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(PromiseNativeHandlerShim) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PromiseNativeHandlerShim) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInner) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + tmp->mState = ClearedFromCC; +#endif +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PromiseNativeHandlerShim) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInner) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseNativeHandlerShim) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseNativeHandlerShim) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseNativeHandlerShim) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +} // anonymous namespace + +void Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) { + NS_ASSERT_OWNINGTHREAD(Promise); + + AutoJSAPI jsapi; + if (NS_WARN_IF(!mPromiseObj || !jsapi.Init(mGlobal))) { + // Our API doesn't allow us to return a useful error. Not like this should + // happen anyway. + return; + } + + // The self-hosted promise js may keep the object we pass to it alive + // for quite a while depending on when GC runs. Therefore, pass a shim + // object instead. The shim will free its inner PromiseNativeHandler + // after the promise has settled just like our previous c++ promises did. + RefPtr<PromiseNativeHandlerShim> shim = + new PromiseNativeHandlerShim(aRunnable); + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> handlerWrapper(cx); + // Note: PromiseNativeHandler is NOT wrappercached. So we can't use + // ToJSValue here, because it will try to do XPConnect wrapping on it, sadly. + if (NS_WARN_IF(!shim->WrapObject(cx, nullptr, &handlerWrapper))) { + // Again, no way to report errors. + jsapi.ClearException(); + return; + } + + JS::Rooted<JSObject*> resolveFunc(cx); + resolveFunc = CreateNativeHandlerFunction(cx, handlerWrapper, + NativeHandlerTask::Resolve); + if (NS_WARN_IF(!resolveFunc)) { + jsapi.ClearException(); + return; + } + + JS::Rooted<JSObject*> rejectFunc(cx); + rejectFunc = CreateNativeHandlerFunction(cx, handlerWrapper, + NativeHandlerTask::Reject); + if (NS_WARN_IF(!rejectFunc)) { + jsapi.ClearException(); + return; + } + + JS::Rooted<JSObject*> promiseObj(cx, PromiseObj()); + if (NS_WARN_IF( + !JS::AddPromiseReactions(cx, promiseObj, resolveFunc, rejectFunc))) { + jsapi.ClearException(); + return; + } +} + +void Promise::HandleException(JSContext* aCx) { + JS::Rooted<JS::Value> exn(aCx); + if (JS_GetPendingException(aCx, &exn)) { + JS_ClearPendingException(aCx); + // Always reject even if this was called in *Resolve. + MaybeReject(aCx, exn); + } +} + +// static +already_AddRefed<Promise> Promise::RejectWithExceptionFromContext( + nsIGlobalObject* aGlobal, JSContext* aCx, ErrorResult& aError) { + JS::Rooted<JS::Value> exn(aCx); + if (!JS_GetPendingException(aCx, &exn)) { + // This is very important: if there is no pending exception here but we're + // ending up in this code, that means the callee threw an uncatchable + // exception. Just propagate that out as-is. + aError.ThrowUncatchableException(); + return nullptr; + } + + JSAutoRealm ar(aCx, aGlobal->GetGlobalJSObject()); + if (!JS_WrapValue(aCx, &exn)) { + // We just give up. + aError.StealExceptionFromJSContext(aCx); + return nullptr; + } + + JS_ClearPendingException(aCx); + + IgnoredErrorResult error; + RefPtr<Promise> promise = Promise::Reject(aGlobal, aCx, exn, error); + if (!promise) { + // We just give up, let's store the exception in the ErrorResult. + aError.ThrowJSException(aCx, exn); + return nullptr; + } + + return promise.forget(); +} + +// static +already_AddRefed<Promise> Promise::CreateFromExisting( + nsIGlobalObject* aGlobal, JS::Handle<JSObject*> aPromiseObj, + PropagateUserInteraction aPropagateUserInteraction) { + MOZ_ASSERT(JS::GetCompartment(aGlobal->GetGlobalJSObjectPreserveColor()) == + JS::GetCompartment(aPromiseObj)); + RefPtr<Promise> p = new Promise(aGlobal); + p->mPromiseObj = aPromiseObj; + if (aPropagateUserInteraction == ePropagateUserInteraction && + !p->MaybePropagateUserInputEventHandling()) { + return nullptr; + } + return p.forget(); +} + +void Promise::MaybeResolveWithUndefined() { + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeResolve(JS::UndefinedHandleValue); +} + +void Promise::MaybeReject(const RefPtr<MediaStreamError>& aArg) { + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeSomething(aArg, &Promise::MaybeReject); +} + +void Promise::MaybeRejectWithUndefined() { + NS_ASSERT_OWNINGTHREAD(Promise); + + MaybeSomething(JS::UndefinedHandleValue, &Promise::MaybeReject); +} + +void Promise::ReportRejectedPromise(JSContext* aCx, + JS::Handle<JSObject*> aPromise) { + MOZ_ASSERT(!js::IsWrapper(aPromise)); + + MOZ_ASSERT(JS::GetPromiseState(aPromise) == JS::PromiseState::Rejected); + + bool isChrome = false; + uint64_t innerWindowID = 0; + nsGlobalWindowInner* winForDispatch = nullptr; + if (MOZ_LIKELY(NS_IsMainThread())) { + isChrome = nsContentUtils::ObjectPrincipal(aPromise)->IsSystemPrincipal(); + + if (nsGlobalWindowInner* win = xpc::WindowGlobalOrNull(aPromise)) { + winForDispatch = win; + innerWindowID = win->WindowID(); + } else if (nsGlobalWindowInner* win = xpc::SandboxWindowOrNull( + JS::GetNonCCWObjectGlobal(aPromise), aCx)) { + // Don't dispatch rejections from the sandbox to the associated DOM + // window. + innerWindowID = win->WindowID(); + } + } else if (const WorkerPrivate* wp = GetCurrentThreadWorkerPrivate()) { + isChrome = wp->UsesSystemPrincipal(); + innerWindowID = wp->WindowID(); + } else if (nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(aPromise)) { + if (nsCOMPtr<WorkletGlobalScope> workletGlobal = + do_QueryInterface(global)) { + WorkletImpl* impl = workletGlobal->Impl(); + isChrome = impl->PrincipalInfo().type() == + mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo; + innerWindowID = impl->LoadInfo().InnerWindowID(); + } + } + + JS::Rooted<JS::Value> result(aCx, JS::GetPromiseResult(aPromise)); + // resolutionSite can be null if async stacks are disabled. + JS::Rooted<JSObject*> resolutionSite(aCx, + JS::GetPromiseResolutionSite(aPromise)); + + // We're inspecting the rejection value only to report it to the console, and + // we do so without side-effects, so we can safely unwrap it without regard to + // the privileges of the Promise object that holds it. If we don't unwrap + // before trying to create the error report, we wind up reporting any + // cross-origin objects as "uncaught exception: Object". + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + { + Maybe<JSAutoRealm> ar; + JS::Rooted<JS::Value> unwrapped(aCx, result); + if (unwrapped.isObject()) { + unwrapped.setObject(*js::UncheckedUnwrap(&unwrapped.toObject())); + ar.emplace(aCx, &unwrapped.toObject()); + } + + JS::ErrorReportBuilder report(aCx); + RefPtr<Exception> exn; + if (unwrapped.isObject() && + (NS_SUCCEEDED(UNWRAP_OBJECT(DOMException, &unwrapped, exn)) || + NS_SUCCEEDED(UNWRAP_OBJECT(Exception, &unwrapped, exn)))) { + xpcReport->Init(aCx, exn, isChrome, innerWindowID); + } else { + // Use the resolution site as the exception stack + JS::ExceptionStack exnStack(aCx, unwrapped, resolutionSite); + if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::NoSideEffects)) { + JS_ClearPendingException(aCx); + return; + } + + xpcReport->Init(report.report(), report.toStringResult().c_str(), + isChrome, innerWindowID); + } + } + + // Used to initialize the similarly named nsISciptError attribute. + xpcReport->mIsPromiseRejection = true; + + // Now post an event to do the real reporting async + RefPtr<AsyncErrorReporter> event = new AsyncErrorReporter(xpcReport); + if (winForDispatch) { + if (!winForDispatch->IsDying()) { + // Exceptions from a dying window will cause the window to leak. + event->SetException(aCx, result); + if (resolutionSite) { + event->SerializeStack(aCx, resolutionSite); + } + } + winForDispatch->Dispatch(event.forget()); + } else { + NS_DispatchToMainThread(event); + } +} + +void Promise::MaybeResolveWithClone(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + JS::Rooted<JSObject*> sourceScope(aCx, JS::CurrentGlobalOrNull(aCx)); + AutoEntryScript aes(GetParentObject(), "Promise resolution"); + JSContext* cx = aes.cx(); + JS::Rooted<JS::Value> value(cx, aValue); + + xpc::StackScopedCloneOptions options; + options.wrapReflectors = true; + if (!StackScopedClone(cx, options, sourceScope, &value)) { + HandleException(cx); + return; + } + MaybeResolve(aCx, value); +} + +void Promise::MaybeRejectWithClone(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + JS::Rooted<JSObject*> sourceScope(aCx, JS::CurrentGlobalOrNull(aCx)); + AutoEntryScript aes(GetParentObject(), "Promise rejection"); + JSContext* cx = aes.cx(); + JS::Rooted<JS::Value> value(cx, aValue); + + xpc::StackScopedCloneOptions options; + options.wrapReflectors = true; + if (!StackScopedClone(cx, options, sourceScope, &value)) { + HandleException(cx); + return; + } + MaybeReject(aCx, value); +} + +// A WorkerRunnable to resolve/reject the Promise on the worker thread. +// Calling thread MUST hold PromiseWorkerProxy's mutex before creating this. +class PromiseWorkerProxyRunnable final : public WorkerRunnable { + public: + PromiseWorkerProxyRunnable(PromiseWorkerProxy* aPromiseWorkerProxy, + PromiseWorkerProxy::RunCallbackFunc aFunc) + : WorkerRunnable(aPromiseWorkerProxy->GetWorkerPrivate(), + "PromiseWorkerProxyRunnable", WorkerThread), + mPromiseWorkerProxy(aPromiseWorkerProxy), + mFunc(aFunc) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPromiseWorkerProxy); + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); + + MOZ_ASSERT(mPromiseWorkerProxy); + RefPtr<Promise> workerPromise = mPromiseWorkerProxy->GetWorkerPromise(); + // Once Worker had already started shutdown, workerPromise would be nullptr + if (!workerPromise) { + return true; + } + + // Here we convert the buffer to a JS::Value. + JS::Rooted<JS::Value> value(aCx); + if (!mPromiseWorkerProxy->Read(aCx, &value)) { + JS_ClearPendingException(aCx); + return false; + } + + (workerPromise->*mFunc)(aCx, value); + + // Release the Promise because it has been resolved/rejected for sure. + mPromiseWorkerProxy->CleanUp(); + return true; + } + + protected: + ~PromiseWorkerProxyRunnable() = default; + + private: + RefPtr<PromiseWorkerProxy> mPromiseWorkerProxy; + + // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. + PromiseWorkerProxy::RunCallbackFunc mFunc; +}; + +/* static */ +already_AddRefed<PromiseWorkerProxy> PromiseWorkerProxy::Create( + WorkerPrivate* aWorkerPrivate, Promise* aWorkerPromise, + const PromiseWorkerProxyStructuredCloneCallbacks* aCb) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPromise); + MOZ_ASSERT_IF(aCb, !!aCb->Write && !!aCb->Read); + + RefPtr<PromiseWorkerProxy> proxy = + new PromiseWorkerProxy(aWorkerPromise, aCb); + + // Maintain a reference so that we have a valid object to clean up when + // removing the feature. + proxy.get()->AddRef(); + + // We do this to make sure the worker thread won't shut down before the + // promise is resolved/rejected on the worker thread. + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + aWorkerPrivate, "PromiseWorkerProxy", [proxy]() { proxy->CleanUp(); }); + + if (NS_WARN_IF(!workerRef)) { + // Probably the worker is terminating. We cannot complete the operation + // and we have to release all the resources. CleanUp releases the extra + // ref, too + proxy->CleanUp(); + return nullptr; + } + + proxy->mWorkerRef = new ThreadSafeWorkerRef(workerRef); + + return proxy.forget(); +} + +NS_IMPL_ISUPPORTS0(PromiseWorkerProxy) + +PromiseWorkerProxy::PromiseWorkerProxy( + Promise* aWorkerPromise, + const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks) + : mWorkerPromise(aWorkerPromise), + mCleanedUp(false), + mCallbacks(aCallbacks), + mCleanUpLock("cleanUpLock") {} + +PromiseWorkerProxy::~PromiseWorkerProxy() { + MOZ_ASSERT(mCleanedUp); + MOZ_ASSERT(!mWorkerPromise); + MOZ_ASSERT(!mWorkerRef); +} + +WorkerPrivate* PromiseWorkerProxy::GetWorkerPrivate() const { +#ifdef DEBUG + if (NS_IsMainThread()) { + mCleanUpLock.AssertCurrentThreadOwns(); + } +#endif + // Safe to check this without a lock since we assert lock ownership on the + // main thread above. + MOZ_ASSERT(!mCleanedUp); + MOZ_ASSERT(mWorkerRef); + + return mWorkerRef->Private(); +} + +bool PromiseWorkerProxy::OnWritingThread() const { + return IsCurrentThreadRunningWorker(); +} + +Promise* PromiseWorkerProxy::GetWorkerPromise() const { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + return mWorkerPromise; +} + +void PromiseWorkerProxy::RunCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + RunCallbackFunc aFunc) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(Lock()); + // If the worker thread's been cancelled we don't need to resolve the Promise. + if (CleanedUp()) { + return; + } + + // The |aValue| is written into the StructuredCloneHolderBase. + if (!Write(aCx, aValue)) { + JS_ClearPendingException(aCx); + MOZ_ASSERT(false, + "cannot serialize the value with the StructuredCloneAlgorithm!"); + } + + RefPtr<PromiseWorkerProxyRunnable> runnable = + new PromiseWorkerProxyRunnable(this, aFunc); + + runnable->Dispatch(); +} + +void PromiseWorkerProxy::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + RunCallback(aCx, aValue, &Promise::MaybeResolve); +} + +void PromiseWorkerProxy::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + RunCallback(aCx, aValue, &Promise::MaybeReject); +} + +void PromiseWorkerProxy::CleanUp() { + // Can't release Mutex while it is still locked, so scope the lock. + { + MutexAutoLock lock(Lock()); + + if (CleanedUp()) { + return; + } + + // We can be called if we failed to get a WorkerRef + if (mWorkerRef) { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + } + + // Release the Promise and remove the PromiseWorkerProxy from the holders of + // the worker thread since the Promise has been resolved/rejected or the + // worker thread has been cancelled. + mCleanedUp = true; + mWorkerPromise = nullptr; + mWorkerRef = nullptr; + + // Clear the StructuredCloneHolderBase class. + Clear(); + } + Release(); +} + +JSObject* PromiseWorkerProxy::CustomReadHandler( + JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, + uint32_t aIndex) { + if (NS_WARN_IF(!mCallbacks)) { + return nullptr; + } + + return mCallbacks->Read(aCx, aReader, this, aTag, aIndex); +} + +bool PromiseWorkerProxy::CustomWriteHandler(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + JS::Handle<JSObject*> aObj, + bool* aSameProcessScopeRequired) { + if (NS_WARN_IF(!mCallbacks)) { + return false; + } + + return mCallbacks->Write(aCx, aWriter, this, aObj); +} + +// Specializations of MaybeRejectBrokenly we actually support. +template <> +void Promise::MaybeRejectBrokenly(const RefPtr<DOMException>& aArg) { + MaybeSomething(aArg, &Promise::MaybeReject); +} +template <> +void Promise::MaybeRejectBrokenly(const nsAString& aArg) { + MaybeSomething(aArg, &Promise::MaybeReject); +} + +Promise::PromiseState Promise::State() const { + JS::Rooted<JSObject*> p(RootingCx(), PromiseObj()); + const JS::PromiseState state = JS::GetPromiseState(p); + + if (state == JS::PromiseState::Fulfilled) { + return PromiseState::Resolved; + } + + if (state == JS::PromiseState::Rejected) { + return PromiseState::Rejected; + } + + return PromiseState::Pending; +} + +bool Promise::SetSettledPromiseIsHandled() { + if (!mPromiseObj) { + // Do nothing as it's a no-op promise + return false; + } + AutoAllowLegacyScriptExecution exemption; + AutoEntryScript aes(mGlobal, "Set settled promise handled"); + JSContext* cx = aes.cx(); + JS::Rooted<JSObject*> promiseObj(cx, mPromiseObj); + return JS::SetSettledPromiseIsHandled(cx, promiseObj); +} + +bool Promise::SetAnyPromiseIsHandled() { + if (!mPromiseObj) { + // Do nothing as it's a no-op promise + return false; + } + AutoAllowLegacyScriptExecution exemption; + AutoEntryScript aes(mGlobal, "Set any promise handled"); + JSContext* cx = aes.cx(); + JS::Rooted<JSObject*> promiseObj(cx, mPromiseObj); + return JS::SetAnyPromiseIsHandled(cx, promiseObj); +} + +/* static */ +already_AddRefed<Promise> Promise::CreateResolvedWithUndefined( + nsIGlobalObject* global, ErrorResult& aRv) { + RefPtr<Promise> returnPromise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + returnPromise->MaybeResolveWithUndefined(); + return returnPromise.forget(); +} + +already_AddRefed<Promise> Promise::CreateRejected( + nsIGlobalObject* aGlobal, JS::Handle<JS::Value> aRejectionError, + ErrorResult& aRv) { + RefPtr<Promise> promise = Promise::Create(aGlobal, aRv); + if (aRv.Failed()) { + return nullptr; + } + promise->MaybeReject(aRejectionError); + return promise.forget(); +} + +already_AddRefed<Promise> Promise::CreateRejectedWithTypeError( + nsIGlobalObject* aGlobal, const nsACString& aMessage, ErrorResult& aRv) { + RefPtr<Promise> returnPromise = Promise::Create(aGlobal, aRv); + if (aRv.Failed()) { + return nullptr; + } + returnPromise->MaybeRejectWithTypeError(aMessage); + return returnPromise.forget(); +} + +already_AddRefed<Promise> Promise::CreateRejectedWithErrorResult( + nsIGlobalObject* aGlobal, ErrorResult& aRejectionError) { + RefPtr<Promise> returnPromise = Promise::Create(aGlobal, IgnoreErrors()); + if (!returnPromise) { + return nullptr; + } + returnPromise->MaybeReject(std::move(aRejectionError)); + return returnPromise.forget(); +} + +nsresult Promise::TryExtractNSResultFromRejectionValue( + JS::Handle<JS::Value> aValue) { + if (aValue.isInt32()) { + return nsresult(aValue.toInt32()); + } + + if (aValue.isObject()) { + RefPtr<DOMException> domException; + UNWRAP_OBJECT(DOMException, aValue, domException); + if (domException) { + return domException->GetResult(); + } + } + + return NS_ERROR_DOM_NOT_NUMBER_ERR; +} + +} // namespace mozilla::dom + +extern "C" { + +// These functions are used in the implementation of ffi bindings for +// dom::Promise from Rust. + +void DomPromise_AddRef(mozilla::dom::Promise* aPromise) { + MOZ_ASSERT(aPromise); + aPromise->AddRef(); +} + +void DomPromise_Release(mozilla::dom::Promise* aPromise) { + MOZ_ASSERT(aPromise); + aPromise->Release(); +} + +#define DOM_PROMISE_FUNC_WITH_VARIANT(name, func) \ + void name(mozilla::dom::Promise* aPromise, nsIVariant* aVariant) { \ + MOZ_ASSERT(aPromise); \ + MOZ_ASSERT(aVariant); \ + mozilla::dom::AutoEntryScript aes(aPromise->GetGlobalObject(), \ + "Promise resolution or rejection"); \ + JSContext* cx = aes.cx(); \ + \ + JS::Rooted<JS::Value> val(cx); \ + nsresult rv = NS_OK; \ + if (!XPCVariant::VariantDataToJS(cx, aVariant, &rv, &val)) { \ + aPromise->MaybeRejectWithTypeError( \ + "Failed to convert nsIVariant to JS"); \ + return; \ + } \ + aPromise->func(val); \ + } + +DOM_PROMISE_FUNC_WITH_VARIANT(DomPromise_RejectWithVariant, MaybeReject) +DOM_PROMISE_FUNC_WITH_VARIANT(DomPromise_ResolveWithVariant, MaybeResolve) + +#undef DOM_PROMISE_FUNC_WITH_VARIANT +} |