diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /dom/serviceworkers/ServiceWorkerOp.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/serviceworkers/ServiceWorkerOp.cpp')
-rw-r--r-- | dom/serviceworkers/ServiceWorkerOp.cpp | 1918 |
1 files changed, 1918 insertions, 0 deletions
diff --git a/dom/serviceworkers/ServiceWorkerOp.cpp b/dom/serviceworkers/ServiceWorkerOp.cpp new file mode 100644 index 0000000000..7747df49c6 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerOp.cpp @@ -0,0 +1,1918 @@ +/* -*- 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 "ServiceWorkerOp.h" + +#include <utility> + +#include "ServiceWorkerOpPromise.h" +#include "js/Exception.h" // JS::ExceptionStack, JS::StealPendingExceptionStack +#include "jsapi.h" + +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsINamed.h" +#include "nsIPushErrorReporter.h" +#include "nsISupportsImpl.h" +#include "nsITimer.h" +#include "nsServiceManagerUtils.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +#include "ServiceWorkerCloneData.h" +#include "ServiceWorkerShutdownState.h" +#include "mozilla/Assertions.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Client.h" +#include "mozilla/dom/ExtendableMessageEventBinding.h" +#include "mozilla/dom/FetchEventBinding.h" +#include "mozilla/dom/FetchEventOpProxyChild.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/Notification.h" +#include "mozilla/dom/NotificationEvent.h" +#include "mozilla/dom/NotificationEventBinding.h" +#include "mozilla/dom/PerformanceTiming.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/PushEventBinding.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/RemoteWorkerService.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/extensions/ExtensionBrowser.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/net/MozURL.h" + +namespace mozilla::dom { + +namespace { + +class ExtendableEventKeepAliveHandler final + : public ExtendableEvent::ExtensionsHandler, + public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + static RefPtr<ExtendableEventKeepAliveHandler> Create( + RefPtr<ExtendableEventCallback> aCallback) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + + RefPtr<ExtendableEventKeepAliveHandler> self = + new ExtendableEventKeepAliveHandler(std::move(aCallback)); + + self->mWorkerRef = StrongWorkerRef::Create( + GetCurrentThreadWorkerPrivate(), "ExtendableEventKeepAliveHandler", + [self]() { self->Cleanup(); }); + + if (NS_WARN_IF(!self->mWorkerRef)) { + return nullptr; + } + + return self; + } + + /** + * ExtendableEvent::ExtensionsHandler interface + */ + bool WaitOnPromise(Promise& aPromise) override { + if (!mAcceptingPromises) { + MOZ_ASSERT(!GetDispatchFlag()); + MOZ_ASSERT(!mSelfRef, "We shouldn't be holding a self reference!"); + return false; + } + + if (!mSelfRef) { + MOZ_ASSERT(!mPendingPromisesCount); + mSelfRef = this; + } + + ++mPendingPromisesCount; + aPromise.AppendNativeHandler(this); + + return true; + } + + /** + * PromiseNativeHandler interface + */ + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + RemovePromise(Resolved); + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + RemovePromise(Rejected); + } + + void MaybeDone() { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(!GetDispatchFlag()); + + if (mPendingPromisesCount) { + return; + } + + if (mCallback) { + mCallback->FinishedWithResult(mRejected ? Rejected : Resolved); + mCallback = nullptr; + } + + Cleanup(); + } + + private: + /** + * This class is useful for the case where pending microtasks will continue + * extending the event, which means that the event is not "done." For example: + * + * // `e` is an ExtendableEvent, `p` is a Promise + * e.waitUntil(p); + * p.then(() => e.waitUntil(otherPromise)); + */ + class MaybeDoneRunner : public MicroTaskRunnable { + public: + explicit MaybeDoneRunner(RefPtr<ExtendableEventKeepAliveHandler> aHandler) + : mHandler(std::move(aHandler)) {} + + void Run(AutoSlowOperation& /* unused */) override { + mHandler->MaybeDone(); + } + + private: + RefPtr<ExtendableEventKeepAliveHandler> mHandler; + }; + + explicit ExtendableEventKeepAliveHandler( + RefPtr<ExtendableEventCallback> aCallback) + : mCallback(std::move(aCallback)) {} + + ~ExtendableEventKeepAliveHandler() { Cleanup(); } + + void Cleanup() { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + + if (mCallback) { + mCallback->FinishedWithResult(Rejected); + } + + mSelfRef = nullptr; + mWorkerRef = nullptr; + mCallback = nullptr; + mAcceptingPromises = false; + } + + void RemovePromise(ExtendableEventResult aResult) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_DIAGNOSTIC_ASSERT(mPendingPromisesCount > 0); + + // NOTE: mSelfRef can be nullptr here if MaybeCleanup() was just called + // before a promise settled. This can happen, for example, if the worker + // thread is being terminated for running too long, browser shutdown, etc. + + mRejected |= (aResult == Rejected); + + --mPendingPromisesCount; + if (mPendingPromisesCount || GetDispatchFlag()) { + return; + } + + CycleCollectedJSContext* cx = CycleCollectedJSContext::Get(); + MOZ_ASSERT(cx); + + RefPtr<MaybeDoneRunner> r = new MaybeDoneRunner(this); + cx->DispatchToMicroTask(r.forget()); + } + + /** + * We start holding a self reference when the first extension promise is + * added, and this reference is released when the last promise settles or + * when the worker is shutting down. + * + * This is needed in the case that we're waiting indefinitely on a to-be-GC'ed + * promise that's no longer reachable and will never be settled. + */ + RefPtr<ExtendableEventKeepAliveHandler> mSelfRef; + + RefPtr<StrongWorkerRef> mWorkerRef; + + RefPtr<ExtendableEventCallback> mCallback; + + uint32_t mPendingPromisesCount = 0; + + bool mRejected = false; + bool mAcceptingPromises = true; +}; + +NS_IMPL_ISUPPORTS0(ExtendableEventKeepAliveHandler) + +nsresult DispatchExtendableEventOnWorkerScope( + JSContext* aCx, WorkerGlobalScope* aWorkerScope, ExtendableEvent* aEvent, + RefPtr<ExtendableEventCallback> aCallback) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWorkerScope); + MOZ_ASSERT(aEvent); + + nsCOMPtr<nsIGlobalObject> globalObject = aWorkerScope; + WidgetEvent* internalEvent = aEvent->WidgetEventPtr(); + + RefPtr<ExtendableEventKeepAliveHandler> keepAliveHandler = + ExtendableEventKeepAliveHandler::Create(std::move(aCallback)); + if (NS_WARN_IF(!keepAliveHandler)) { + return NS_ERROR_FAILURE; + } + + // This must be always set *before* dispatching the event, otherwise + // waitUntil() calls will fail. + aEvent->SetKeepAliveHandler(keepAliveHandler); + + ErrorResult result; + aWorkerScope->DispatchEvent(*aEvent, result); + if (NS_WARN_IF(result.Failed())) { + result.SuppressException(); + return NS_ERROR_FAILURE; + } + + keepAliveHandler->MaybeDone(); + + // We don't block the event when getting an exception but still report the + // error message. NOTE: this will not stop the event. + if (internalEvent->mFlags.mExceptionWasRaised) { + return NS_ERROR_XPC_JS_THREW_EXCEPTION; + } + + return NS_OK; +} + +bool DispatchFailed(nsresult aStatus) { + return NS_FAILED(aStatus) && aStatus != NS_ERROR_XPC_JS_THREW_EXCEPTION; +} + +} // anonymous namespace + +class ServiceWorkerOp::ServiceWorkerOpRunnable : public WorkerDebuggeeRunnable { + public: + NS_DECL_ISUPPORTS_INHERITED + + ServiceWorkerOpRunnable(RefPtr<ServiceWorkerOp> aOwner, + WorkerPrivate* aWorkerPrivate) + : WorkerDebuggeeRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount), + mOwner(std::move(aOwner)) { + AssertIsOnMainThread(); + MOZ_ASSERT(mOwner); + MOZ_ASSERT(aWorkerPrivate); + } + + private: + ~ServiceWorkerOpRunnable() = default; + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(mOwner); + + bool rv = mOwner->Exec(aCx, aWorkerPrivate); + Unused << NS_WARN_IF(!rv); + mOwner = nullptr; + + return rv; + } + + nsresult Cancel() override { + // We need to check first if cancel is permitted + nsresult rv = WorkerRunnable::Cancel(); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(mOwner); + + mOwner->RejectAll(NS_ERROR_DOM_ABORT_ERR); + mOwner = nullptr; + + return NS_OK; + } + + RefPtr<ServiceWorkerOp> mOwner; +}; + +NS_IMPL_ISUPPORTS_INHERITED0(ServiceWorkerOp::ServiceWorkerOpRunnable, + WorkerRunnable) + +bool ServiceWorkerOp::MaybeStart(RemoteWorkerChild* aOwner, + RemoteWorkerChild::State& aState) { + MOZ_ASSERT(!mStarted); + MOZ_ASSERT(aOwner); + MOZ_ASSERT(aOwner->GetActorEventTarget()->IsOnCurrentThread()); + + auto launcherData = aOwner->mLauncherData.Access(); + + if (NS_WARN_IF(!aOwner->CanSend())) { + RejectAll(NS_ERROR_DOM_ABORT_ERR); + mStarted = true; + return true; + } + + // Allow termination to happen while the Service Worker is initializing. + if (aState.is<Pending>() && !IsTerminationOp()) { + return false; + } + + if (NS_WARN_IF(aState.is<RemoteWorkerChild::Canceled>()) || + NS_WARN_IF(aState.is<RemoteWorkerChild::Killed>())) { + RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR); + mStarted = true; + return true; + } + + MOZ_ASSERT(aState.is<RemoteWorkerChild::Running>() || IsTerminationOp()); + + RefPtr<ServiceWorkerOp> self = this; + + if (IsTerminationOp()) { + aOwner->GetTerminationPromise()->Then( + GetCurrentSerialEventTarget(), __func__, + [self]( + const GenericNonExclusivePromise::ResolveOrRejectValue& aResult) { + MaybeReportServiceWorkerShutdownProgress(self->mArgs, true); + + MOZ_ASSERT(!self->mPromiseHolder.IsEmpty()); + + if (NS_WARN_IF(aResult.IsReject())) { + self->mPromiseHolder.Reject(aResult.RejectValue(), __func__); + return; + } + + self->mPromiseHolder.Resolve(NS_OK, __func__); + }); + } + + // NewRunnableMethod doesn't work here because the template does not appear to + // be able to deal with the owner argument having storage as a RefPtr but + // with the method taking a RefPtr&. + RefPtr<RemoteWorkerChild> owner = aOwner; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), owner = std::move(owner)]() mutable { + self->StartOnMainThread(owner); + }); + + mStarted = true; + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return true; +} + +void ServiceWorkerOp::StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) { + MaybeReportServiceWorkerShutdownProgress(mArgs); + + { + auto lock = aOwner->mState.Lock(); + + if (NS_WARN_IF(!lock->is<Running>() && !IsTerminationOp())) { + RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + } + + if (IsTerminationOp()) { + aOwner->CloseWorkerOnMainThread(); + } else { + auto lock = aOwner->mState.Lock(); + MOZ_ASSERT(lock->is<Running>()); + + RefPtr<WorkerRunnable> workerRunnable = + GetRunnable(lock->as<Running>().mWorkerPrivate); + + if (NS_WARN_IF(!workerRunnable->Dispatch())) { + RejectAll(NS_ERROR_FAILURE); + } + } +} + +void ServiceWorkerOp::Cancel() { RejectAll(NS_ERROR_DOM_ABORT_ERR); } + +ServiceWorkerOp::ServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, + std::function<void(const ServiceWorkerOpResult&)>&& aCallback) + : mArgs(std::move(aArgs)) { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + + RefPtr<ServiceWorkerOpPromise> promise = mPromiseHolder.Ensure(__func__); + + promise->Then( + GetCurrentSerialEventTarget(), __func__, + [callback = std::move(aCallback)]( + ServiceWorkerOpPromise::ResolveOrRejectValue&& aResult) mutable { + if (NS_WARN_IF(aResult.IsReject())) { + MOZ_ASSERT(NS_FAILED(aResult.RejectValue())); + callback(aResult.RejectValue()); + return; + } + + callback(aResult.ResolveValue()); + }); +} + +ServiceWorkerOp::~ServiceWorkerOp() { + Unused << NS_WARN_IF(!mPromiseHolder.IsEmpty()); + mPromiseHolder.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__); +} + +bool ServiceWorkerOp::Started() const { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + + return mStarted; +} + +bool ServiceWorkerOp::IsTerminationOp() const { + return mArgs.type() == + ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs; +} + +RefPtr<WorkerRunnable> ServiceWorkerOp::GetRunnable( + WorkerPrivate* aWorkerPrivate) { + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + + return new ServiceWorkerOpRunnable(this, aWorkerPrivate); +} + +void ServiceWorkerOp::RejectAll(nsresult aStatus) { + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + mPromiseHolder.Reject(aStatus, __func__); +} + +class CheckScriptEvaluationOp final : public ServiceWorkerOp { + using ServiceWorkerOp::ServiceWorkerOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CheckScriptEvaluationOp, override) + + private: + ~CheckScriptEvaluationOp() = default; + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + ServiceWorkerCheckScriptEvaluationOpResult result; + result.workerScriptExecutedSuccessfully() = + aWorkerPrivate->WorkerScriptExecutedSuccessfully(); + result.fetchHandlerWasAdded() = aWorkerPrivate->FetchHandlerWasAdded(); + + mPromiseHolder.Resolve(result, __func__); + + return true; + } +}; + +class TerminateServiceWorkerOp final : public ServiceWorkerOp { + using ServiceWorkerOp::ServiceWorkerOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TerminateServiceWorkerOp, override) + + private: + ~TerminateServiceWorkerOp() = default; + + bool Exec(JSContext*, WorkerPrivate*) override { + MOZ_ASSERT_UNREACHABLE( + "Worker termination should be handled in " + "`ServiceWorkerOp::MaybeStart()`"); + + return false; + } +}; + +class UpdateServiceWorkerStateOp final : public ServiceWorkerOp { + using ServiceWorkerOp::ServiceWorkerOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UpdateServiceWorkerStateOp, override); + + private: + class UpdateStateOpRunnable final : public MainThreadWorkerControlRunnable { + public: + NS_DECL_ISUPPORTS_INHERITED + + UpdateStateOpRunnable(RefPtr<UpdateServiceWorkerStateOp> aOwner, + WorkerPrivate* aWorkerPrivate) + : MainThreadWorkerControlRunnable(aWorkerPrivate), + mOwner(std::move(aOwner)) { + AssertIsOnMainThread(); + MOZ_ASSERT(mOwner); + MOZ_ASSERT(aWorkerPrivate); + } + + private: + ~UpdateStateOpRunnable() = default; + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + + if (mOwner) { + Unused << mOwner->Exec(aCx, aWorkerPrivate); + mOwner = nullptr; + } + + return true; + } + + nsresult Cancel() override { + MOZ_ASSERT(mOwner); + + mOwner->RejectAll(NS_ERROR_DOM_ABORT_ERR); + mOwner = nullptr; + + return MainThreadWorkerControlRunnable::Cancel(); + } + + RefPtr<UpdateServiceWorkerStateOp> mOwner; + }; + + ~UpdateServiceWorkerStateOp() = default; + + RefPtr<WorkerRunnable> GetRunnable(WorkerPrivate* aWorkerPrivate) override { + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(mArgs.type() == + ServiceWorkerOpArgs::TServiceWorkerUpdateStateOpArgs); + + return new UpdateStateOpRunnable(this, aWorkerPrivate); + } + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + ServiceWorkerState state = + mArgs.get_ServiceWorkerUpdateStateOpArgs().state(); + aWorkerPrivate->UpdateServiceWorkerState(state); + + mPromiseHolder.Resolve(NS_OK, __func__); + + return true; + } +}; + +NS_IMPL_ISUPPORTS_INHERITED0(UpdateServiceWorkerStateOp::UpdateStateOpRunnable, + MainThreadWorkerControlRunnable) + +void ExtendableEventOp::FinishedWithResult(ExtendableEventResult aResult) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + mPromiseHolder.Resolve(aResult == Resolved ? NS_OK : NS_ERROR_FAILURE, + __func__); +} + +class LifeCycleEventOp final : public ExtendableEventOp { + using ExtendableEventOp::ExtendableEventOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(LifeCycleEventOp, override) + + private: + ~LifeCycleEventOp() = default; + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + RefPtr<ExtendableEvent> event; + RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope(); + + const nsString& eventName = + mArgs.get_ServiceWorkerLifeCycleEventOpArgs().eventName(); + + if (eventName.EqualsASCII("install") || eventName.EqualsASCII("activate")) { + ExtendableEventInit init; + init.mBubbles = false; + init.mCancelable = false; + event = ExtendableEvent::Constructor(target, eventName, init); + } else { + MOZ_CRASH("Unexpected lifecycle event"); + } + + event->SetTrusted(true); + + nsresult rv = DispatchExtendableEventOnWorkerScope( + aCx, aWorkerPrivate->GlobalScope(), event, this); + + if (NS_WARN_IF(DispatchFailed(rv))) { + RejectAll(rv); + } + + return !DispatchFailed(rv); + } +}; + +/** + * PushEventOp + */ +class PushEventOp final : public ExtendableEventOp { + using ExtendableEventOp::ExtendableEventOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PushEventOp, override) + + private: + ~PushEventOp() = default; + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + ErrorResult result; + + auto scopeExit = MakeScopeExit([&] { + MOZ_ASSERT(result.Failed()); + + RejectAll(result.StealNSResult()); + ReportError(aWorkerPrivate); + }); + + const ServiceWorkerPushEventOpArgs& args = + mArgs.get_ServiceWorkerPushEventOpArgs(); + + RootedDictionary<PushEventInit> pushEventInit(aCx); + + if (args.data().type() != OptionalPushData::Tvoid_t) { + auto& bytes = args.data().get_ArrayOfuint8_t(); + JSObject* data = + Uint8Array::Create(aCx, bytes.Length(), bytes.Elements()); + + if (!data) { + result = ErrorResult(NS_ERROR_FAILURE); + return false; + } + + DebugOnly<bool> inited = + pushEventInit.mData.Construct().SetAsArrayBufferView().Init(data); + MOZ_ASSERT(inited); + } + + pushEventInit.mBubbles = false; + pushEventInit.mCancelable = false; + + GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper()); + RefPtr<PushEvent> pushEvent = + PushEvent::Constructor(globalObj, u"push"_ns, pushEventInit, result); + + if (NS_WARN_IF(result.Failed())) { + return false; + } + + pushEvent->SetTrusted(true); + + scopeExit.release(); + + nsresult rv = DispatchExtendableEventOnWorkerScope( + aCx, aWorkerPrivate->GlobalScope(), pushEvent, this); + + if (NS_FAILED(rv)) { + if (NS_WARN_IF(DispatchFailed(rv))) { + RejectAll(rv); + } + + // We don't cancel WorkerPrivate when catching an exception. + ReportError(aWorkerPrivate, + nsIPushErrorReporter::DELIVERY_UNCAUGHT_EXCEPTION); + } + + return !DispatchFailed(rv); + } + + void FinishedWithResult(ExtendableEventResult aResult) override { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + + if (aResult == Rejected) { + ReportError(workerPrivate, + nsIPushErrorReporter::DELIVERY_UNHANDLED_REJECTION); + } + + ExtendableEventOp::FinishedWithResult(aResult); + } + + void ReportError( + WorkerPrivate* aWorkerPrivate, + uint16_t aError = nsIPushErrorReporter::DELIVERY_INTERNAL_ERROR) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + + if (NS_WARN_IF(aError > nsIPushErrorReporter::DELIVERY_INTERNAL_ERROR) || + mArgs.get_ServiceWorkerPushEventOpArgs().messageId().IsEmpty()) { + return; + } + + nsString messageId = mArgs.get_ServiceWorkerPushEventOpArgs().messageId(); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [messageId = std::move(messageId), error = aError] { + nsCOMPtr<nsIPushErrorReporter> reporter = + do_GetService("@mozilla.org/push/Service;1"); + + if (reporter) { + nsresult rv = reporter->ReportDeliveryError(messageId, error); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + }); + + MOZ_ALWAYS_SUCCEEDS(aWorkerPrivate->DispatchToMainThread(r.forget())); + } +}; + +class PushSubscriptionChangeEventOp final : public ExtendableEventOp { + using ExtendableEventOp::ExtendableEventOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PushSubscriptionChangeEventOp, override) + + private: + ~PushSubscriptionChangeEventOp() = default; + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope(); + + ExtendableEventInit init; + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<ExtendableEvent> event = ExtendableEvent::Constructor( + target, u"pushsubscriptionchange"_ns, init); + event->SetTrusted(true); + + nsresult rv = DispatchExtendableEventOnWorkerScope( + aCx, aWorkerPrivate->GlobalScope(), event, this); + + if (NS_WARN_IF(DispatchFailed(rv))) { + RejectAll(rv); + } + + return !DispatchFailed(rv); + } +}; + +class NotificationEventOp : public ExtendableEventOp, + public nsITimerCallback, + public nsINamed { + using ExtendableEventOp::ExtendableEventOp; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + private: + ~NotificationEventOp() { + MOZ_DIAGNOSTIC_ASSERT(!mTimer); + MOZ_DIAGNOSTIC_ASSERT(!mWorkerRef); + } + + void ClearWindowAllowed(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + + if (!mTimer) { + return; + } + + // This might be executed after the global was unrooted, in which case + // GlobalScope() will return null. Making the check here just to be safe. + WorkerGlobalScope* globalScope = aWorkerPrivate->GlobalScope(); + if (!globalScope) { + return; + } + + globalScope->ConsumeWindowInteraction(); + mTimer->Cancel(); + mTimer = nullptr; + + mWorkerRef = nullptr; + } + + void StartClearWindowTimer(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mTimer); + + nsresult rv; + nsCOMPtr<nsITimer> timer = + NS_NewTimer(aWorkerPrivate->ControlEventTarget()); + if (NS_WARN_IF(!timer)) { + return; + } + + MOZ_ASSERT(!mWorkerRef); + RefPtr<NotificationEventOp> self = this; + mWorkerRef = StrongWorkerRef::Create( + aWorkerPrivate, "NotificationEventOp", [self = std::move(self)] { + // We could try to hold the worker alive until the timer fires, but + // other APIs are not likely to work in this partially shutdown state. + // We might as well let the worker thread exit. + self->ClearWindowAllowed(self->mWorkerRef->Private()); + }); + + if (!mWorkerRef) { + return; + } + + aWorkerPrivate->GlobalScope()->AllowWindowInteraction(); + timer.swap(mTimer); + + // We swap first and then initialize the timer so that even if initializing + // fails, we still clean the busy count and interaction count correctly. + // The timer can't be initialized before modyfing the busy count since the + // timer thread could run and call the timeout but the worker may + // already be terminating and modifying the busy count could fail. + uint32_t delay = mArgs.get_ServiceWorkerNotificationEventOpArgs() + .disableOpenClickDelay(); + rv = mTimer->InitWithCallback(this, delay, nsITimer::TYPE_ONE_SHOT); + + if (NS_WARN_IF(NS_FAILED(rv))) { + ClearWindowAllowed(aWorkerPrivate); + return; + } + } + + // ExtendableEventOp interface + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope(); + + ServiceWorkerNotificationEventOpArgs& args = + mArgs.get_ServiceWorkerNotificationEventOpArgs(); + + ErrorResult result; + RefPtr<Notification> notification = Notification::ConstructFromFields( + aWorkerPrivate->GlobalScope(), args.id(), args.title(), args.dir(), + args.lang(), args.body(), args.tag(), args.icon(), args.data(), + args.scope(), result); + + if (NS_WARN_IF(result.Failed())) { + return false; + } + + NotificationEventInit init; + init.mNotification = notification; + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<NotificationEvent> notificationEvent = + NotificationEvent::Constructor(target, args.eventName(), init); + + notificationEvent->SetTrusted(true); + + if (args.eventName().EqualsLiteral("notificationclick")) { + StartClearWindowTimer(aWorkerPrivate); + } + + nsresult rv = DispatchExtendableEventOnWorkerScope( + aCx, aWorkerPrivate->GlobalScope(), notificationEvent, this); + + if (NS_WARN_IF(DispatchFailed(rv))) { + // This will reject mPromiseHolder. + FinishedWithResult(Rejected); + } + + return !DispatchFailed(rv); + } + + void FinishedWithResult(ExtendableEventResult aResult) override { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + ClearWindowAllowed(workerPrivate); + + ExtendableEventOp::FinishedWithResult(aResult); + } + + // nsITimerCallback interface + NS_IMETHOD Notify(nsITimer* aTimer) override { + MOZ_DIAGNOSTIC_ASSERT(mTimer == aTimer); + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + ClearWindowAllowed(workerPrivate); + return NS_OK; + } + + // nsINamed interface + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("NotificationEventOp"); + return NS_OK; + } + + nsCOMPtr<nsITimer> mTimer; + RefPtr<StrongWorkerRef> mWorkerRef; +}; + +NS_IMPL_ISUPPORTS(NotificationEventOp, nsITimerCallback, nsINamed) + +class MessageEventOp final : public ExtendableEventOp { + using ExtendableEventOp::ExtendableEventOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MessageEventOp, override) + + MessageEventOp(ServiceWorkerOpArgs&& aArgs, + std::function<void(const ServiceWorkerOpResult&)>&& aCallback) + : ExtendableEventOp(std::move(aArgs), std::move(aCallback)), + mData(new ServiceWorkerCloneData()) { + mData->CopyFromClonedMessageData( + mArgs.get_ServiceWorkerMessageEventOpArgs().clonedData()); + } + + private: + ~MessageEventOp() = default; + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + JS::Rooted<JS::Value> messageData(aCx); + nsCOMPtr<nsIGlobalObject> sgo = aWorkerPrivate->GlobalScope(); + ErrorResult rv; + if (!mData->IsErrorMessageData()) { + mData->Read(aCx, &messageData, rv); + } + + // If mData is an error message data, then it means that it failed to + // serialize on the caller side because it contains a shared memory object. + // If deserialization fails, we will fire a messageerror event. + const bool deserializationFailed = + rv.Failed() || mData->IsErrorMessageData(); + + Sequence<OwningNonNull<MessagePort>> ports; + if (!mData->TakeTransferredPortsAsSequence(ports)) { + RejectAll(NS_ERROR_FAILURE); + rv.SuppressException(); + return false; + } + + RootedDictionary<ExtendableMessageEventInit> init(aCx); + + init.mBubbles = false; + init.mCancelable = false; + + // On a messageerror event, we disregard ports: + // https://w3c.github.io/ServiceWorker/#service-worker-postmessage + if (!deserializationFailed) { + init.mData = messageData; + init.mPorts = std::move(ports); + } + + RefPtr<net::MozURL> mozUrl; + nsresult result = net::MozURL::Init( + getter_AddRefs(mozUrl), mArgs.get_ServiceWorkerMessageEventOpArgs() + .clientInfoAndState() + .info() + .url()); + if (NS_WARN_IF(NS_FAILED(result))) { + RejectAll(result); + rv.SuppressException(); + return false; + } + + nsCString origin; + mozUrl->Origin(origin); + + CopyUTF8toUTF16(origin, init.mOrigin); + + init.mSource.SetValue().SetAsClient() = new Client( + sgo, mArgs.get_ServiceWorkerMessageEventOpArgs().clientInfoAndState()); + + rv.SuppressException(); + RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope(); + RefPtr<ExtendableMessageEvent> extendableEvent = + ExtendableMessageEvent::Constructor( + target, deserializationFailed ? u"messageerror"_ns : u"message"_ns, + init); + + extendableEvent->SetTrusted(true); + + nsresult rv2 = DispatchExtendableEventOnWorkerScope( + aCx, aWorkerPrivate->GlobalScope(), extendableEvent, this); + + if (NS_WARN_IF(DispatchFailed(rv2))) { + RejectAll(rv2); + } + + return !DispatchFailed(rv2); + } + + RefPtr<ServiceWorkerCloneData> mData; +}; + +/** + * Used for ScopeExit-style network request cancelation in + * `ResolvedCallback()` (e.g. if `FetchEvent::RespondWith()` is resolved with + * a non-JS object). + */ +class MOZ_STACK_CLASS FetchEventOp::AutoCancel { + public: + explicit AutoCancel(FetchEventOp* aOwner) + : mOwner(aOwner), + mLine(0), + mColumn(0), + mMessageName("InterceptionFailedWithURL"_ns) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(mOwner); + + nsAutoString requestURL; + mOwner->GetRequestURL(requestURL); + mParams.AppendElement(requestURL); + } + + ~AutoCancel() { + if (mOwner) { + if (mSourceSpec.IsEmpty()) { + mOwner->AsyncLog(mMessageName, std::move(mParams)); + } else { + mOwner->AsyncLog(mSourceSpec, mLine, mColumn, mMessageName, + std::move(mParams)); + } + + MOZ_ASSERT(!mOwner->mRespondWithPromiseHolder.IsEmpty()); + mOwner->mHandled->MaybeRejectWithNetworkError("AutoCancel"_ns); + mOwner->mRespondWithPromiseHolder.Reject( + CancelInterceptionArgs( + NS_ERROR_INTERCEPTION_FAILED, + FetchEventTimeStamps(mOwner->mFetchHandlerStart, + mOwner->mFetchHandlerFinish)), + __func__); + } + } + + // This function steals the error message from a ErrorResult. + void SetCancelErrorResult(JSContext* aCx, ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(aRv.Failed()); + MOZ_DIAGNOSTIC_ASSERT(!JS_IsExceptionPending(aCx)); + + // Storing the error as exception in the JSContext. + if (!aRv.MaybeSetPendingException(aCx)) { + return; + } + + MOZ_ASSERT(!aRv.Failed()); + + // Let's take the pending exception. + JS::ExceptionStack exnStack(aCx); + if (!JS::StealPendingExceptionStack(aCx, &exnStack)) { + return; + } + + // Converting the exception in a JS::ErrorReportBuilder. + JS::ErrorReportBuilder report(aCx); + if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + JS_ClearPendingException(aCx); + return; + } + + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL")); + MOZ_ASSERT(mParams.Length() == 1); + + // Let's store the error message here. + mMessageName.Assign(report.toStringResult().c_str()); + mParams.Clear(); + } + + template <typename... Params> + void SetCancelMessage(const nsACString& aMessageName, Params&&... aParams) { + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL")); + MOZ_ASSERT(mParams.Length() == 1); + mMessageName = aMessageName; + mParams.Clear(); + StringArrayAppender::Append(mParams, sizeof...(Params), + std::forward<Params>(aParams)...); + } + + template <typename... Params> + void SetCancelMessageAndLocation(const nsACString& aSourceSpec, + uint32_t aLine, uint32_t aColumn, + const nsACString& aMessageName, + Params&&... aParams) { + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL")); + MOZ_ASSERT(mParams.Length() == 1); + + mSourceSpec = aSourceSpec; + mLine = aLine; + mColumn = aColumn; + + mMessageName = aMessageName; + mParams.Clear(); + StringArrayAppender::Append(mParams, sizeof...(Params), + std::forward<Params>(aParams)...); + } + + void Reset() { mOwner = nullptr; } + + private: + FetchEventOp* MOZ_NON_OWNING_REF mOwner; + nsCString mSourceSpec; + uint32_t mLine; + uint32_t mColumn; + nsCString mMessageName; + nsTArray<nsString> mParams; +}; + +NS_IMPL_ISUPPORTS0(FetchEventOp) + +void FetchEventOp::SetActor(RefPtr<FetchEventOpProxyChild> aActor) { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + MOZ_ASSERT(!Started()); + MOZ_ASSERT(!mActor); + + mActor = std::move(aActor); +} + +void FetchEventOp::RevokeActor(FetchEventOpProxyChild* aActor) { + MOZ_ASSERT(aActor); + MOZ_ASSERT_IF(mActor, mActor == aActor); + + mActor = nullptr; +} + +RefPtr<FetchEventRespondWithPromise> FetchEventOp::GetRespondWithPromise() { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + MOZ_ASSERT(!Started()); + MOZ_ASSERT(mRespondWithPromiseHolder.IsEmpty()); + + return mRespondWithPromiseHolder.Ensure(__func__); +} + +void FetchEventOp::RespondWithCalledAt(const nsCString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, + uint32_t aRespondWithColumnNumber) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(!mRespondWithClosure); + + mRespondWithClosure.emplace(aRespondWithScriptSpec, aRespondWithLineNumber, + aRespondWithColumnNumber); +} + +void FetchEventOp::ReportCanceled(const nsCString& aPreventDefaultScriptSpec, + uint32_t aPreventDefaultLineNumber, + uint32_t aPreventDefaultColumnNumber) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + nsString requestURL; + GetRequestURL(requestURL); + + AsyncLog(aPreventDefaultScriptSpec, aPreventDefaultLineNumber, + aPreventDefaultColumnNumber, "InterceptionCanceledWithURL"_ns, + {std::move(requestURL)}); +} + +FetchEventOp::~FetchEventOp() { + mRespondWithPromiseHolder.RejectIfExists( + CancelInterceptionArgs( + NS_ERROR_DOM_ABORT_ERR, + FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish)), + __func__); +} + +void FetchEventOp::RejectAll(nsresult aStatus) { + MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + if (mFetchHandlerStart.IsNull()) { + mFetchHandlerStart = TimeStamp::Now(); + } + if (mFetchHandlerFinish.IsNull()) { + mFetchHandlerFinish = TimeStamp::Now(); + } + + mRespondWithPromiseHolder.Reject( + CancelInterceptionArgs( + aStatus, + FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish)), + __func__); + mPromiseHolder.Reject(aStatus, __func__); +} + +void FetchEventOp::FinishedWithResult(ExtendableEventResult aResult) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + MOZ_ASSERT(!mResult); + + mResult.emplace(aResult); + + /** + * This should only return early if neither waitUntil() nor respondWith() + * are called. The early return is so that mRespondWithPromiseHolder has a + * chance to settle before mPromiseHolder does. + */ + if (!mPostDispatchChecksDone) { + return; + } + + MaybeFinished(); +} + +void FetchEventOp::MaybeFinished() { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + if (mResult) { + // It's possible that mRespondWithPromiseHolder wasn't settled. That happens + // if the worker was terminated before the respondWith promise settled. + + mHandled = nullptr; + mPreloadResponse = nullptr; + mPreloadResponseAvailablePromiseRequestHolder.DisconnectIfExists(); + mPreloadResponseTimingPromiseRequestHolder.DisconnectIfExists(); + mPreloadResponseEndPromiseRequestHolder.DisconnectIfExists(); + + ServiceWorkerFetchEventOpResult result( + mResult.value() == Resolved ? NS_OK : NS_ERROR_FAILURE); + + mPromiseHolder.Resolve(result, __func__); + } +} + +bool FetchEventOp::Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + nsresult rv = DispatchFetchEvent(aCx, aWorkerPrivate); + + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectAll(rv); + } + + return NS_SUCCEEDED(rv); +} + +void FetchEventOp::AsyncLog(const nsCString& aMessageName, + nsTArray<nsString> aParams) { + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + MOZ_ASSERT(mRespondWithClosure); + + const FetchEventRespondWithClosure& closure = mRespondWithClosure.ref(); + + AsyncLog(closure.respondWithScriptSpec(), closure.respondWithLineNumber(), + closure.respondWithColumnNumber(), aMessageName, std::move(aParams)); +} + +void FetchEventOp::AsyncLog(const nsCString& aScriptSpec, uint32_t aLineNumber, + uint32_t aColumnNumber, + const nsCString& aMessageName, + nsTArray<nsString> aParams) { + MOZ_ASSERT(mActor); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + // Capture `this` because FetchEventOpProxyChild (mActor) is not thread + // safe, so an AddRef from RefPtr<FetchEventOpProxyChild>'s constructor will + // assert. + RefPtr<FetchEventOp> self = this; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), spec = aScriptSpec, line = aLineNumber, + column = aColumnNumber, messageName = aMessageName, + params = std::move(aParams)] { + if (NS_WARN_IF(!self->mActor)) { + return; + } + + Unused << self->mActor->SendAsyncLog(spec, line, column, messageName, + params); + }); + + MOZ_ALWAYS_SUCCEEDS( + RemoteWorkerService::Thread()->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); +} + +void FetchEventOp::GetRequestURL(nsAString& aOutRequestURL) { + nsTArray<nsCString>& urls = + mArgs.get_ParentToChildServiceWorkerFetchEventOpArgs() + .common() + .internalRequest() + .urlList(); + MOZ_ASSERT(!urls.IsEmpty()); + + CopyUTF8toUTF16(urls.LastElement(), aOutRequestURL); +} + +void FetchEventOp::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(mRespondWithClosure); + MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + mFetchHandlerFinish = TimeStamp::Now(); + + nsAutoString requestURL; + GetRequestURL(requestURL); + + AutoCancel autoCancel(this); + + if (!aValue.isObject()) { + NS_WARNING( + "FetchEvent::RespondWith was passed a promise resolved to a " + "non-Object " + "value"); + + nsCString sourceSpec; + uint32_t line = 0; + uint32_t column = 0; + nsString valueString; + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); + + autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column, + "InterceptedNonResponseWithURL"_ns, + requestURL, valueString); + return; + } + + RefPtr<Response> response; + nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response); + if (NS_FAILED(rv)) { + nsCString sourceSpec; + uint32_t line = 0; + uint32_t column = 0; + nsString valueString; + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); + + autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column, + "InterceptedNonResponseWithURL"_ns, + requestURL, valueString); + return; + } + + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + // Section "HTTP Fetch", step 3.3: + // If one of the following conditions is true, return a network error: + // * response's type is "error". + // * request's mode is not "no-cors" and response's type is "opaque". + // * request's redirect mode is not "manual" and response's type is + // "opaqueredirect". + // * request's redirect mode is not "follow" and response's url list + // has more than one item. + + if (response->Type() == ResponseType::Error) { + autoCancel.SetCancelMessage("InterceptedErrorResponseWithURL"_ns, + requestURL); + return; + } + + const ParentToChildServiceWorkerFetchEventOpArgs& args = + mArgs.get_ParentToChildServiceWorkerFetchEventOpArgs(); + const RequestMode requestMode = args.common().internalRequest().requestMode(); + + if (response->Type() == ResponseType::Opaque && + requestMode != RequestMode::No_cors) { + NS_ConvertASCIItoUTF16 modeString( + RequestModeValues::GetString(requestMode)); + + nsAutoString requestURL; + GetRequestURL(requestURL); + + autoCancel.SetCancelMessage("BadOpaqueInterceptionRequestModeWithURL"_ns, + requestURL, modeString); + return; + } + + const RequestRedirect requestRedirectMode = + args.common().internalRequest().requestRedirect(); + + if (requestRedirectMode != RequestRedirect::Manual && + response->Type() == ResponseType::Opaqueredirect) { + autoCancel.SetCancelMessage("BadOpaqueRedirectInterceptionWithURL"_ns, + requestURL); + return; + } + + if (requestRedirectMode != RequestRedirect::Follow && + response->Redirected()) { + autoCancel.SetCancelMessage("BadRedirectModeInterceptionWithURL"_ns, + requestURL); + return; + } + + if (NS_WARN_IF(response->BodyUsed())) { + autoCancel.SetCancelMessage("InterceptedUsedResponseWithURL"_ns, + requestURL); + return; + } + + SafeRefPtr<InternalResponse> ir = response->GetInternalResponse(); + if (NS_WARN_IF(!ir)) { + return; + } + + // An extra safety check to make sure our invariant that opaque and cors + // responses always have a URL does not break. + if (NS_WARN_IF((response->Type() == ResponseType::Opaque || + response->Type() == ResponseType::Cors) && + ir->GetUnfilteredURL().IsEmpty())) { + MOZ_DIAGNOSTIC_ASSERT(false, "Cors or opaque Response without a URL"); + return; + } + + if (requestMode == RequestMode::Same_origin && + response->Type() == ResponseType::Cors) { + Telemetry::ScalarAdd(Telemetry::ScalarID::SW_CORS_RES_FOR_SO_REQ_COUNT, 1); + + // XXXtt: Will have a pref to enable the quirk response in bug 1419684. + // The variadic template provided by StringArrayAppender requires exactly + // an nsString. + NS_ConvertUTF8toUTF16 responseURL(ir->GetUnfilteredURL()); + autoCancel.SetCancelMessage("CorsResponseForSameOriginRequest"_ns, + requestURL, responseURL); + return; + } + + nsCOMPtr<nsIInputStream> body; + ir->GetUnfilteredBody(getter_AddRefs(body)); + // Errors and redirects may not have a body. + if (body) { + ErrorResult error; + response->SetBodyUsed(aCx, error); + error.WouldReportJSException(); + if (NS_WARN_IF(error.Failed())) { + autoCancel.SetCancelErrorResult(aCx, error); + return; + } + } + + if (!ir->GetChannelInfo().IsInitialized()) { + // This is a synthetic response (I think and hope so). + ir->InitChannelInfo(worker->GetChannelInfo()); + } + + autoCancel.Reset(); + + // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm Step 26: If + // eventHandled is not null, then resolve eventHandled. + // + // mRespondWithPromiseHolder will resolve a MozPromise that will resolve on + // the worker owner's thread, so it's fine to resolve the mHandled promise now + // because content will not interfere with respondWith getting the Response to + // where it's going. + mHandled->MaybeResolveWithUndefined(); + mRespondWithPromiseHolder.Resolve( + FetchEventRespondWithResult(std::make_tuple( + std::move(ir), mRespondWithClosure.ref(), + FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))), + __func__); +} + +void FetchEventOp::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(mRespondWithClosure); + MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + mFetchHandlerFinish = TimeStamp::Now(); + + FetchEventRespondWithClosure& closure = mRespondWithClosure.ref(); + + nsCString sourceSpec = closure.respondWithScriptSpec(); + uint32_t line = closure.respondWithLineNumber(); + uint32_t column = closure.respondWithColumnNumber(); + nsString valueString; + + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); + + nsString requestURL; + GetRequestURL(requestURL); + + AsyncLog(sourceSpec, line, column, "InterceptionRejectedResponseWithURL"_ns, + {std::move(requestURL), valueString}); + + // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm Step 25.1: + // If eventHandled is not null, then reject eventHandled with a "NetworkError" + // DOMException in workerRealm. + mHandled->MaybeRejectWithNetworkError( + "FetchEvent.respondWith() Promise rejected"_ns); + mRespondWithPromiseHolder.Resolve( + FetchEventRespondWithResult(CancelInterceptionArgs( + NS_ERROR_INTERCEPTION_FAILED, + FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))), + __func__); +} + +nsresult FetchEventOp::DispatchFetchEvent(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + + ParentToChildServiceWorkerFetchEventOpArgs& args = + mArgs.get_ParentToChildServiceWorkerFetchEventOpArgs(); + + /** + * Testing: Failure injection. + * + * There are a number of different ways that this fetch event could have + * failed that would result in cancellation. This injection point helps + * simulate them without worrying about shifting implementation details with + * full fidelity reproductions of current scenarios. + * + * Broadly speaking, we expect fetch event scenarios to fail because of: + * - Script load failure, which results in the CompileScriptRunnable closing + * the worker and thereby cancelling all pending operations, including this + * fetch. The `ServiceWorkerOp::Cancel` impl just calls + * RejectAll(NS_ERROR_DOM_ABORT_ERR) which we are able to approximate by + * returning the same nsresult here, as our caller also calls RejectAll. + * (And timing-wise, this rejection will happen in the correct sequence.) + * - An exception gets thrown in the processing of the promise that was passed + * to respondWith and it ends up rejecting. The rejection will be converted + * by `FetchEventOp::RejectedCallback` into a cancellation with + * NS_ERROR_INTERCEPTION_FAILED, and by returning that here we approximate + * that failure mode. + */ + if (NS_FAILED(args.common().testingInjectCancellation())) { + return args.common().testingInjectCancellation(); + } + + /** + * Step 1: get the InternalRequest. The InternalRequest can't be constructed + * here from mArgs because the IPCStream has to be deserialized on the + * thread receiving the ServiceWorkerFetchEventOpArgs. + * FetchEventOpProxyChild will have already deserialized the stream on the + * correct thread before creating this op, so we can take its saved + * InternalRequest. + */ + SafeRefPtr<InternalRequest> internalRequest = + mActor->ExtractInternalRequest(); + + /** + * Step 2: get the worker's global object + */ + GlobalObject globalObject(aCx, aWorkerPrivate->GlobalScope()->GetWrapper()); + nsCOMPtr<nsIGlobalObject> globalObjectAsSupports = + do_QueryInterface(globalObject.GetAsSupports()); + if (NS_WARN_IF(!globalObjectAsSupports)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + /** + * Step 3: create the public DOM Request object + * TODO: this Request object should be created with an AbortSignal object + * which should be aborted if the loading is aborted. See but 1394102. + */ + RefPtr<Request> request = + new Request(globalObjectAsSupports, internalRequest.clonePtr(), nullptr); + MOZ_ASSERT_IF(internalRequest->IsNavigationRequest(), + request->Redirect() == RequestRedirect::Manual); + + /** + * Step 4a: create the FetchEventInit + */ + RootedDictionary<FetchEventInit> fetchEventInit(aCx); + fetchEventInit.mRequest = request; + fetchEventInit.mBubbles = false; + fetchEventInit.mCancelable = true; + + /** + * TODO: only expose the FetchEvent.clientId on subresource requests for + * now. Once we implement .targetClientId we can then start exposing + * .clientId on non-subresource requests as well. See bug 1487534. + */ + if (!args.common().clientId().IsEmpty() && + !internalRequest->IsNavigationRequest()) { + fetchEventInit.mClientId = args.common().clientId(); + } + + /* + * https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm + * + * "If request is a non-subresource request and request’s + * destination is not "report", initialize e’s resultingClientId attribute + * to reservedClient’s [resultingClient's] id, and to the empty string + * otherwise." (Step 18.8) + */ + if (!args.common().resultingClientId().IsEmpty() && + args.common().isNonSubresourceRequest() && + internalRequest->Destination() != RequestDestination::Report) { + fetchEventInit.mResultingClientId = args.common().resultingClientId(); + } + + /** + * Step 4b: create the FetchEvent + */ + RefPtr<FetchEvent> fetchEvent = + FetchEvent::Constructor(globalObject, u"fetch"_ns, fetchEventInit); + fetchEvent->SetTrusted(true); + fetchEvent->PostInit(args.common().workerScriptSpec(), this); + mHandled = fetchEvent->Handled(); + mPreloadResponse = fetchEvent->PreloadResponse(); + + if (args.common().preloadNavigation()) { + RefPtr<FetchEventPreloadResponseAvailablePromise> preloadResponsePromise = + mActor->GetPreloadResponseAvailablePromise(); + MOZ_ASSERT(preloadResponsePromise); + + // If preloadResponsePromise has already settled then this callback will get + // run synchronously here. + RefPtr<FetchEventOp> self = this; + preloadResponsePromise + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, globalObjectAsSupports]( + SafeRefPtr<InternalResponse>&& aPreloadResponse) { + self->mPreloadResponse->MaybeResolve( + MakeRefPtr<Response>(globalObjectAsSupports, + std::move(aPreloadResponse), nullptr)); + self->mPreloadResponseAvailablePromiseRequestHolder.Complete(); + }, + [self](int) { + self->mPreloadResponseAvailablePromiseRequestHolder.Complete(); + }) + ->Track(mPreloadResponseAvailablePromiseRequestHolder); + + RefPtr<PerformanceStorage> performanceStorage = + aWorkerPrivate->GetPerformanceStorage(); + + RefPtr<FetchEventPreloadResponseTimingPromise> + preloadResponseTimingPromise = + mActor->GetPreloadResponseTimingPromise(); + MOZ_ASSERT(preloadResponseTimingPromise); + preloadResponseTimingPromise + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, performanceStorage, + globalObjectAsSupports](ResponseTiming&& aTiming) { + if (performanceStorage && !aTiming.entryName().IsEmpty() && + aTiming.initiatorType().Equals(u"navigation"_ns)) { + performanceStorage->AddEntry( + aTiming.entryName(), aTiming.initiatorType(), + MakeUnique<PerformanceTimingData>(aTiming.timingData())); + } + self->mPreloadResponseTimingPromiseRequestHolder.Complete(); + }, + [self](int) { + self->mPreloadResponseTimingPromiseRequestHolder.Complete(); + }) + ->Track(mPreloadResponseTimingPromiseRequestHolder); + + RefPtr<FetchEventPreloadResponseEndPromise> preloadResponseEndPromise = + mActor->GetPreloadResponseEndPromise(); + MOZ_ASSERT(preloadResponseEndPromise); + preloadResponseEndPromise + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, globalObjectAsSupports](ResponseEndArgs&& aArgs) { + if (aArgs.endReason() == FetchDriverObserver::eAborted) { + self->mPreloadResponse->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + self->mPreloadResponseEndPromiseRequestHolder.Complete(); + }, + [self](int) { + self->mPreloadResponseEndPromiseRequestHolder.Complete(); + }) + ->Track(mPreloadResponseEndPromiseRequestHolder); + } else { + // preload navigation is disabled, resolved preload response promise with + // undefined as default behavior. + mPreloadResponse->MaybeResolveWithUndefined(); + } + + mFetchHandlerStart = TimeStamp::Now(); + + /** + * Step 5: Dispatch the FetchEvent to the worker's global object + */ + nsresult rv = DispatchExtendableEventOnWorkerScope( + aCx, aWorkerPrivate->GlobalScope(), fetchEvent, this); + bool dispatchFailed = NS_FAILED(rv) && rv != NS_ERROR_XPC_JS_THREW_EXCEPTION; + + if (NS_WARN_IF(dispatchFailed)) { + mHandled = nullptr; + mPreloadResponse = nullptr; + return rv; + } + + /** + * At this point, there are 4 (legal) scenarios: + * + * 1) If neither waitUntil() nor respondWith() are called, + * DispatchExtendableEventOnWorkerScope() will have already called + * FinishedWithResult(), but this call will have recorded the result + * (mResult) and returned early so that mRespondWithPromiseHolder can be + * settled first. mRespondWithPromiseHolder will be settled below, followed + * by a call to MaybeFinished() which settles mPromiseHolder. + * + * 2) If waitUntil() is called at least once, and respondWith() is not + * called, DispatchExtendableEventOnWorkerScope() will NOT have called + * FinishedWithResult(). We'll settle mRespondWithPromiseHolder first, and + * at some point in the future when the last waitUntil() promise settles, + * FinishedWithResult() will be called, settling mPromiseHolder. + * + * 3) If waitUntil() is not called, and respondWith() is called, + * DispatchExtendableEventOnWorkerScope() will NOT have called + * FinishedWithResult(). We can also guarantee that + * mRespondWithPromiseHolder will be settled before mPromiseHolder, due to + * the Promise::AppendNativeHandler() call ordering in + * FetchEvent::RespondWith(). + * + * 4) If waitUntil() is called at least once, and respondWith() is also + * called, the effect is similar to scenario 3), with the most imporant + * property being mRespondWithPromiseHolder settling before mPromiseHolder. + * + * Note that if mPromiseHolder is settled before mRespondWithPromiseHolder, + * FetchEventOpChild will cancel the interception. + */ + if (!fetchEvent->WaitToRespond()) { + MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty()); + MOZ_ASSERT(!aWorkerPrivate->UsesSystemPrincipal(), + "We don't support system-principal serviceworkers"); + + mFetchHandlerFinish = TimeStamp::Now(); + + if (fetchEvent->DefaultPrevented(CallerType::NonSystem)) { + // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm + // Step 24.1.1: If eventHandled is not null, then reject eventHandled with + // a "NetworkError" DOMException in workerRealm. + mHandled->MaybeRejectWithNetworkError( + "FetchEvent.preventDefault() called"_ns); + mRespondWithPromiseHolder.Resolve( + FetchEventRespondWithResult(CancelInterceptionArgs( + NS_ERROR_INTERCEPTION_FAILED, + FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))), + __func__); + } else { + // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm + // Step 24.2: If eventHandled is not null, then resolve eventHandled. + mHandled->MaybeResolveWithUndefined(); + mRespondWithPromiseHolder.Resolve( + FetchEventRespondWithResult(ResetInterceptionArgs( + FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))), + __func__); + } + } else { + MOZ_ASSERT(mRespondWithClosure); + } + + mPostDispatchChecksDone = true; + MaybeFinished(); + + return NS_OK; +} + +class ExtensionAPIEventOp final : public ServiceWorkerOp { + using ServiceWorkerOp::ServiceWorkerOp; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExtensionAPIEventOp, override) + + private: + ~ExtensionAPIEventOp() = default; + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(aWorkerPrivate->ExtensionAPIAllowed()); + MOZ_ASSERT(!mPromiseHolder.IsEmpty()); + + ServiceWorkerExtensionAPIEventOpArgs& args = + mArgs.get_ServiceWorkerExtensionAPIEventOpArgs(); + + ServiceWorkerExtensionAPIEventOpResult result; + result.extensionAPIEventListenerWasAdded() = false; + + if (aWorkerPrivate->WorkerScriptExecutedSuccessfully()) { + GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper()); + RefPtr<ServiceWorkerGlobalScope> scope; + UNWRAP_OBJECT(ServiceWorkerGlobalScope, globalObj.Get(), scope); + SafeRefPtr<extensions::ExtensionBrowser> extensionAPI = + scope->AcquireExtensionBrowser(); + if (!extensionAPI) { + // If the worker script did never access the WebExtension APIs + // then we can return earlier, no event listener could have been added. + mPromiseHolder.Resolve(result, __func__); + return true; + } + // Check if a listener has been subscribed on the expected WebExtensions + // API event. + bool hasWakeupListener = extensionAPI->HasWakeupEventListener( + args.apiNamespace(), args.apiEventName()); + result.extensionAPIEventListenerWasAdded() = hasWakeupListener; + mPromiseHolder.Resolve(result, __func__); + } else { + mPromiseHolder.Resolve(result, __func__); + } + + return true; + } +}; + +/* static */ already_AddRefed<ServiceWorkerOp> ServiceWorkerOp::Create( + ServiceWorkerOpArgs&& aArgs, + std::function<void(const ServiceWorkerOpResult&)>&& aCallback) { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + + RefPtr<ServiceWorkerOp> op; + + switch (aArgs.type()) { + case ServiceWorkerOpArgs::TServiceWorkerCheckScriptEvaluationOpArgs: + op = MakeRefPtr<CheckScriptEvaluationOp>(std::move(aArgs), + std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerUpdateStateOpArgs: + op = MakeRefPtr<UpdateServiceWorkerStateOp>(std::move(aArgs), + std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs: + op = MakeRefPtr<TerminateServiceWorkerOp>(std::move(aArgs), + std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerLifeCycleEventOpArgs: + op = MakeRefPtr<LifeCycleEventOp>(std::move(aArgs), std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerPushEventOpArgs: + op = MakeRefPtr<PushEventOp>(std::move(aArgs), std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerPushSubscriptionChangeEventOpArgs: + op = MakeRefPtr<PushSubscriptionChangeEventOp>(std::move(aArgs), + std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerNotificationEventOpArgs: + op = MakeRefPtr<NotificationEventOp>(std::move(aArgs), + std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerMessageEventOpArgs: + op = MakeRefPtr<MessageEventOp>(std::move(aArgs), std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TParentToChildServiceWorkerFetchEventOpArgs: + op = MakeRefPtr<FetchEventOp>(std::move(aArgs), std::move(aCallback)); + break; + case ServiceWorkerOpArgs::TServiceWorkerExtensionAPIEventOpArgs: + op = MakeRefPtr<ExtensionAPIEventOp>(std::move(aArgs), + std::move(aCallback)); + break; + default: + MOZ_CRASH("Unknown Service Worker operation!"); + return nullptr; + } + + return op.forget(); +} + +} // namespace mozilla::dom |