diff options
Diffstat (limited to 'dom/serviceworkers')
487 files changed, 45632 insertions, 0 deletions
diff --git a/dom/serviceworkers/FetchEventOpChild.cpp b/dom/serviceworkers/FetchEventOpChild.cpp new file mode 100644 index 0000000000..e7e068a864 --- /dev/null +++ b/dom/serviceworkers/FetchEventOpChild.cpp @@ -0,0 +1,600 @@ +/* -*- 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 "FetchEventOpChild.h" + +#include <utility> + +#include "MainThreadUtils.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIChannel.h" +#include "nsIConsoleReportCollector.h" +#include "nsIContentPolicy.h" +#include "nsIInputStream.h" +#include "nsILoadInfo.h" +#include "nsINetworkInterceptController.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsISupportsImpl.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +#include "ServiceWorkerPrivate.h" +#include "mozilla/Assertions.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/dom/FetchService.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/PRemoteWorkerControllerChild.h" +#include "mozilla/dom/ServiceWorkerRegistrationInfo.h" +#include "mozilla/net/NeckoChannelParams.h" + +namespace mozilla::dom { + +namespace { + +bool CSPPermitsResponse(nsILoadInfo* aLoadInfo, + SafeRefPtr<InternalResponse> aResponse, + const nsACString& aWorkerScriptSpec) { + AssertIsOnMainThread(); + MOZ_ASSERT(aLoadInfo); + + nsCString url = aResponse->GetUnfilteredURL(); + if (url.IsEmpty()) { + // Synthetic response. + url = aWorkerScriptSpec; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), url, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + int16_t decision = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(uri, aLoadInfo, ""_ns, &decision); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return decision == nsIContentPolicy::ACCEPT; +} + +void AsyncLog(nsIInterceptedChannel* aChannel, const nsACString& aScriptSpec, + uint32_t aLineNumber, uint32_t aColumnNumber, + const nsACString& aMessageName, nsTArray<nsString>&& aParams) { + AssertIsOnMainThread(); + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsIConsoleReportCollector> reporter = + aChannel->GetConsoleReportCollector(); + + if (reporter) { + // NOTE: is appears that `const nsTArray<nsString>&` is required for + // nsIConsoleReportCollector::AddConsoleReport to resolve to the correct + // overload. + const nsTArray<nsString> params = std::move(aParams); + + reporter->AddConsoleReport( + nsIScriptError::errorFlag, "Service Worker Interception"_ns, + nsContentUtils::eDOM_PROPERTIES, aScriptSpec, aLineNumber, + aColumnNumber, aMessageName, params); + } +} + +class SynthesizeResponseWatcher final : public nsIInterceptedBodyCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + SynthesizeResponseWatcher( + const nsMainThreadPtrHandle<nsIInterceptedChannel>& aInterceptedChannel, + const nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + const bool aIsNonSubresourceRequest, + FetchEventRespondWithClosure&& aClosure, nsAString&& aRequestURL) + : mInterceptedChannel(aInterceptedChannel), + mRegistration(aRegistration), + mIsNonSubresourceRequest(aIsNonSubresourceRequest), + mClosure(std::move(aClosure)), + mRequestURL(std::move(aRequestURL)) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + MOZ_ASSERT(mRegistration); + } + + NS_IMETHOD + BodyComplete(nsresult aRv) override { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + + if (NS_WARN_IF(NS_FAILED(aRv))) { + AsyncLog(mInterceptedChannel, mClosure.respondWithScriptSpec(), + mClosure.respondWithLineNumber(), + mClosure.respondWithColumnNumber(), + "InterceptionFailedWithURL"_ns, {mRequestURL}); + + CancelInterception(NS_ERROR_INTERCEPTION_FAILED); + + return NS_OK; + } + + nsresult rv = mInterceptedChannel->FinishSynthesizedResponse(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + CancelInterception(rv); + } + + mInterceptedChannel = nullptr; + + return NS_OK; + } + + // See FetchEventOpChild::MaybeScheduleRegistrationUpdate() for comments. + void CancelInterception(nsresult aStatus) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + MOZ_ASSERT(mRegistration); + + mInterceptedChannel->CancelInterception(aStatus); + + if (mIsNonSubresourceRequest) { + mRegistration->MaybeScheduleUpdate(); + } else { + mRegistration->MaybeScheduleTimeCheckAndUpdate(); + } + + mInterceptedChannel = nullptr; + mRegistration = nullptr; + } + + private: + ~SynthesizeResponseWatcher() { + if (NS_WARN_IF(mInterceptedChannel)) { + CancelInterception(NS_ERROR_DOM_ABORT_ERR); + } + } + + nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + const bool mIsNonSubresourceRequest; + const FetchEventRespondWithClosure mClosure; + const nsString mRequestURL; +}; + +NS_IMPL_ISUPPORTS(SynthesizeResponseWatcher, nsIInterceptedBodyCallback) + +} // anonymous namespace + +/* static */ RefPtr<GenericPromise> FetchEventOpChild::SendFetchEvent( + PRemoteWorkerControllerChild* aManager, + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel> aInterceptedChannel, + RefPtr<ServiceWorkerRegistrationInfo> aRegistration, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises, + RefPtr<KeepAliveToken>&& aKeepAliveToken) { + AssertIsOnMainThread(); + MOZ_ASSERT(aManager); + MOZ_ASSERT(aInterceptedChannel); + MOZ_ASSERT(aKeepAliveToken); + + FetchEventOpChild* actor = new FetchEventOpChild( + std::move(aArgs), std::move(aInterceptedChannel), + std::move(aRegistration), std::move(aPreloadResponseReadyPromises), + std::move(aKeepAliveToken)); + + actor->mWasSent = true; + Unused << aManager->SendPFetchEventOpConstructor(actor, actor->mArgs); + + return actor->mPromiseHolder.Ensure(__func__); +} + +FetchEventOpChild::~FetchEventOpChild() { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannelHandled); + MOZ_DIAGNOSTIC_ASSERT(mPromiseHolder.IsEmpty()); +} + +FetchEventOpChild::FetchEventOpChild( + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel>&& aInterceptedChannel, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises, + RefPtr<KeepAliveToken>&& aKeepAliveToken) + : mArgs(std::move(aArgs)), + mInterceptedChannel(std::move(aInterceptedChannel)), + mRegistration(std::move(aRegistration)), + mKeepAliveToken(std::move(aKeepAliveToken)), + mPreloadResponseReadyPromises(std::move(aPreloadResponseReadyPromises)) { + if (mPreloadResponseReadyPromises) { + // This promise should be configured to use synchronous dispatch, so if it's + // already resolved when we run this code then the callback will be called + // synchronously and pass the preload response with the constructor message. + // + // Note that it's fine to capture the this pointer in the callbacks because + // we disconnect the request in Recv__delete__(). + mPreloadResponseReadyPromises->GetResponseAvailablePromise() + ->Then( + GetCurrentSerialEventTarget(), __func__, + [this](FetchServiceResponse&& aResponse) { + if (!mWasSent) { + // The actor wasn't sent yet, we can still send the preload + // response with it. + mArgs.preloadResponse() = + Some(aResponse->ToParentToParentInternalResponse()); + } else { + // It's too late to send the preload response with the actor, we + // have to send it in a separate message. + SendPreloadResponse( + aResponse->ToParentToParentInternalResponse()); + } + mPreloadResponseAvailablePromiseRequestHolder.Complete(); + }, + [this](const CopyableErrorResult&) { + mPreloadResponseAvailablePromiseRequestHolder.Complete(); + }) + ->Track(mPreloadResponseAvailablePromiseRequestHolder); + + mPreloadResponseReadyPromises->GetResponseEndPromise() + ->Then( + GetCurrentSerialEventTarget(), __func__, + [this](ResponseEndArgs&& aResponse) { + if (!mWasSent) { + // The actor wasn't sent yet, we can still send the preload + // response end args with it. + mArgs.preloadResponseEndArgs() = Some(std::move(aResponse)); + } else { + // It's too late to send the preload response end with the + // actor, we have to send it in a separate message. + SendPreloadResponseEnd(aResponse); + } + mPreloadResponseReadyPromises = nullptr; + mPreloadResponseEndPromiseRequestHolder.Complete(); + }, + [this](const CopyableErrorResult&) { + mPreloadResponseReadyPromises = nullptr; + mPreloadResponseEndPromiseRequestHolder.Complete(); + }) + ->Track(mPreloadResponseEndPromiseRequestHolder); + } +} + +mozilla::ipc::IPCResult FetchEventOpChild::RecvAsyncLog( + const nsCString& aScriptSpec, const uint32_t& aLineNumber, + const uint32_t& aColumnNumber, const nsCString& aMessageName, + nsTArray<nsString>&& aParams) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + + AsyncLog(mInterceptedChannel, aScriptSpec, aLineNumber, aColumnNumber, + aMessageName, std::move(aParams)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchEventOpChild::RecvRespondWith( + ParentToParentFetchEventRespondWithResult&& aResult) { + AssertIsOnMainThread(); + + switch (aResult.type()) { + case ParentToParentFetchEventRespondWithResult:: + TParentToParentSynthesizeResponseArgs: + mInterceptedChannel->SetFetchHandlerStart( + aResult.get_ParentToParentSynthesizeResponseArgs() + .timeStamps() + .fetchHandlerStart()); + mInterceptedChannel->SetFetchHandlerFinish( + aResult.get_ParentToParentSynthesizeResponseArgs() + .timeStamps() + .fetchHandlerFinish()); + SynthesizeResponse( + std::move(aResult.get_ParentToParentSynthesizeResponseArgs())); + break; + case ParentToParentFetchEventRespondWithResult::TResetInterceptionArgs: + mInterceptedChannel->SetFetchHandlerStart( + aResult.get_ResetInterceptionArgs().timeStamps().fetchHandlerStart()); + mInterceptedChannel->SetFetchHandlerFinish( + aResult.get_ResetInterceptionArgs() + .timeStamps() + .fetchHandlerFinish()); + ResetInterception(false); + break; + case ParentToParentFetchEventRespondWithResult::TCancelInterceptionArgs: + mInterceptedChannel->SetFetchHandlerStart( + aResult.get_CancelInterceptionArgs() + .timeStamps() + .fetchHandlerStart()); + mInterceptedChannel->SetFetchHandlerFinish( + aResult.get_CancelInterceptionArgs() + .timeStamps() + .fetchHandlerFinish()); + CancelInterception(aResult.get_CancelInterceptionArgs().status()); + break; + default: + MOZ_CRASH("Unknown IPCFetchEventRespondWithResult type!"); + break; + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchEventOpChild::Recv__delete__( + const ServiceWorkerFetchEventOpResult& aResult) { + AssertIsOnMainThread(); + MOZ_ASSERT(mRegistration); + + if (NS_WARN_IF(!mInterceptedChannelHandled)) { + MOZ_ASSERT(NS_FAILED(aResult.rv())); + NS_WARNING( + "Failed to handle intercepted network request; canceling " + "interception!"); + + CancelInterception(aResult.rv()); + } + + mPromiseHolder.ResolveIfExists(true, __func__); + + // FetchEvent is completed. + // Disconnect preload response related promises and cancel the preload. + mPreloadResponseAvailablePromiseRequestHolder.DisconnectIfExists(); + mPreloadResponseEndPromiseRequestHolder.DisconnectIfExists(); + if (mPreloadResponseReadyPromises) { + RefPtr<FetchService> fetchService = FetchService::GetInstance(); + fetchService->CancelFetch(std::move(mPreloadResponseReadyPromises)); + } + + /** + * This corresponds to the "Fire Functional Event" algorithm's step 9: + * + * "If the time difference in seconds calculated by the current time minus + * registration's last update check time is greater than 84600, invoke Soft + * Update algorithm with registration." + * + * TODO: this is probably being called later than it should be; it should be + * called ASAP after dispatching the FetchEvent. + */ + mRegistration->MaybeScheduleTimeCheckAndUpdate(); + + return IPC_OK(); +} + +void FetchEventOpChild::ActorDestroy(ActorDestroyReason) { + AssertIsOnMainThread(); + + // If `Recv__delete__` was called, it would have resolved the promise already. + mPromiseHolder.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__); + + if (NS_WARN_IF(!mInterceptedChannelHandled)) { + Unused << Recv__delete__(NS_ERROR_DOM_ABORT_ERR); + } +} + +nsresult FetchEventOpChild::StartSynthesizedResponse( + ParentToParentSynthesizeResponseArgs&& aArgs) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + MOZ_ASSERT(!mInterceptedChannelHandled); + MOZ_ASSERT(mRegistration); + + /** + * TODO: moving the IPCInternalResponse won't do anything right now because + * there isn't a prefect-forwarding or rvalue-ref-parameter overload of + * `InternalResponse::FromIPC().` + */ + SafeRefPtr<InternalResponse> response = + InternalResponse::FromIPC(aArgs.internalResponse()); + if (NS_WARN_IF(!response)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIChannel> underlyingChannel; + nsresult rv = + mInterceptedChannel->GetChannel(getter_AddRefs(underlyingChannel)); + if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!underlyingChannel)) { + return NS_FAILED(rv) ? rv : NS_ERROR_FAILURE; + } + + nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->LoadInfo(); + if (!CSPPermitsResponse(loadInfo, response.clonePtr(), + mArgs.common().workerScriptSpec())) { + return NS_ERROR_CONTENT_BLOCKED; + } + + MOZ_ASSERT(response->GetChannelInfo().IsInitialized()); + ChannelInfo channelInfo = response->GetChannelInfo(); + rv = mInterceptedChannel->SetChannelInfo(&channelInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_INTERCEPTION_FAILED; + } + + rv = mInterceptedChannel->SynthesizeStatus( + response->GetUnfilteredStatus(), response->GetUnfilteredStatusText()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AutoTArray<InternalHeaders::Entry, 5> entries; + response->UnfilteredHeaders()->GetEntries(entries); + for (auto& entry : entries) { + mInterceptedChannel->SynthesizeHeader(entry.mName, entry.mValue); + } + + auto castLoadInfo = static_cast<mozilla::net::LoadInfo*>(loadInfo.get()); + castLoadInfo->SynthesizeServiceWorkerTainting(response->GetTainting()); + + // Get the preferred alternative data type of the outer channel + nsAutoCString preferredAltDataType(""_ns); + nsCOMPtr<nsICacheInfoChannel> outerChannel = + do_QueryInterface(underlyingChannel); + if (outerChannel && + !outerChannel->PreferredAlternativeDataTypes().IsEmpty()) { + preferredAltDataType.Assign( + outerChannel->PreferredAlternativeDataTypes()[0].type()); + } + + nsCOMPtr<nsIInputStream> body; + if (preferredAltDataType.Equals(response->GetAlternativeDataType())) { + body = response->TakeAlternativeBody(); + } + if (!body) { + response->GetUnfilteredBody(getter_AddRefs(body)); + } else { + Telemetry::ScalarAdd(Telemetry::ScalarID::SW_ALTERNATIVE_BODY_USED_COUNT, + 1); + } + + // Propagate the URL to the content if the request mode is not "navigate". + // Note that, we only reflect the final URL if the response.redirected is + // false. We propagate all the URLs if the response.redirected is true. + const IPCInternalRequest& request = mArgs.common().internalRequest(); + nsAutoCString responseURL; + if (request.requestMode() != RequestMode::Navigate) { + responseURL = response->GetUnfilteredURL(); + + // Similar to how we apply the request fragment to redirects automatically + // we also want to apply it automatically when propagating the response + // URL from a service worker interception. Currently response.url strips + // the fragment, so this will never conflict with an existing fragment + // on the response. In the future we will have to check for a response + // fragment and avoid overriding in that case. + if (!request.fragment().IsEmpty() && !responseURL.IsEmpty()) { + MOZ_ASSERT(!responseURL.Contains('#')); + responseURL.AppendLiteral("#"); + responseURL.Append(request.fragment()); + } + } + + nsMainThreadPtrHandle<nsIInterceptedChannel> interceptedChannel( + new nsMainThreadPtrHolder<nsIInterceptedChannel>( + "nsIInterceptedChannel", mInterceptedChannel, false)); + + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> registration( + new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>( + "ServiceWorkerRegistrationInfo", mRegistration, false)); + + nsCString requestURL = request.urlList().LastElement(); + if (!request.fragment().IsEmpty()) { + requestURL.AppendLiteral("#"); + requestURL.Append(request.fragment()); + } + + RefPtr<SynthesizeResponseWatcher> watcher = new SynthesizeResponseWatcher( + interceptedChannel, registration, + mArgs.common().isNonSubresourceRequest(), std::move(aArgs.closure()), + NS_ConvertUTF8toUTF16(responseURL)); + + rv = mInterceptedChannel->StartSynthesizedResponse( + body, watcher, nullptr /* TODO */, responseURL, response->IsRedirected()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(underlyingChannel, + "service-worker-synthesized-response", nullptr); + } + + return rv; +} + +void FetchEventOpChild::SynthesizeResponse( + ParentToParentSynthesizeResponseArgs&& aArgs) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + MOZ_ASSERT(!mInterceptedChannelHandled); + + nsresult rv = StartSynthesizedResponse(std::move(aArgs)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_WARNING("Failed to synthesize response!"); + + mInterceptedChannel->CancelInterception(rv); + } + + mInterceptedChannelHandled = true; + + MaybeScheduleRegistrationUpdate(); +} + +void FetchEventOpChild::ResetInterception(bool aBypass) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + MOZ_ASSERT(!mInterceptedChannelHandled); + + nsresult rv = mInterceptedChannel->ResetInterception(aBypass); + + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_WARNING("Failed to resume intercepted network request!"); + + mInterceptedChannel->CancelInterception(rv); + } + + mInterceptedChannelHandled = true; + + MaybeScheduleRegistrationUpdate(); +} + +void FetchEventOpChild::CancelInterception(nsresult aStatus) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInterceptedChannel); + MOZ_ASSERT(!mInterceptedChannelHandled); + MOZ_ASSERT(NS_FAILED(aStatus)); + + // Report a navigation fault if this is a navigation (and we have an active + // worker, which should be the case in non-shutdown/content-process-crash + // situations). + RefPtr<ServiceWorkerInfo> mActive = mRegistration->GetActive(); + if (mActive && mArgs.common().isNonSubresourceRequest()) { + mActive->ReportNavigationFault(); + // Additional mitigations such as unregistering the registration are handled + // in ServiceWorkerRegistrationInfo::MaybeScheduleUpdate which will be + // called by MaybeScheduleRegistrationUpdate which gets called by our call + // to ResetInterception. + if (StaticPrefs::dom_serviceWorkers_mitigations_bypass_on_fault()) { + ResetInterception(true); + return; + } + } + + mInterceptedChannel->CancelInterception(aStatus); + mInterceptedChannelHandled = true; + + MaybeScheduleRegistrationUpdate(); +} + +/** + * This corresponds to the "Handle Fetch" algorithm's steps 20.3, 21.2, and + * 22.2: + * + * "If request is a non-subresource request, or request is a subresource + * request and the time difference in seconds calculated by the current time + * minus registration's last update check time is greater than 86400, invoke + * Soft Update algorithm with registration." + */ +void FetchEventOpChild::MaybeScheduleRegistrationUpdate() const { + AssertIsOnMainThread(); + MOZ_ASSERT(mRegistration); + MOZ_ASSERT(mInterceptedChannelHandled); + + if (mArgs.common().isNonSubresourceRequest()) { + mRegistration->MaybeScheduleUpdate(); + } else { + mRegistration->MaybeScheduleTimeCheckAndUpdate(); + } +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/FetchEventOpChild.h b/dom/serviceworkers/FetchEventOpChild.h new file mode 100644 index 0000000000..13eecd3415 --- /dev/null +++ b/dom/serviceworkers/FetchEventOpChild.h @@ -0,0 +1,92 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_fetcheventopchild_h__ +#define mozilla_dom_fetcheventopchild_h__ + +#include "nsCOMPtr.h" + +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/FetchService.h" +#include "mozilla/dom/PFetchEventOpChild.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" + +class nsIInterceptedChannel; + +namespace mozilla::dom { + +class KeepAliveToken; +class PRemoteWorkerControllerChild; +class ServiceWorkerRegistrationInfo; + +/** + * FetchEventOpChild represents an in-flight FetchEvent operation. + */ +class FetchEventOpChild final : public PFetchEventOpChild { + friend class PFetchEventOpChild; + + public: + static RefPtr<GenericPromise> SendFetchEvent( + PRemoteWorkerControllerChild* aManager, + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel> aInterceptedChannel, + RefPtr<ServiceWorkerRegistrationInfo> aRegistrationInfo, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises, + RefPtr<KeepAliveToken>&& aKeepAliveToken); + + ~FetchEventOpChild(); + + private: + FetchEventOpChild( + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel>&& aInterceptedChannel, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistrationInfo, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises, + RefPtr<KeepAliveToken>&& aKeepAliveToken); + + mozilla::ipc::IPCResult RecvAsyncLog(const nsCString& aScriptSpec, + const uint32_t& aLineNumber, + const uint32_t& aColumnNumber, + const nsCString& aMessageName, + nsTArray<nsString>&& aParams); + + mozilla::ipc::IPCResult RecvRespondWith( + ParentToParentFetchEventRespondWithResult&& aResult); + + mozilla::ipc::IPCResult Recv__delete__( + const ServiceWorkerFetchEventOpResult& aResult); + + void ActorDestroy(ActorDestroyReason) override; + + nsresult StartSynthesizedResponse( + ParentToParentSynthesizeResponseArgs&& aArgs); + + void SynthesizeResponse(ParentToParentSynthesizeResponseArgs&& aArgs); + + void ResetInterception(bool aBypass); + + void CancelInterception(nsresult aStatus); + + void MaybeScheduleRegistrationUpdate() const; + + ParentToParentServiceWorkerFetchEventOpArgs mArgs; + nsCOMPtr<nsIInterceptedChannel> mInterceptedChannel; + RefPtr<ServiceWorkerRegistrationInfo> mRegistration; + RefPtr<KeepAliveToken> mKeepAliveToken; + bool mInterceptedChannelHandled = false; + MozPromiseHolder<GenericPromise> mPromiseHolder; + bool mWasSent = false; + MozPromiseRequestHolder<FetchServiceResponseAvailablePromise> + mPreloadResponseAvailablePromiseRequestHolder; + MozPromiseRequestHolder<FetchServiceResponseEndPromise> + mPreloadResponseEndPromiseRequestHolder; + RefPtr<FetchServicePromises> mPreloadResponseReadyPromises; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_fetcheventopchild_h__ diff --git a/dom/serviceworkers/FetchEventOpParent.cpp b/dom/serviceworkers/FetchEventOpParent.cpp new file mode 100644 index 0000000000..41dc3d77d0 --- /dev/null +++ b/dom/serviceworkers/FetchEventOpParent.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "FetchEventOpParent.h" + +#include "mozilla/dom/FetchTypes.h" +#include "nsDebug.h" + +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/FetchEventOpProxyParent.h" +#include "mozilla/dom/FetchStreamUtils.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/RemoteWorkerControllerParent.h" +#include "mozilla/dom/RemoteWorkerParent.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +Tuple<Maybe<ParentToParentInternalResponse>, Maybe<ResponseEndArgs>> +FetchEventOpParent::OnStart( + MovingNotNull<RefPtr<FetchEventOpProxyParent>> aFetchEventOpProxyParent) { + Maybe<ParentToParentInternalResponse> preloadResponse = + std::move(mState.as<Pending>().mPreloadResponse); + Maybe<ResponseEndArgs> preloadResponseEndArgs = + std::move(mState.as<Pending>().mEndArgs); + mState = AsVariant(Started{std::move(aFetchEventOpProxyParent)}); + return MakeTuple(preloadResponse, preloadResponseEndArgs); +} + +void FetchEventOpParent::OnFinish() { + MOZ_ASSERT(mState.is<Started>()); + mState = AsVariant(Finished()); +} + +mozilla::ipc::IPCResult FetchEventOpParent::RecvPreloadResponse( + ParentToParentInternalResponse&& aResponse) { + AssertIsOnBackgroundThread(); + + mState.match( + [&aResponse](Pending& aPending) { + MOZ_ASSERT(aPending.mPreloadResponse.isNothing()); + aPending.mPreloadResponse = Some(std::move(aResponse)); + }, + [&aResponse](Started& aStarted) { + auto backgroundParent = WrapNotNull( + WrapNotNull(aStarted.mFetchEventOpProxyParent->Manager()) + ->Manager()); + Unused << aStarted.mFetchEventOpProxyParent->SendPreloadResponse( + ToParentToChild(aResponse, backgroundParent)); + }, + [](const Finished&) {}); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchEventOpParent::RecvPreloadResponseEnd( + ResponseEndArgs&& aArgs) { + AssertIsOnBackgroundThread(); + + mState.match( + [&aArgs](Pending& aPending) { + MOZ_ASSERT(aPending.mEndArgs.isNothing()); + aPending.mEndArgs = Some(std::move(aArgs)); + }, + [&aArgs](Started& aStarted) { + Unused << aStarted.mFetchEventOpProxyParent->SendPreloadResponseEnd( + std::move(aArgs)); + }, + [](const Finished&) {}); + + return IPC_OK(); +} + +void FetchEventOpParent::ActorDestroy(ActorDestroyReason) { + AssertIsOnBackgroundThread(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/FetchEventOpParent.h b/dom/serviceworkers/FetchEventOpParent.h new file mode 100644 index 0000000000..ebac93bc10 --- /dev/null +++ b/dom/serviceworkers/FetchEventOpParent.h @@ -0,0 +1,72 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_fetcheventopparent_h__ +#define mozilla_dom_fetcheventopparent_h__ + +#include "nsISupports.h" + +#include "mozilla/Tuple.h" +#include "mozilla/dom/FetchEventOpProxyParent.h" +#include "mozilla/dom/PFetchEventOpParent.h" + +namespace mozilla::dom { + +class FetchEventOpParent final : public PFetchEventOpParent { + friend class PFetchEventOpParent; + + public: + NS_INLINE_DECL_REFCOUNTING(FetchEventOpParent) + + FetchEventOpParent() = default; + + // Transition from the Pending state to the Started state. Returns the preload + // response and response end args, if it has already arrived. + Tuple<Maybe<ParentToParentInternalResponse>, Maybe<ResponseEndArgs>> OnStart( + MovingNotNull<RefPtr<FetchEventOpProxyParent>> aFetchEventOpProxyParent); + + // Transition from the Started state to the Finished state. + void OnFinish(); + + private: + ~FetchEventOpParent() = default; + + // IPDL methods + + mozilla::ipc::IPCResult RecvPreloadResponse( + ParentToParentInternalResponse&& aResponse); + + mozilla::ipc::IPCResult RecvPreloadResponseEnd(ResponseEndArgs&& aArgs); + + void ActorDestroy(ActorDestroyReason) override; + + struct Pending { + Maybe<ParentToParentInternalResponse> mPreloadResponse; + Maybe<ResponseEndArgs> mEndArgs; + }; + + struct Started { + NotNull<RefPtr<FetchEventOpProxyParent>> mFetchEventOpProxyParent; + }; + + struct Finished {}; + + using State = Variant<Pending, Started, Finished>; + + // Tracks the state of the fetch event. + // + // Pending: the fetch event is waiting in RemoteWorkerController::mPendingOps + // and if the preload response arrives, we have to save it. + // Started: the FetchEventOpProxyParent has been created, and if the preload + // response arrives then we should forward it. + // Finished: the response has been propagated to the parent process, if the + // preload response arrives now then we simply drop it. + State mState = AsVariant(Pending()); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_fetcheventopparent_h__ diff --git a/dom/serviceworkers/FetchEventOpProxyChild.cpp b/dom/serviceworkers/FetchEventOpProxyChild.cpp new file mode 100644 index 0000000000..94bdbaa4bb --- /dev/null +++ b/dom/serviceworkers/FetchEventOpProxyChild.cpp @@ -0,0 +1,256 @@ +/* -*- 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 "FetchEventOpProxyChild.h" + +#include <utility> + +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/dom/ServiceWorkerOpPromise.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsThreadUtils.h" + +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/RemoteWorkerService.h" +#include "mozilla/dom/ServiceWorkerOp.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/IPCStreamUtils.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +nsresult GetIPCSynthesizeResponseArgs( + ChildToParentSynthesizeResponseArgs* aIPCArgs, + SynthesizeResponseArgs&& aArgs) { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + + SafeRefPtr<InternalResponse> internalResponse; + FetchEventRespondWithClosure closure; + FetchEventTimeStamps timeStamps; + Tie(internalResponse, closure, timeStamps) = std::move(aArgs); + + aIPCArgs->closure() = std::move(closure); + aIPCArgs->timeStamps() = std::move(timeStamps); + + PBackgroundChild* bgChild = BackgroundChild::GetOrCreateForCurrentThread(); + + if (NS_WARN_IF(!bgChild)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + internalResponse->ToChildToParentInternalResponse( + &aIPCArgs->internalResponse(), bgChild); + return NS_OK; +} + +} // anonymous namespace + +void FetchEventOpProxyChild::Initialize( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + MOZ_ASSERT(!mOp); + + mInternalRequest = + MakeSafeRefPtr<InternalRequest>(aArgs.common().internalRequest()); + + if (aArgs.common().preloadNavigation()) { + // We use synchronous task dispatch here to make sure that if the preload + // response arrived before we dispatch the fetch event, then the JS preload + // response promise will get resolved immediately. + mPreloadResponseAvailablePromise = + MakeRefPtr<FetchEventPreloadResponseAvailablePromise::Private>( + __func__); + mPreloadResponseAvailablePromise->UseSynchronousTaskDispatch(__func__); + + if (aArgs.preloadResponse().isSome()) { + mPreloadResponseAvailablePromise->Resolve( + InternalResponse::FromIPC(aArgs.preloadResponse().ref()), __func__); + } + mPreloadResponseEndPromise = + MakeRefPtr<FetchEventPreloadResponseEndPromise::Private>(__func__); + mPreloadResponseEndPromise->UseSynchronousTaskDispatch(__func__); + if (aArgs.preloadResponseEndArgs().isSome()) { + mPreloadResponseEndPromise->Resolve(aArgs.preloadResponseEndArgs().ref(), + __func__); + } + } + + RemoteWorkerChild* manager = static_cast<RemoteWorkerChild*>(Manager()); + MOZ_ASSERT(manager); + + RefPtr<FetchEventOpProxyChild> self = this; + + auto callback = [self](const ServiceWorkerOpResult& aResult) { + // FetchEventOp could finish before NavigationPreload fetch finishes. + // If NavigationPreload is available in FetchEvent, caching FetchEventOp + // result until RecvPreloadResponseEnd is called, such that the response's + // ResourceTiming could be recorded in worker's performanceStorage. + if (self->mPreloadResponseEndPromise && + !self->mPreloadResponseEndPromise->IsResolved() && + self->mPreloadResponseAvailablePromise->IsResolved()) { + self->mCachedOpResult = Some(aResult); + return; + } + if (!self->CanSend()) { + return; + } + + if (NS_WARN_IF(aResult.type() == ServiceWorkerOpResult::Tnsresult)) { + Unused << self->Send__delete__(self, aResult.get_nsresult()); + return; + } + + MOZ_ASSERT(aResult.type() == + ServiceWorkerOpResult::TServiceWorkerFetchEventOpResult); + + Unused << self->Send__delete__(self, aResult); + }; + + RefPtr<FetchEventOp> op = ServiceWorkerOp::Create(aArgs, std::move(callback)) + .template downcast<FetchEventOp>(); + + MOZ_ASSERT(op); + + op->SetActor(this); + mOp = op; + + op->GetRespondWithPromise() + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = std::move(self)]( + FetchEventRespondWithPromise::ResolveOrRejectValue&& aResult) { + self->mRespondWithPromiseRequestHolder.Complete(); + + if (NS_WARN_IF(aResult.IsReject())) { + MOZ_ASSERT(NS_FAILED(aResult.RejectValue().status())); + + Unused << self->SendRespondWith(aResult.RejectValue()); + return; + } + + auto& result = aResult.ResolveValue(); + + if (result.is<SynthesizeResponseArgs>()) { + ChildToParentSynthesizeResponseArgs ipcArgs; + nsresult rv = GetIPCSynthesizeResponseArgs( + &ipcArgs, result.extract<SynthesizeResponseArgs>()); + + if (NS_WARN_IF(NS_FAILED(rv))) { + Unused << self->SendRespondWith( + CancelInterceptionArgs(rv, ipcArgs.timeStamps())); + return; + } + + Unused << self->SendRespondWith(ipcArgs); + } else if (result.is<ResetInterceptionArgs>()) { + Unused << self->SendRespondWith( + result.extract<ResetInterceptionArgs>()); + } else { + Unused << self->SendRespondWith( + result.extract<CancelInterceptionArgs>()); + } + }) + ->Track(mRespondWithPromiseRequestHolder); + + manager->MaybeStartOp(std::move(op)); +} + +SafeRefPtr<InternalRequest> FetchEventOpProxyChild::ExtractInternalRequest() { + MOZ_ASSERT(IsCurrentThreadRunningWorker()); + MOZ_ASSERT(mInternalRequest); + + return std::move(mInternalRequest); +} + +RefPtr<FetchEventPreloadResponseAvailablePromise> +FetchEventOpProxyChild::GetPreloadResponseAvailablePromise() { + return mPreloadResponseAvailablePromise; +} + +RefPtr<FetchEventPreloadResponseEndPromise> +FetchEventOpProxyChild::GetPreloadResponseEndPromise() { + return mPreloadResponseEndPromise; +} + +mozilla::ipc::IPCResult FetchEventOpProxyChild::RecvPreloadResponse( + ParentToChildInternalResponse&& aResponse) { + // Receiving this message implies that navigation preload is enabled, so + // Initialize() should have created this promise. + MOZ_ASSERT(mPreloadResponseAvailablePromise); + + mPreloadResponseAvailablePromise->Resolve( + InternalResponse::FromIPC(aResponse), __func__); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchEventOpProxyChild::RecvPreloadResponseEnd( + ResponseEndArgs&& aArgs) { + // Receiving this message implies that navigation preload is enabled, so + // Initialize() should have created this promise. + MOZ_ASSERT(mPreloadResponseEndPromise); + + mPreloadResponseEndPromise->Resolve(std::move(aArgs), __func__); + // If mCachedOpResult is not nothing, it means FetchEventOp had already done + // and the operation result is cached. Continue closing IPC here. + if (mCachedOpResult.isNothing()) { + return IPC_OK(); + } + + if (!CanSend()) { + return IPC_OK(); + } + + if (NS_WARN_IF(mCachedOpResult.ref().type() == + ServiceWorkerOpResult::Tnsresult)) { + Unused << Send__delete__(this, mCachedOpResult.ref().get_nsresult()); + return IPC_OK(); + } + + MOZ_ASSERT(mCachedOpResult.ref().type() == + ServiceWorkerOpResult::TServiceWorkerFetchEventOpResult); + + Unused << Send__delete__(this, mCachedOpResult.ref()); + + return IPC_OK(); +} + +void FetchEventOpProxyChild::ActorDestroy(ActorDestroyReason) { + Unused << NS_WARN_IF(mRespondWithPromiseRequestHolder.Exists()); + mRespondWithPromiseRequestHolder.DisconnectIfExists(); + + // If mPreloadResponseAvailablePromise exists, navigation preloading response + // will not be valid anymore since it is too late to respond to the + // FetchEvent. Resolve the preload response promise with + // NS_ERROR_DOM_ABORT_ERR. + if (mPreloadResponseAvailablePromise) { + mPreloadResponseAvailablePromise->Resolve( + InternalResponse::NetworkError(NS_ERROR_DOM_ABORT_ERR), __func__); + } + + if (mPreloadResponseEndPromise) { + ResponseEndArgs args(FetchDriverObserver::eAborted, Nothing()); + mPreloadResponseEndPromise->Resolve(args, __func__); + } + + mOp->RevokeActor(this); + mOp = nullptr; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/FetchEventOpProxyChild.h b/dom/serviceworkers/FetchEventOpProxyChild.h new file mode 100644 index 0000000000..ed888069d8 --- /dev/null +++ b/dom/serviceworkers/FetchEventOpProxyChild.h @@ -0,0 +1,71 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_fetcheventopproxychild_h__ +#define mozilla_dom_fetcheventopproxychild_h__ + +#include "nsISupportsImpl.h" + +#include "ServiceWorkerOp.h" +#include "ServiceWorkerOpPromise.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/PFetchEventOpProxyChild.h" + +namespace mozilla::dom { + +class InternalRequest; +class InternalResponse; +class ParentToChildServiceWorkerFetchEventOpArgs; + +class FetchEventOpProxyChild final : public PFetchEventOpProxyChild { + friend class PFetchEventOpProxyChild; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FetchEventOpProxyChild, override); + + FetchEventOpProxyChild() = default; + + void Initialize(const ParentToChildServiceWorkerFetchEventOpArgs& aArgs); + + // Must only be called once and on a worker thread. + SafeRefPtr<InternalRequest> ExtractInternalRequest(); + + RefPtr<FetchEventPreloadResponseAvailablePromise> + GetPreloadResponseAvailablePromise(); + + RefPtr<FetchEventPreloadResponseEndPromise> GetPreloadResponseEndPromise(); + + private: + ~FetchEventOpProxyChild() = default; + + mozilla::ipc::IPCResult RecvPreloadResponse( + ParentToChildInternalResponse&& aResponse); + + mozilla::ipc::IPCResult RecvPreloadResponseEnd(ResponseEndArgs&& aArgs); + + void ActorDestroy(ActorDestroyReason) override; + + MozPromiseRequestHolder<FetchEventRespondWithPromise> + mRespondWithPromiseRequestHolder; + + RefPtr<FetchEventOp> mOp; + + // Initialized on RemoteWorkerService::Thread, read on a worker thread. + SafeRefPtr<InternalRequest> mInternalRequest; + + RefPtr<FetchEventPreloadResponseAvailablePromise::Private> + mPreloadResponseAvailablePromise; + RefPtr<FetchEventPreloadResponseEndPromise::Private> + mPreloadResponseEndPromise; + + Maybe<ServiceWorkerOpResult> mCachedOpResult; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_fetcheventopproxychild_h__ diff --git a/dom/serviceworkers/FetchEventOpProxyParent.cpp b/dom/serviceworkers/FetchEventOpProxyParent.cpp new file mode 100644 index 0000000000..8146131739 --- /dev/null +++ b/dom/serviceworkers/FetchEventOpProxyParent.cpp @@ -0,0 +1,227 @@ +/* -*- 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 "FetchEventOpProxyParent.h" + +#include <utility> + +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" +#include "nsCOMPtr.h" +#include "nsIInputStream.h" + +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/PRemoteWorkerParent.h" +#include "mozilla/dom/PRemoteWorkerControllerParent.h" +#include "mozilla/dom/FetchEventOpParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/RemoteLazyInputStreamStorage.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +nsresult MaybeDeserializeAndWrapForMainThread( + const Maybe<ChildToParentStream>& aSource, int64_t aBodyStreamSize, + Maybe<ParentToParentStream>& aSink, PBackgroundParent* aManager) { + if (aSource.isNothing()) { + return NS_OK; + } + + nsCOMPtr<nsIInputStream> deserialized = + DeserializeIPCStream(aSource->stream()); + + aSink = Some(ParentToParentStream()); + auto& uuid = aSink->uuid(); + + MOZ_TRY(nsID::GenerateUUIDInPlace(uuid)); + + auto storageOrErr = RemoteLazyInputStreamStorage::Get(); + + if (NS_WARN_IF(storageOrErr.isErr())) { + return storageOrErr.unwrapErr(); + } + + auto storage = storageOrErr.unwrap(); + storage->AddStream(deserialized, uuid); + return NS_OK; +} + +ParentToParentInternalResponse ToParentToParent( + const ChildToParentInternalResponse& aResponse, + NotNull<PBackgroundParent*> aBackgroundParent) { + ParentToParentInternalResponse parentToParentResponse( + aResponse.metadata(), Nothing(), aResponse.bodySize(), Nothing()); + + MOZ_ALWAYS_SUCCEEDS(MaybeDeserializeAndWrapForMainThread( + aResponse.body(), aResponse.bodySize(), parentToParentResponse.body(), + aBackgroundParent)); + MOZ_ALWAYS_SUCCEEDS(MaybeDeserializeAndWrapForMainThread( + aResponse.alternativeBody(), InternalResponse::UNKNOWN_BODY_SIZE, + parentToParentResponse.alternativeBody(), aBackgroundParent)); + + return parentToParentResponse; +} + +ParentToParentSynthesizeResponseArgs ToParentToParent( + const ChildToParentSynthesizeResponseArgs& aArgs, + NotNull<PBackgroundParent*> aBackgroundParent) { + return ParentToParentSynthesizeResponseArgs( + ToParentToParent(aArgs.internalResponse(), aBackgroundParent), + aArgs.closure(), aArgs.timeStamps()); +} + +ParentToParentFetchEventRespondWithResult ToParentToParent( + const ChildToParentFetchEventRespondWithResult& aResult, + NotNull<PBackgroundParent*> aBackgroundParent) { + switch (aResult.type()) { + case ChildToParentFetchEventRespondWithResult:: + TChildToParentSynthesizeResponseArgs: + return ToParentToParent(aResult.get_ChildToParentSynthesizeResponseArgs(), + aBackgroundParent); + + case ChildToParentFetchEventRespondWithResult::TResetInterceptionArgs: + return aResult.get_ResetInterceptionArgs(); + + case ChildToParentFetchEventRespondWithResult::TCancelInterceptionArgs: + return aResult.get_CancelInterceptionArgs(); + + default: + MOZ_CRASH("Invalid ParentToParentFetchEventRespondWithResult"); + } +} + +} // anonymous namespace + +/* static */ void FetchEventOpProxyParent::Create( + PRemoteWorkerParent* aManager, + RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise, + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<FetchEventOpParent> aReal, nsCOMPtr<nsIInputStream> aBodyStream) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aManager); + MOZ_ASSERT(aReal); + + ParentToChildServiceWorkerFetchEventOpArgs copyArgs(aArgs.common(), Nothing(), + Nothing()); + if (aArgs.preloadResponse().isSome()) { + // Convert the preload response to ParentToChildInternalResponse. + copyArgs.preloadResponse() = Some(ToParentToChild( + aArgs.preloadResponse().ref(), WrapNotNull(aManager->Manager()))); + } + + if (aArgs.preloadResponseEndArgs().isSome()) { + copyArgs.preloadResponseEndArgs() = aArgs.preloadResponseEndArgs(); + } + + FetchEventOpProxyParent* actor = + new FetchEventOpProxyParent(std::move(aReal), std::move(aPromise)); + + // As long as the fetch event was pending, the FetchEventOpParent was + // responsible for keeping the preload response, if it already arrived. Once + // the fetch event starts it gives up the preload response (if any) and we + // need to add it to the arguments. Note that we have to make sure that the + // arguments don't contain the preload response already, otherwise we'll end + // up overwriting it with a Nothing. + Maybe<ParentToParentInternalResponse> preloadResponse; + Maybe<ResponseEndArgs> preloadResponseEndArgs; + Tie(preloadResponse, preloadResponseEndArgs) = + actor->mReal->OnStart(WrapNotNull(actor)); + if (copyArgs.preloadResponse().isNothing() && preloadResponse.isSome()) { + copyArgs.preloadResponse() = Some(ToParentToChild( + preloadResponse.ref(), WrapNotNull(aManager->Manager()))); + } + if (copyArgs.preloadResponseEndArgs().isNothing() && + preloadResponseEndArgs.isSome()) { + copyArgs.preloadResponseEndArgs() = preloadResponseEndArgs; + } + + IPCInternalRequest& copyRequest = copyArgs.common().internalRequest(); + + if (aBodyStream) { + copyRequest.body() = Some(ParentToChildStream()); + + RefPtr<RemoteLazyInputStream> stream = + RemoteLazyInputStream::WrapStream(aBodyStream); + MOZ_DIAGNOSTIC_ASSERT(stream); + + copyRequest.body().ref().get_ParentToChildStream().stream() = stream; + } + + Unused << aManager->SendPFetchEventOpProxyConstructor(actor, copyArgs); +} + +FetchEventOpProxyParent::~FetchEventOpProxyParent() { + AssertIsOnBackgroundThread(); +} + +FetchEventOpProxyParent::FetchEventOpProxyParent( + RefPtr<FetchEventOpParent>&& aReal, + RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise) + : mReal(std::move(aReal)), mLifetimePromise(std::move(aPromise)) {} + +mozilla::ipc::IPCResult FetchEventOpProxyParent::RecvAsyncLog( + const nsCString& aScriptSpec, const uint32_t& aLineNumber, + const uint32_t& aColumnNumber, const nsCString& aMessageName, + nsTArray<nsString>&& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mReal); + + Unused << mReal->SendAsyncLog(aScriptSpec, aLineNumber, aColumnNumber, + aMessageName, aParams); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchEventOpProxyParent::RecvRespondWith( + const ChildToParentFetchEventRespondWithResult& aResult) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mReal); + + auto manager = WrapNotNull(mReal->Manager()); + auto backgroundParent = WrapNotNull(manager->Manager()); + Unused << mReal->SendRespondWith(ToParentToParent(aResult, backgroundParent)); + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchEventOpProxyParent::Recv__delete__( + const ServiceWorkerFetchEventOpResult& aResult) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mLifetimePromise); + MOZ_ASSERT(mReal); + mReal->OnFinish(); + if (mLifetimePromise) { + mLifetimePromise->Resolve(aResult, __func__); + mLifetimePromise = nullptr; + mReal = nullptr; + } + + return IPC_OK(); +} + +void FetchEventOpProxyParent::ActorDestroy(ActorDestroyReason) { + AssertIsOnBackgroundThread(); + if (mLifetimePromise) { + mLifetimePromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mLifetimePromise = nullptr; + mReal = nullptr; + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/FetchEventOpProxyParent.h b/dom/serviceworkers/FetchEventOpProxyParent.h new file mode 100644 index 0000000000..e657ebd21a --- /dev/null +++ b/dom/serviceworkers/FetchEventOpProxyParent.h @@ -0,0 +1,68 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_fetcheventopproxyparent_h__ +#define mozilla_dom_fetcheventopproxyparent_h__ + +#include "mozilla/RefPtr.h" +#include "mozilla/dom/PFetchEventOpProxyParent.h" +#include "mozilla/dom/ServiceWorkerOpPromise.h" + +namespace mozilla::dom { + +class FetchEventOpParent; +class PRemoteWorkerParent; +class ParentToParentServiceWorkerFetchEventOpArgs; + +/** + * FetchEventOpProxyParent owns a FetchEventOpParent in order to propagate + * the respondWith() value by directly calling SendRespondWith on the + * FetchEventOpParent, but the call to Send__delete__ is handled via MozPromise. + * This is done because this actor may only be created after its managing + * PRemoteWorker is created, which is asynchronous and may fail. We take on + * responsibility for the promise once we are created, but we may not be created + * if the RemoteWorker is never successfully launched. + */ +class FetchEventOpProxyParent final : public PFetchEventOpProxyParent { + friend class PFetchEventOpProxyParent; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FetchEventOpProxyParent, override); + + static void Create( + PRemoteWorkerParent* aManager, + RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise, + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<FetchEventOpParent> aReal, nsCOMPtr<nsIInputStream> aBodyStream); + + private: + FetchEventOpProxyParent( + RefPtr<FetchEventOpParent>&& aReal, + RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise); + + ~FetchEventOpProxyParent(); + + mozilla::ipc::IPCResult RecvAsyncLog(const nsCString& aScriptSpec, + const uint32_t& aLineNumber, + const uint32_t& aColumnNumber, + const nsCString& aMessageName, + nsTArray<nsString>&& aParams); + + mozilla::ipc::IPCResult RecvRespondWith( + const ChildToParentFetchEventRespondWithResult& aResult); + + mozilla::ipc::IPCResult Recv__delete__( + const ServiceWorkerFetchEventOpResult& aResult); + + void ActorDestroy(ActorDestroyReason) override; + + RefPtr<FetchEventOpParent> mReal; + RefPtr<ServiceWorkerFetchEventOpPromise::Private> mLifetimePromise; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_fetcheventopproxyparent_h__ diff --git a/dom/serviceworkers/IPCNavigationPreloadState.ipdlh b/dom/serviceworkers/IPCNavigationPreloadState.ipdlh new file mode 100644 index 0000000000..2ee4f02789 --- /dev/null +++ b/dom/serviceworkers/IPCNavigationPreloadState.ipdlh @@ -0,0 +1,16 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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/. */ + +namespace mozilla { +namespace dom { + +struct IPCNavigationPreloadState { + bool enabled; + nsCString headerValue; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/IPCServiceWorkerDescriptor.ipdlh b/dom/serviceworkers/IPCServiceWorkerDescriptor.ipdlh new file mode 100644 index 0000000000..d02b6fc0f6 --- /dev/null +++ b/dom/serviceworkers/IPCServiceWorkerDescriptor.ipdlh @@ -0,0 +1,30 @@ +/* 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 PBackgroundSharedTypes; + +include "mozilla/dom/ServiceWorkerIPCUtils.h"; + +using ServiceWorkerState from "mozilla/dom/ServiceWorkerBinding.h"; + +namespace mozilla { +namespace dom { + +// IPC type with enough information to create a ServiceWorker DOM object +// in a child process. Note that the state may be slightly out-of-sync +// with the parent and should be updated dynamically if necessary. +[Comparable] struct IPCServiceWorkerDescriptor +{ + uint64_t id; + uint64_t registrationId; + uint64_t registrationVersion; + PrincipalInfo principalInfo; + nsCString scope; + nsCString scriptURL; + ServiceWorkerState state; + bool handlesFetch; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/IPCServiceWorkerRegistrationDescriptor.ipdlh b/dom/serviceworkers/IPCServiceWorkerRegistrationDescriptor.ipdlh new file mode 100644 index 0000000000..db84ae33b1 --- /dev/null +++ b/dom/serviceworkers/IPCServiceWorkerRegistrationDescriptor.ipdlh @@ -0,0 +1,58 @@ +/* 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 PBackgroundSharedTypes; +include IPCServiceWorkerDescriptor; + +include "ipc/ErrorIPCUtils.h"; +include "mozilla/dom/ServiceWorkerIPCUtils.h"; + +using ServiceWorkerUpdateViaCache from "mozilla/dom/ServiceWorkerRegistrationBinding.h"; +using mozilla::CopyableErrorResult from "mozilla/ErrorResult.h"; + +namespace mozilla { +namespace dom { + +// IPC type with enough information to create a ServiceWorker DOM object +// in a child process. Note that the state may be slightly out-of-sync +// with the parent and should be updated dynamically if necessary. +[Comparable] struct IPCServiceWorkerRegistrationDescriptor +{ + uint64_t id; + uint64_t version; + + // These values should match the principal and scope in each + // associated worker. It may be possible to optimize in the future, + // but for now we duplicate the information here to ensure correctness. + // Its possible we may need to reference a registration before the + // worker is installed yet, etc. + PrincipalInfo principalInfo; + nsCString scope; + + ServiceWorkerUpdateViaCache updateViaCache; + + IPCServiceWorkerDescriptor? installing; + IPCServiceWorkerDescriptor? waiting; + IPCServiceWorkerDescriptor? active; +}; + +union IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult +{ + IPCServiceWorkerRegistrationDescriptor; + CopyableErrorResult; +}; + +struct IPCServiceWorkerRegistrationDescriptorList +{ + IPCServiceWorkerRegistrationDescriptor[] values; +}; + +union IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult +{ + IPCServiceWorkerRegistrationDescriptorList; + CopyableErrorResult; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/NavigationPreloadManager.cpp b/dom/serviceworkers/NavigationPreloadManager.cpp new file mode 100644 index 0000000000..93c17353ab --- /dev/null +++ b/dom/serviceworkers/NavigationPreloadManager.cpp @@ -0,0 +1,139 @@ +/* -*- 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 "NavigationPreloadManager.h" +#include "ServiceWorkerUtils.h" +#include "nsNetUtil.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/NavigationPreloadManagerBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ServiceWorker.h" +#include "mozilla/ipc/MessageChannel.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(NavigationPreloadManager) +NS_IMPL_CYCLE_COLLECTING_RELEASE(NavigationPreloadManager) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NavigationPreloadManager) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(NavigationPreloadManager, + mServiceWorkerRegistration) + +/* static */ +bool NavigationPreloadManager::IsValidHeader(const nsACString& aHeader) { + return NS_IsReasonableHTTPHeaderValue(aHeader); +} + +bool NavigationPreloadManager::IsEnabled(JSContext* aCx, JSObject* aGlobal) { + return StaticPrefs::dom_serviceWorkers_navigationPreload_enabled() && + ServiceWorkerVisible(aCx, aGlobal); +} + +NavigationPreloadManager::NavigationPreloadManager( + RefPtr<ServiceWorkerRegistration>& aServiceWorkerRegistration) + : mServiceWorkerRegistration(aServiceWorkerRegistration) {} + +JSObject* NavigationPreloadManager::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return NavigationPreloadManager_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<Promise> NavigationPreloadManager::SetEnabled( + bool aEnabled, ErrorResult& aError) { + RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError); + + if (NS_WARN_IF(aError.Failed())) { + return nullptr; + } + + if (!mServiceWorkerRegistration) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + + mServiceWorkerRegistration->SetNavigationPreloadEnabled( + aEnabled, + [promise](bool aSuccess) { + if (aSuccess) { + promise->MaybeResolveWithUndefined(); + return; + } + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + }, + [promise](ErrorResult&& aRv) { promise->MaybeReject(std::move(aRv)); }); + + return promise.forget(); +} + +already_AddRefed<Promise> NavigationPreloadManager::Enable( + ErrorResult& aError) { + return SetEnabled(true, aError); +} + +already_AddRefed<Promise> NavigationPreloadManager::Disable( + ErrorResult& aError) { + return SetEnabled(false, aError); +} + +already_AddRefed<Promise> NavigationPreloadManager::SetHeaderValue( + const nsACString& aHeader, ErrorResult& aError) { + RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError); + + if (NS_WARN_IF(aError.Failed())) { + return nullptr; + } + + if (!IsValidHeader(aHeader)) { + promise->MaybeRejectWithTypeError<MSG_INVALID_HEADER_VALUE>(aHeader); + return promise.forget(); + } + + if (!mServiceWorkerRegistration) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + + mServiceWorkerRegistration->SetNavigationPreloadHeader( + nsAutoCString(aHeader), + [promise](bool aSuccess) { + if (aSuccess) { + promise->MaybeResolveWithUndefined(); + return; + } + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + }, + [promise](ErrorResult&& aRv) { promise->MaybeReject(std::move(aRv)); }); + + return promise.forget(); +} + +already_AddRefed<Promise> NavigationPreloadManager::GetState( + ErrorResult& aError) { + RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError); + + if (NS_WARN_IF(aError.Failed())) { + return nullptr; + } + + if (!mServiceWorkerRegistration) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return promise.forget(); + } + + mServiceWorkerRegistration->GetNavigationPreloadState( + [promise](NavigationPreloadState&& aState) { + promise->MaybeResolve(std::move(aState)); + }, + [promise](ErrorResult&& aRv) { promise->MaybeReject(std::move(aRv)); }); + + return promise.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/NavigationPreloadManager.h b/dom/serviceworkers/NavigationPreloadManager.h new file mode 100644 index 0000000000..7b4d0ac50b --- /dev/null +++ b/dom/serviceworkers/NavigationPreloadManager.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_NavigationPreloadManager_h +#define mozilla_dom_NavigationPreloadManager_h + +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/ServiceWorkerRegistration.h" +#include "mozilla/RefPtr.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +class Promise; + +class NavigationPreloadManager final : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(NavigationPreloadManager) + + static bool IsValidHeader(const nsACString& aHeader); + + static bool IsEnabled(JSContext* aCx, JSObject* aGlobal); + + explicit NavigationPreloadManager( + RefPtr<ServiceWorkerRegistration>& aServiceWorkerRegistration); + + // Webidl binding + nsIGlobalObject* GetParentObject() const { + return mServiceWorkerRegistration->GetParentObject(); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIdl implementation + already_AddRefed<Promise> Enable(ErrorResult& aError); + + already_AddRefed<Promise> Disable(ErrorResult& aError); + + already_AddRefed<Promise> SetHeaderValue(const nsACString& aHeader, + ErrorResult& aError); + + already_AddRefed<Promise> GetState(ErrorResult& aError); + + private: + ~NavigationPreloadManager() = default; + + // General method for Enable()/Disable() + already_AddRefed<Promise> SetEnabled(bool aEnabled, ErrorResult& aError); + + RefPtr<ServiceWorkerRegistration> mServiceWorkerRegistration; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_NavigationPreloadManager_h diff --git a/dom/serviceworkers/PFetchEventOp.ipdl b/dom/serviceworkers/PFetchEventOp.ipdl new file mode 100644 index 0000000000..b78bf3e2af --- /dev/null +++ b/dom/serviceworkers/PFetchEventOp.ipdl @@ -0,0 +1,33 @@ +/* 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 protocol PRemoteWorkerController; + +include ServiceWorkerOpArgs; +include FetchTypes; + +namespace mozilla { +namespace dom { + +[ManualDealloc] +protocol PFetchEventOp { + manager PRemoteWorkerController; + + parent: + async PreloadResponse(ParentToParentInternalResponse aResponse); + + async PreloadResponseEnd(ResponseEndArgs aArgs); + + child: + async AsyncLog(nsCString aScriptSpec, uint32_t aLineNumber, + uint32_t aColumnNumber, nsCString aMessageName, + nsString[] aParams); + + async RespondWith(ParentToParentFetchEventRespondWithResult aResult); + + async __delete__(ServiceWorkerFetchEventOpResult aResult); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/PFetchEventOpProxy.ipdl b/dom/serviceworkers/PFetchEventOpProxy.ipdl new file mode 100644 index 0000000000..3f3ddbb44a --- /dev/null +++ b/dom/serviceworkers/PFetchEventOpProxy.ipdl @@ -0,0 +1,32 @@ +/* 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 protocol PRemoteWorker; + +include ServiceWorkerOpArgs; +include FetchTypes; + +namespace mozilla { +namespace dom { + +protocol PFetchEventOpProxy { + manager PRemoteWorker; + + parent: + async AsyncLog(nsCString aScriptSpec, uint32_t aLineNumber, + uint32_t aColumnNumber, nsCString aMessageName, + nsString[] aParams); + + async RespondWith(ChildToParentFetchEventRespondWithResult aResult); + + async __delete__(ServiceWorkerFetchEventOpResult aResult); + + child: + async PreloadResponse(ParentToChildInternalResponse aResponse); + + async PreloadResponseEnd(ResponseEndArgs aArgs); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/PServiceWorker.ipdl b/dom/serviceworkers/PServiceWorker.ipdl new file mode 100644 index 0000000000..1f8c54481d --- /dev/null +++ b/dom/serviceworkers/PServiceWorker.ipdl @@ -0,0 +1,28 @@ +/* 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 protocol PBackground; + +include ClientIPCTypes; +include DOMTypes; + +namespace mozilla { +namespace dom { + +[ChildImpl=virtual, ParentImpl=virtual] +protocol PServiceWorker +{ + manager PBackground; + +parent: + async Teardown(); + + async PostMessage(ClonedOrErrorMessageData aClonedData, ClientInfoAndState aSource); + +child: + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/PServiceWorkerContainer.ipdl b/dom/serviceworkers/PServiceWorkerContainer.ipdl new file mode 100644 index 0000000000..b24b494631 --- /dev/null +++ b/dom/serviceworkers/PServiceWorkerContainer.ipdl @@ -0,0 +1,41 @@ +/* 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 protocol PBackground; + +include ClientIPCTypes; +include IPCServiceWorkerRegistrationDescriptor; + +include "mozilla/dom/ServiceWorkerIPCUtils.h"; + +namespace mozilla { +namespace dom { + +[ChildImpl=virtual, ParentImpl=virtual] +protocol PServiceWorkerContainer +{ + manager PBackground; + +parent: + async Teardown(); + + async Register(IPCClientInfo aClientInfo, nsCString aScopeURL, nsCString aScriptURL, + ServiceWorkerUpdateViaCache aUpdateViaCache) + returns (IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult); + + async GetRegistration(IPCClientInfo aClientInfo, nsCString aURL) + returns (IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult); + + async GetRegistrations(IPCClientInfo aClientInfo) + returns (IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult aResult); + + async GetReady(IPCClientInfo aClientInfo) + returns (IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult); + +child: + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/PServiceWorkerManager.ipdl b/dom/serviceworkers/PServiceWorkerManager.ipdl new file mode 100644 index 0000000000..a41e64e348 --- /dev/null +++ b/dom/serviceworkers/PServiceWorkerManager.ipdl @@ -0,0 +1,31 @@ +/* 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 protocol PBackground; + +include PBackgroundSharedTypes; +include ServiceWorkerRegistrarTypes; + +using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h"; + +namespace mozilla { +namespace dom { + +[ManualDealloc] +protocol PServiceWorkerManager +{ + manager PBackground; + +parent: + async Register(ServiceWorkerRegistrationData data); + + async Unregister(PrincipalInfo principalInfo, nsString scope); + + async PropagateUnregister(PrincipalInfo principalInfo, nsString scope); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/PServiceWorkerRegistration.ipdl b/dom/serviceworkers/PServiceWorkerRegistration.ipdl new file mode 100644 index 0000000000..45efc2f52d --- /dev/null +++ b/dom/serviceworkers/PServiceWorkerRegistration.ipdl @@ -0,0 +1,40 @@ +/* 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 protocol PBackground; + +include IPCNavigationPreloadState; +include IPCServiceWorkerRegistrationDescriptor; + +include "ipc/ErrorIPCUtils.h"; + +namespace mozilla { +namespace dom { + +[ChildImpl=virtual, ParentImpl=virtual] +protocol PServiceWorkerRegistration +{ + manager PBackground; + +parent: + async Teardown(); + + async Unregister() returns (bool aSuccess, CopyableErrorResult aRv); + async Update(nsCString aNewestWorkerScriptUrl) returns ( + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult); + + // For NavigationPreload interface + async SetNavigationPreloadEnabled(bool aEnabled) returns (bool aSuccess); + async SetNavigationPreloadHeader(nsCString aHeader) returns (bool aSuccess); + async GetNavigationPreloadState() returns (IPCNavigationPreloadState? aState); + +child: + async __delete__(); + + async UpdateState(IPCServiceWorkerRegistrationDescriptor aDescriptor); + async FireUpdateFound(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/ServiceWorker.cpp b/dom/serviceworkers/ServiceWorker.cpp new file mode 100644 index 0000000000..74aded5250 --- /dev/null +++ b/dom/serviceworkers/ServiceWorker.cpp @@ -0,0 +1,379 @@ +/* -*- 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 "ServiceWorker.h" + +#include "mozilla/dom/Document.h" +#include "nsGlobalWindowInner.h" +#include "nsPIDOMWindow.h" +#include "ServiceWorkerChild.h" +#include "ServiceWorkerCloneData.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerRegistration.h" +#include "ServiceWorkerUtils.h" + +#include "mozilla/dom/ClientIPCTypes.h" +#include "mozilla/dom/ClientState.h" +#include "mozilla/dom/MessagePortBinding.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StorageAccess.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +using mozilla::ipc::BackgroundChild; +using mozilla::ipc::PBackgroundChild; + +namespace mozilla::dom { + +static bool IsServiceWorkersTestingEnabledInWindow(JSObject* const aGlobal) { + if (const nsCOMPtr<nsPIDOMWindowInner> innerWindow = + Navigator::GetWindowFromGlobal(aGlobal)) { + if (auto* bc = innerWindow->GetBrowsingContext()) { + return bc->Top()->ServiceWorkersTestingEnabled(); + } + } + return false; +} + +static bool IsInPrivateBrowsing(JSContext* const aCx) { + if (const nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx)) { + if (const nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull()) { + return principal->GetPrivateBrowsingId() > 0; + } + } + return false; +} + +bool ServiceWorkersEnabled(JSContext* aCx, JSObject* aGlobal) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!StaticPrefs::dom_serviceWorkers_enabled()) { + return false; + } + + // xpc::CurrentNativeGlobal below requires rooting + JS::Rooted<JSObject*> global(aCx, aGlobal); + + if (IsInPrivateBrowsing(aCx)) { + return false; + } + + // Allow a webextension principal to register a service worker script with + // a moz-extension url only if 'extensions.service_worker_register.allowed' + // is true. + if (!StaticPrefs::extensions_serviceWorkerRegister_allowed()) { + nsIPrincipal* principal = nsContentUtils::SubjectPrincipal(aCx); + if (principal && BasePrincipal::Cast(principal)->AddonPolicy()) { + return false; + } + } + + if (IsSecureContextOrObjectIsFromSecureContext(aCx, global)) { + return true; + } + + return StaticPrefs::dom_serviceWorkers_testing_enabled() || + IsServiceWorkersTestingEnabledInWindow(global); +} + +bool ServiceWorkerVisible(JSContext* aCx, JSObject* aGlobal) { + if (NS_IsMainThread()) { + // We want to expose ServiceWorker interface only when + // navigator.serviceWorker is available. Currently it may not be available + // with some reasons: + // 1. navigator.serviceWorker is not supported in workers. (bug 1131324) + return ServiceWorkersEnabled(aCx, aGlobal); + } + + // We are already in ServiceWorker and interfaces need to be exposed for e.g. + // globalThis.registration.serviceWorker. Note that navigator.serviceWorker + // is still not supported. (bug 1131324) + return IS_INSTANCE_OF(ServiceWorkerGlobalScope, aGlobal); +} + +// static +already_AddRefed<ServiceWorker> ServiceWorker::Create( + nsIGlobalObject* aOwner, const ServiceWorkerDescriptor& aDescriptor) { + RefPtr<ServiceWorker> ref = new ServiceWorker(aOwner, aDescriptor); + return ref.forget(); +} + +ServiceWorker::ServiceWorker(nsIGlobalObject* aGlobal, + const ServiceWorkerDescriptor& aDescriptor) + : DOMEventTargetHelper(aGlobal), + mDescriptor(aDescriptor), + mShutdown(false), + mLastNotifiedState(ServiceWorkerState::Installing) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aGlobal); + + PBackgroundChild* parentActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!parentActor)) { + Shutdown(); + return; + } + + RefPtr<ServiceWorkerChild> actor = ServiceWorkerChild::Create(); + if (NS_WARN_IF(!actor)) { + Shutdown(); + return; + } + + PServiceWorkerChild* sentActor = + parentActor->SendPServiceWorkerConstructor(actor, aDescriptor.ToIPC()); + if (NS_WARN_IF(!sentActor)) { + Shutdown(); + return; + } + MOZ_DIAGNOSTIC_ASSERT(sentActor == actor); + + mActor = std::move(actor); + mActor->SetOwner(this); + + KeepAliveIfHasListenersFor(nsGkAtoms::onstatechange); + + // The error event handler is required by the spec currently, but is not used + // anywhere. Don't keep the object alive in that case. + + // Attempt to get an existing binding object for the registration + // associated with this ServiceWorker. + RefPtr<ServiceWorkerRegistration> reg = + aGlobal->GetServiceWorkerRegistration(ServiceWorkerRegistrationDescriptor( + mDescriptor.RegistrationId(), mDescriptor.RegistrationVersion(), + mDescriptor.PrincipalInfo(), mDescriptor.Scope(), + ServiceWorkerUpdateViaCache::Imports)); + + if (reg) { + MaybeAttachToRegistration(reg); + // Following codes are commented since GetRegistration has no + // implementation. If we can not get an existing binding object, probably + // need to create one to associate to it. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1769652 + /* + } else { + + RefPtr<ServiceWorker> self = this; + GetRegistration( + [self = std::move(self)]( + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + nsIGlobalObject* global = self->GetParentObject(); + NS_ENSURE_TRUE_VOID(global); + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration(aDescriptor); + self->MaybeAttachToRegistration(reg); + }, + [](ErrorResult&& aRv) { + // do nothing + aRv.SuppressException(); + }); + */ + } +} + +ServiceWorker::~ServiceWorker() { + MOZ_ASSERT(NS_IsMainThread()); + Shutdown(); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorker, DOMEventTargetHelper, + mRegistration); + +NS_IMPL_ADDREF_INHERITED(ServiceWorker, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(ServiceWorker, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorker) + NS_INTERFACE_MAP_ENTRY(ServiceWorker) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +JSObject* ServiceWorker::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + MOZ_ASSERT(NS_IsMainThread()); + + return ServiceWorker_Binding::Wrap(aCx, this, aGivenProto); +} + +ServiceWorkerState ServiceWorker::State() const { return mDescriptor.State(); } + +void ServiceWorker::SetState(ServiceWorkerState aState) { + NS_ENSURE_TRUE_VOID(aState >= mDescriptor.State()); + mDescriptor.SetState(aState); +} + +void ServiceWorker::MaybeDispatchStateChangeEvent() { + if (mDescriptor.State() <= mLastNotifiedState || !GetParentObject()) { + return; + } + mLastNotifiedState = mDescriptor.State(); + + DOMEventTargetHelper::DispatchTrustedEvent(u"statechange"_ns); + + // Once we have transitioned to the redundant state then no + // more statechange events will occur. We can allow the DOM + // object to GC if script is not holding it alive. + if (mLastNotifiedState == ServiceWorkerState::Redundant) { + IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onstatechange); + } +} + +void ServiceWorker::GetScriptURL(nsString& aURL) const { + CopyUTF8toUTF16(mDescriptor.ScriptURL(), aURL); +} + +void ServiceWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, + ErrorResult& aRv) { + // Step 6.1 of + // https://w3c.github.io/ServiceWorker/#service-worker-postmessage-options + // invokes + // https://w3c.github.io/ServiceWorker/#run-service-worker + // which returns failure in step 3 if the ServiceWorker state is redundant. + // This will result in the "in parallel" step 6.1 of postMessage itself early + // returning without starting the ServiceWorker and without throwing an error. + if (State() == ServiceWorkerState::Redundant) { + return; + } + + nsPIDOMWindowInner* window = GetOwner(); + if (NS_WARN_IF(!window || !window->GetExtantDoc())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + auto storageAllowed = StorageAllowedForWindow(window); + if (storageAllowed != StorageAccess::eAllow && + (!StaticPrefs::privacy_partition_serviceWorkers() || + !StoragePartitioningEnabled( + storageAllowed, window->GetExtantDoc()->CookieJarSettings()))) { + ServiceWorkerManager::LocalizeAndReportToAllClients( + mDescriptor.Scope(), "ServiceWorkerPostMessageStorageError", + nsTArray<nsString>{NS_ConvertUTF8toUTF16(mDescriptor.Scope())}); + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + Maybe<ClientInfo> clientInfo = window->GetClientInfo(); + Maybe<ClientState> clientState = window->GetClientState(); + if (NS_WARN_IF(clientInfo.isNothing() || clientState.isNothing())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue()); + aRv = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransferable, + &transferable); + if (aRv.Failed()) { + return; + } + + // Window-to-SW messages do not allow memory sharing since they are not in the + // same agent cluster group, but we do not want to throw an error during the + // serialization. Because of this, ServiceWorkerCloneData will propagate an + // error message data if the SameProcess serialization is required. So that + // the receiver (service worker) knows that it needs to throw while + // deserialization and sharing memory objects are not propagated to the other + // process. + JS::CloneDataPolicy clonePolicy; + if (nsGlobalWindowInner::Cast(window)->IsSharedMemoryAllowed()) { + clonePolicy.allowSharedMemoryObjects(); + } + + RefPtr<ServiceWorkerCloneData> data = new ServiceWorkerCloneData(); + data->Write(aCx, aMessage, transferable, clonePolicy, aRv); + if (aRv.Failed()) { + return; + } + + // The value of CloneScope() is set while StructuredCloneData::Write(). If the + // aValue contiains a shared memory object, then the scope will be restricted + // and thus return SameProcess. If not, it will return DifferentProcess. + // + // When we postMessage a shared memory object from a window to a service + // worker, the object must be sent from a cross-origin isolated process to + // another one. So, we mark mark this data as an error message data if the + // scope is limited to same process. + if (data->CloneScope() == + StructuredCloneHolder::StructuredCloneScope::SameProcess) { + data->SetAsErrorMessageData(); + } + + if (!mActor) { + return; + } + + ClonedOrErrorMessageData clonedData; + if (!data->BuildClonedMessageData(clonedData)) { + return; + } + + mActor->SendPostMessage( + clonedData, + ClientInfoAndState(clientInfo.ref().ToIPC(), clientState.ref().ToIPC())); +} + +void ServiceWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const StructuredSerializeOptions& aOptions, + ErrorResult& aRv) { + PostMessage(aCx, aMessage, aOptions.mTransfer, aRv); +} + +const ServiceWorkerDescriptor& ServiceWorker::Descriptor() const { + return mDescriptor; +} + +void ServiceWorker::DisconnectFromOwner() { + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void ServiceWorker::RevokeActor(ServiceWorkerChild* aActor) { + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor->RevokeOwner(this); + mActor = nullptr; + + mShutdown = true; +} + +void ServiceWorker::MaybeAttachToRegistration( + ServiceWorkerRegistration* aRegistration) { + MOZ_DIAGNOSTIC_ASSERT(aRegistration); + MOZ_DIAGNOSTIC_ASSERT(!mRegistration); + + // If the registration no longer actually references this ServiceWorker + // then we must be in the redundant state. + if (!aRegistration->Descriptor().HasWorker(mDescriptor)) { + SetState(ServiceWorkerState::Redundant); + MaybeDispatchStateChangeEvent(); + return; + } + + mRegistration = aRegistration; +} + +void ServiceWorker::Shutdown() { + if (mShutdown) { + return; + } + mShutdown = true; + + if (mActor) { + mActor->RevokeOwner(this); + mActor->MaybeStartTeardown(); + mActor = nullptr; + } +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorker.h b/dom/serviceworkers/ServiceWorker.h new file mode 100644 index 0000000000..e64a773474 --- /dev/null +++ b/dom/serviceworkers/ServiceWorker.h @@ -0,0 +1,98 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworker_h__ +#define mozilla_dom_serviceworker_h__ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/dom/ServiceWorkerUtils.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +class nsIGlobalObject; + +namespace mozilla::dom { + +class ServiceWorkerChild; +class ServiceWorkerCloneData; +struct StructuredSerializeOptions; + +#define NS_DOM_SERVICEWORKER_IID \ + { \ + 0xd42e0611, 0x3647, 0x4319, { \ + 0xae, 0x05, 0x19, 0x89, 0x59, 0xba, 0x99, 0x5e \ + } \ + } + +bool ServiceWorkersEnabled(JSContext* aCx, JSObject* aGlobal); + +bool ServiceWorkerVisible(JSContext* aCx, JSObject* aGlobal); + +class ServiceWorker final : public DOMEventTargetHelper { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_SERVICEWORKER_IID) + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorker, DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(statechange) + IMPL_EVENT_HANDLER(error) + + static already_AddRefed<ServiceWorker> Create( + nsIGlobalObject* aOwner, const ServiceWorkerDescriptor& aDescriptor); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + ServiceWorkerState State() const; + + void SetState(ServiceWorkerState aState); + + void MaybeDispatchStateChangeEvent(); + + void GetScriptURL(nsString& aURL) const; + + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv); + + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const StructuredSerializeOptions& aOptions, + ErrorResult& aRv); + + const ServiceWorkerDescriptor& Descriptor() const; + + void DisconnectFromOwner() override; + + void RevokeActor(ServiceWorkerChild* aActor); + + private: + ServiceWorker(nsIGlobalObject* aWindow, + const ServiceWorkerDescriptor& aDescriptor); + + // This class is reference-counted and will be destroyed from Release(). + ~ServiceWorker(); + + void MaybeAttachToRegistration(ServiceWorkerRegistration* aRegistration); + + void Shutdown(); + + ServiceWorkerDescriptor mDescriptor; + + RefPtr<ServiceWorkerChild> mActor; + bool mShutdown; + + RefPtr<ServiceWorkerRegistration> mRegistration; + ServiceWorkerState mLastNotifiedState; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(ServiceWorker, NS_DOM_SERVICEWORKER_IID) + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworker_h__ diff --git a/dom/serviceworkers/ServiceWorkerActors.cpp b/dom/serviceworkers/ServiceWorkerActors.cpp new file mode 100644 index 0000000000..145698f3ad --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerActors.cpp @@ -0,0 +1,37 @@ +/* -*- 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 "ServiceWorkerActors.h" + +#include "ServiceWorkerChild.h" +#include "ServiceWorkerContainerChild.h" +#include "ServiceWorkerContainerParent.h" +#include "ServiceWorkerParent.h" +#include "ServiceWorkerRegistrationChild.h" +#include "ServiceWorkerRegistrationParent.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +void InitServiceWorkerParent(PServiceWorkerParent* aActor, + const IPCServiceWorkerDescriptor& aDescriptor) { + auto actor = static_cast<ServiceWorkerParent*>(aActor); + actor->Init(aDescriptor); +} + +void InitServiceWorkerContainerParent(PServiceWorkerContainerParent* aActor) { + auto actor = static_cast<ServiceWorkerContainerParent*>(aActor); + actor->Init(); +} + +void InitServiceWorkerRegistrationParent( + PServiceWorkerRegistrationParent* aActor, + const IPCServiceWorkerRegistrationDescriptor& aDescriptor) { + auto actor = static_cast<ServiceWorkerRegistrationParent*>(aActor); + actor->Init(aDescriptor); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerActors.h b/dom/serviceworkers/ServiceWorkerActors.h new file mode 100644 index 0000000000..cad7cf38e7 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerActors.h @@ -0,0 +1,37 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkeractors_h__ +#define mozilla_dom_serviceworkeractors_h__ + +namespace mozilla::dom { + +// PServiceWorker + +class IPCServiceWorkerDescriptor; +class PServiceWorkerParent; + +void InitServiceWorkerParent(PServiceWorkerParent* aActor, + const IPCServiceWorkerDescriptor& aDescriptor); + +// PServiceWorkerContainer + +class PServiceWorkerContainerParent; + +void InitServiceWorkerContainerParent(PServiceWorkerContainerParent* aActor); + +// PServiceWorkerRegistration + +class IPCServiceWorkerRegistrationDescriptor; +class PServiceWorkerRegistrationParent; + +void InitServiceWorkerRegistrationParent( + PServiceWorkerRegistrationParent* aActor, + const IPCServiceWorkerRegistrationDescriptor& aDescriptor); + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkeractors_h__ diff --git a/dom/serviceworkers/ServiceWorkerChild.cpp b/dom/serviceworkers/ServiceWorkerChild.cpp new file mode 100644 index 0000000000..9c1b045e9f --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerChild.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "ServiceWorkerChild.h" +#include "ServiceWorker.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +void ServiceWorkerChild::ActorDestroy(ActorDestroyReason aReason) { + mIPCWorkerRef = nullptr; + + if (mOwner) { + mOwner->RevokeActor(this); + MOZ_DIAGNOSTIC_ASSERT(!mOwner); + } +} + +// static +RefPtr<ServiceWorkerChild> ServiceWorkerChild::Create() { + RefPtr<ServiceWorkerChild> actor = new ServiceWorkerChild(); + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_DIAGNOSTIC_ASSERT(workerPrivate); + + RefPtr<IPCWorkerRefHelper<ServiceWorkerChild>> helper = + new IPCWorkerRefHelper<ServiceWorkerChild>(actor); + + actor->mIPCWorkerRef = IPCWorkerRef::Create( + workerPrivate, "ServiceWorkerChild", + [helper] { helper->Actor()->MaybeStartTeardown(); }); + + if (NS_WARN_IF(!actor->mIPCWorkerRef)) { + return nullptr; + } + } + + return actor; +} + +ServiceWorkerChild::ServiceWorkerChild() + : mOwner(nullptr), mTeardownStarted(false) {} + +void ServiceWorkerChild::SetOwner(ServiceWorker* aOwner) { + MOZ_DIAGNOSTIC_ASSERT(!mOwner); + MOZ_DIAGNOSTIC_ASSERT(aOwner); + mOwner = aOwner; +} + +void ServiceWorkerChild::RevokeOwner(ServiceWorker* aOwner) { + MOZ_DIAGNOSTIC_ASSERT(mOwner); + MOZ_DIAGNOSTIC_ASSERT(aOwner == mOwner); + mOwner = nullptr; +} + +void ServiceWorkerChild::MaybeStartTeardown() { + if (mTeardownStarted) { + return; + } + mTeardownStarted = true; + Unused << SendTeardown(); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerChild.h b/dom/serviceworkers/ServiceWorkerChild.h new file mode 100644 index 0000000000..2352c1d0a5 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerChild.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerchild_h__ +#define mozilla_dom_serviceworkerchild_h__ + +#include "mozilla/dom/PServiceWorkerChild.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +class IPCWorkerRef; +class ServiceWorker; + +class ServiceWorkerChild final : public PServiceWorkerChild { + RefPtr<IPCWorkerRef> mIPCWorkerRef; + ServiceWorker* mOwner; + bool mTeardownStarted; + + ServiceWorkerChild(); + + ~ServiceWorkerChild() = default; + + // PServiceWorkerChild + void ActorDestroy(ActorDestroyReason aReason) override; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerChild, override); + + static RefPtr<ServiceWorkerChild> Create(); + + void SetOwner(ServiceWorker* aOwner); + + void RevokeOwner(ServiceWorker* aOwner); + + void MaybeStartTeardown(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerchild_h__ diff --git a/dom/serviceworkers/ServiceWorkerCloneData.cpp b/dom/serviceworkers/ServiceWorkerCloneData.cpp new file mode 100644 index 0000000000..b74d45386a --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerCloneData.cpp @@ -0,0 +1,80 @@ +/* -*- 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 "ServiceWorkerCloneData.h" + +#include <utility> +#include "mozilla/RefPtr.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "nsISerialEventTarget.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +ServiceWorkerCloneData::~ServiceWorkerCloneData() { + RefPtr<ipc::SharedJSAllocatedData> sharedData = TakeSharedData(); + if (sharedData) { + NS_ProxyRelease(__func__, mEventTarget, sharedData.forget()); + } +} + +ServiceWorkerCloneData::ServiceWorkerCloneData() + : ipc::StructuredCloneData( + StructuredCloneHolder::StructuredCloneScope::UnknownDestination, + StructuredCloneHolder::TransferringSupported), + mEventTarget(GetCurrentSerialEventTarget()), + mIsErrorMessageData(false) { + MOZ_DIAGNOSTIC_ASSERT(mEventTarget); +} + +bool ServiceWorkerCloneData::BuildClonedMessageData( + ClonedOrErrorMessageData& aClonedData) { + if (IsErrorMessageData()) { + aClonedData = ErrorMessageData(); + return true; + } + + MOZ_DIAGNOSTIC_ASSERT( + CloneScope() == + StructuredCloneHolder::StructuredCloneScope::DifferentProcess); + + ClonedMessageData messageData; + if (!StructuredCloneData::BuildClonedMessageData(messageData)) { + return false; + } + + aClonedData = std::move(messageData); + + return true; +} + +void ServiceWorkerCloneData::CopyFromClonedMessageData( + const ClonedOrErrorMessageData& aClonedData) { + if (aClonedData.type() == ClonedOrErrorMessageData::TErrorMessageData) { + mIsErrorMessageData = true; + return; + } + + MOZ_DIAGNOSTIC_ASSERT(aClonedData.type() == + ClonedOrErrorMessageData::TClonedMessageData); + + StructuredCloneData::CopyFromClonedMessageData(aClonedData); +} + +void ServiceWorkerCloneData::SetAsErrorMessageData() { + MOZ_ASSERT(CloneScope() == + StructuredCloneHolder::StructuredCloneScope::SameProcess); + + mIsErrorMessageData = true; +} + +bool ServiceWorkerCloneData::IsErrorMessageData() const { + return mIsErrorMessageData; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerCloneData.h b/dom/serviceworkers/ServiceWorkerCloneData.h new file mode 100644 index 0000000000..b29b43414b --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerCloneData.h @@ -0,0 +1,71 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerCloneData_h__ +#define mozilla_dom_ServiceWorkerCloneData_h__ + +#include "mozilla/Assertions.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" +#include "nsCOMPtr.h" +#include "nsISupports.h" + +class nsISerialEventTarget; + +namespace mozilla { +namespace ipc { +class PBackgroundChild; +class PBackgroundParent; +} // namespace ipc + +namespace dom { + +class ClonedOrErrorMessageData; + +// Helper class used to pack structured clone data so that it can be +// passed across thread and process boundaries. Currently the raw +// StructuredCloneData and StructureCloneHolder APIs both make it +// difficult to meet this needs directly. This helper class improves +// the situation by: +// +// 1. Provides a ref-counted version of StructuredCloneData. We need +// StructuredCloneData so we can serialize/deserialize across IPC. +// The move constructor problems in StructuredCloneData (bug 1462676), +// though, makes it hard to pass it around. Passing a ref-counted +// pointer addresses this problem. +// 2. Normally StructuredCloneData runs into problems if you try to move +// it across thread boundaries because it releases its SharedJSAllocatedData +// on the wrong thread. This helper will correctly proxy release the +// shared data on the correct thread. +// +// This helper class should really just be used to serialize on one thread +// and then move the reference across thread/process boundries to the +// target worker thread. This class is not intended to support simultaneous +// read/write operations from different threads at the same time. +class ServiceWorkerCloneData final : public ipc::StructuredCloneData { + nsCOMPtr<nsISerialEventTarget> mEventTarget; + bool mIsErrorMessageData; + + ~ServiceWorkerCloneData(); + + public: + ServiceWorkerCloneData(); + + bool BuildClonedMessageData(ClonedOrErrorMessageData& aClonedData); + + void CopyFromClonedMessageData(const ClonedOrErrorMessageData& aClonedData); + + void SetAsErrorMessageData(); + + bool IsErrorMessageData() const; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerCloneData) +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ServiceWorkerCloneData_h__ diff --git a/dom/serviceworkers/ServiceWorkerContainer.cpp b/dom/serviceworkers/ServiceWorkerContainer.cpp new file mode 100644 index 0000000000..4e33b9fc75 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainer.cpp @@ -0,0 +1,893 @@ +/* -*- 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 "ServiceWorkerContainer.h" + +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "mozilla/dom/Document.h" +#include "nsIServiceWorkerManager.h" +#include "nsIScriptError.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "mozilla/Components.h" +#include "mozilla/StaticPrefs_dom.h" + +#include "nsCycleCollectionParticipant.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/dom/ClientIPCTypes.h" +#include "mozilla/dom/DOMMozPromiseRequestHolder.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ServiceWorker.h" +#include "mozilla/dom/ServiceWorkerContainerBinding.h" +#include "mozilla/dom/ServiceWorkerContainerChild.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" + +#include "ServiceWorker.h" +#include "ServiceWorkerRegistration.h" +#include "ServiceWorkerUtils.h" + +// This is defined to something else on Windows +#ifdef DispatchMessage +# undef DispatchMessage +#endif + +namespace mozilla::dom { + +using mozilla::ipc::BackgroundChild; +using mozilla::ipc::PBackgroundChild; +using mozilla::ipc::ResponseRejectReason; + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerContainer) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper, + mControllerWorker, mReadyPromise) + +// static +already_AddRefed<ServiceWorkerContainer> ServiceWorkerContainer::Create( + nsIGlobalObject* aGlobal) { + RefPtr<ServiceWorkerContainer> ref = new ServiceWorkerContainer(aGlobal); + return ref.forget(); +} + +ServiceWorkerContainer::ServiceWorkerContainer(nsIGlobalObject* aGlobal) + : DOMEventTargetHelper(aGlobal), mShutdown(false) { + PBackgroundChild* parentActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!parentActor)) { + Shutdown(); + return; + } + + RefPtr<ServiceWorkerContainerChild> actor = + ServiceWorkerContainerChild::Create(); + if (NS_WARN_IF(!actor)) { + Shutdown(); + return; + } + + PServiceWorkerContainerChild* sentActor = + parentActor->SendPServiceWorkerContainerConstructor(actor); + if (NS_WARN_IF(!sentActor)) { + Shutdown(); + return; + } + MOZ_DIAGNOSTIC_ASSERT(sentActor == actor); + + mActor = std::move(actor); + mActor->SetOwner(this); + + Maybe<ServiceWorkerDescriptor> controller = aGlobal->GetController(); + if (controller.isSome()) { + mControllerWorker = aGlobal->GetOrCreateServiceWorker(controller.ref()); + } +} + +ServiceWorkerContainer::~ServiceWorkerContainer() { Shutdown(); } + +void ServiceWorkerContainer::DisconnectFromOwner() { + mControllerWorker = nullptr; + mReadyPromise = nullptr; + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> go = GetParentObject(); + if (!go) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + mControllerWorker = go->GetOrCreateServiceWorker(go->GetController().ref()); + aRv = DispatchTrustedEvent(u"controllerchange"_ns); +} + +using mozilla::dom::ipc::StructuredCloneData; + +// A ReceivedMessage represents a message sent via +// Client.postMessage(). It is used as used both for queuing of +// incoming messages and as an interface to DispatchMessage(). +struct MOZ_HEAP_CLASS ServiceWorkerContainer::ReceivedMessage { + explicit ReceivedMessage(const ClientPostMessageArgs& aArgs) + : mServiceWorker(aArgs.serviceWorker()) { + mClonedData.CopyFromClonedMessageData(aArgs.clonedData()); + } + + ServiceWorkerDescriptor mServiceWorker; + StructuredCloneData mClonedData; + + NS_INLINE_DECL_REFCOUNTING(ReceivedMessage) + + private: + ~ReceivedMessage() = default; +}; + +void ServiceWorkerContainer::ReceiveMessage( + const ClientPostMessageArgs& aArgs) { + RefPtr<ReceivedMessage> message = new ReceivedMessage(aArgs); + if (mMessagesStarted) { + EnqueueReceivedMessageDispatch(std::move(message)); + } else { + mPendingMessages.AppendElement(message.forget()); + } +} + +void ServiceWorkerContainer::RevokeActor(ServiceWorkerContainerChild* aActor) { + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor->RevokeOwner(this); + mActor = nullptr; + + mShutdown = true; +} + +JSObject* ServiceWorkerContainer::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ServiceWorkerContainer_Binding::Wrap(aCx, this, aGivenProto); +} + +namespace { + +already_AddRefed<nsIURI> GetBaseURIFromGlobal(nsIGlobalObject* aGlobal, + ErrorResult& aRv) { + // It would be nice not to require a window here, but right + // now we don't have a great way to get the base URL just + // from the nsIGlobalObject. + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (!window) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + Document* doc = window->GetExtantDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<nsIURI> baseURI = doc->GetDocBaseURI(); + if (!baseURI) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + return baseURI.forget(); +} + +} // anonymous namespace + +already_AddRefed<Promise> ServiceWorkerContainer::Register( + const nsAString& aScriptURL, const RegistrationOptions& aOptions, + const CallerType aCallerType, ErrorResult& aRv) { + // Note, we can't use GetGlobalIfValid() from the start here. If we + // hit a storage failure we want to log a message with the final + // scope string we put together below. + nsIGlobalObject* global = GetParentObject(); + if (!global) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + Maybe<ClientInfo> clientInfo = global->GetClientInfo(); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. + nsAutoCString scriptURL; + if (!AppendUTF16toUTF8(aScriptURL, scriptURL, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + nsCOMPtr<nsIURI> scriptURI; + nsresult rv = + NS_NewURI(getter_AddRefs(scriptURI), scriptURL, nullptr, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_INVALID_URL>(scriptURL); + return nullptr; + } + + // Never allow script URL with moz-extension scheme if support is fully + // disabled by the 'extensions.background_service_worker.enabled' pref. + if (scriptURI->SchemeIs("moz-extension") && + !StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // In ServiceWorkerContainer.register() the scope argument is parsed against + // different base URLs depending on whether it was passed or not. + nsCOMPtr<nsIURI> scopeURI; + + // Step 4. If none passed, parse against script's URL + if (!aOptions.mScope.WasPassed()) { + constexpr auto defaultScope = "./"_ns; + rv = NS_NewURI(getter_AddRefs(scopeURI), defaultScope, nullptr, scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsAutoCString spec; + scriptURI->GetSpec(spec); + aRv.ThrowTypeError<MSG_INVALID_SCOPE>(defaultScope, spec); + return nullptr; + } + } else { + // Step 5. Parse against entry settings object's base URL. + rv = NS_NewURI(getter_AddRefs(scopeURI), aOptions.mScope.Value(), nullptr, + baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsIURI* uri = baseURI ? baseURI : scriptURI; + nsAutoCString spec; + uri->GetSpec(spec); + aRv.ThrowTypeError<MSG_INVALID_SCOPE>( + NS_ConvertUTF16toUTF8(aOptions.mScope.Value()), spec); + return nullptr; + } + } + + // Strip the any ref from both the script and scope URLs. + nsCOMPtr<nsIURI> cloneWithoutRef; + aRv = NS_GetURIWithoutRef(scriptURI, getter_AddRefs(cloneWithoutRef)); + if (aRv.Failed()) { + return nullptr; + } + scriptURI = std::move(cloneWithoutRef); + + aRv = NS_GetURIWithoutRef(scopeURI, getter_AddRefs(cloneWithoutRef)); + if (aRv.Failed()) { + return nullptr; + } + scopeURI = std::move(cloneWithoutRef); + + ServiceWorkerScopeAndScriptAreValid(clientInfo.ref(), scopeURI, scriptURI, + aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global); + if (!window) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + Document* doc = window->GetExtantDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // The next section of code executes an NS_CheckContentLoadPolicy() + // check. This is necessary to enforce the CSP of the calling client. + // Currently this requires an Document. Once bug 965637 lands we + // should try to move this into ServiceWorkerScopeAndScriptAreValid() + // using the ClientInfo instead of doing a window-specific check here. + // See bug 1455077 for further investigation. + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo( + doc->NodePrincipal(), // loading principal + doc->NodePrincipal(), // triggering principal + doc, // loading node + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER); + + // Check content policy. + int16_t decision = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(scriptURI, secCheckLoadInfo, + "application/javascript"_ns, &decision); + if (NS_FAILED(rv)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) { + aRv.Throw(NS_ERROR_CONTENT_BLOCKED); + return nullptr; + } + + // Get the string representation for both the script and scope since + // we sanitized them above. + nsCString cleanedScopeURL; + aRv = scopeURI->GetSpec(cleanedScopeURL); + if (aRv.Failed()) { + return nullptr; + } + + nsCString cleanedScriptURL; + aRv = scriptURI->GetSpec(cleanedScriptURL); + if (aRv.Failed()) { + return nullptr; + } + + // Verify that the global is valid and has permission to store + // data. We perform this late so that we can report the final + // scope URL in any error message. + Unused << GetGlobalIfValid(aRv, [&](Document* aDoc) { + AutoTArray<nsString, 1> param; + CopyUTF8toUTF16(cleanedScopeURL, *param.AppendElement()); + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + "Service Workers"_ns, aDoc, + nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerRegisterStorageError", param); + }); + + window->NoteCalledRegisterForServiceWorkerScope(cleanedScopeURL); + + RefPtr<Promise> outer = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + + if (!mActor) { + aRv.ThrowInvalidStateError("Can't register service worker"); + return nullptr; + } + + mActor->SendRegister( + clientInfo.ref().ToIPC(), nsCString(cleanedScopeURL), + nsCString(cleanedScriptURL), aOptions.mUpdateViaCache, + [self, + outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult:: + TCopyableErrorResult) { + // application layer error + CopyableErrorResult rv = aResult.get_CopyableErrorResult(); + MOZ_DIAGNOSTIC_ASSERT(rv.Failed()); + outer->MaybeReject(std::move(rv)); + return; + } + // success + const auto& ipcDesc = + aResult.get_IPCServiceWorkerRegistrationDescriptor(); + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration( + ServiceWorkerRegistrationDescriptor(ipcDesc)); + outer->MaybeResolve(reg); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + CopyableErrorResult rv; + rv.ThrowInvalidStateError("Failed to register service worker"); + outer->MaybeReject(std::move(rv)); + }); + + return outer.forget(); +} + +already_AddRefed<ServiceWorker> ServiceWorkerContainer::GetController() { + RefPtr<ServiceWorker> ref = mControllerWorker; + return ref.forget(); +} + +already_AddRefed<Promise> ServiceWorkerContainer::GetRegistrations( + ErrorResult& aRv) { + nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + "Service Workers"_ns, aDoc, + nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerGetRegistrationStorageError"); + }); + if (aRv.Failed()) { + return nullptr; + } + + Maybe<ClientInfo> clientInfo = global->GetClientInfo(); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + RefPtr<Promise> outer = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + + if (!mActor) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return outer.forget(); + } + + mActor->SendGetRegistrations( + clientInfo.ref().ToIPC(), + [self, outer]( + const IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult:: + TCopyableErrorResult) { + // application layer error + const auto& rv = aResult.get_CopyableErrorResult(); + MOZ_DIAGNOSTIC_ASSERT(rv.Failed()); + outer->MaybeReject(CopyableErrorResult(rv)); + return; + } + // success + const auto& ipcList = + aResult.get_IPCServiceWorkerRegistrationDescriptorList(); + nsTArray<ServiceWorkerRegistrationDescriptor> list( + ipcList.values().Length()); + for (const auto& ipcDesc : ipcList.values()) { + list.AppendElement(ServiceWorkerRegistrationDescriptor(ipcDesc)); + } + + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + nsTArray<RefPtr<ServiceWorkerRegistration>> regList; + for (auto& desc : list) { + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration(desc); + if (reg) { + regList.AppendElement(std::move(reg)); + } + } + outer->MaybeResolve(regList); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + }); + + return outer.forget(); +} + +void ServiceWorkerContainer::StartMessages() { + while (!mPendingMessages.IsEmpty()) { + EnqueueReceivedMessageDispatch(mPendingMessages.ElementAt(0)); + mPendingMessages.RemoveElementAt(0); + } + mMessagesStarted = true; +} + +already_AddRefed<Promise> ServiceWorkerContainer::GetRegistration( + const nsAString& aURL, ErrorResult& aRv) { + nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + "Service Workers"_ns, aDoc, + nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerGetRegistrationStorageError"); + }); + if (aRv.Failed()) { + return nullptr; + } + + Maybe<ClientInfo> clientInfo = global->GetClientInfo(); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsCOMPtr<nsIURI> uri; + aRv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, baseURI); + if (aRv.Failed()) { + return nullptr; + } + + nsCString spec; + aRv = uri->GetSpec(spec); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<Promise> outer = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + + if (!mActor) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return outer.forget(); + } + + mActor->SendGetRegistration( + clientInfo.ref().ToIPC(), spec, + [self, + outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult:: + TCopyableErrorResult) { + CopyableErrorResult ipcRv(aResult.get_CopyableErrorResult()); + ErrorResult rv(std::move(ipcRv)); + if (!rv.Failed()) { + // ErrorResult rv; + // If rv is a failure then this is an application layer error. + // Note, though, we also reject with NS_OK to indicate that we just + // didn't find a registration. + Unused << self->GetGlobalIfValid(rv); + if (!rv.Failed()) { + outer->MaybeResolveWithUndefined(); + return; + } + } + outer->MaybeReject(std::move(rv)); + return; + } + // success + const auto& ipcDesc = + aResult.get_IPCServiceWorkerRegistrationDescriptor(); + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration( + ServiceWorkerRegistrationDescriptor(ipcDesc)); + outer->MaybeResolve(reg); + }, + [self, outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + }); + return outer.forget(); +} + +Promise* ServiceWorkerContainer::GetReady(ErrorResult& aRv) { + if (mReadyPromise) { + return mReadyPromise; + } + + nsIGlobalObject* global = GetGlobalIfValid(aRv); + if (aRv.Failed()) { + return nullptr; + } + MOZ_DIAGNOSTIC_ASSERT(global); + + Maybe<ClientInfo> clientInfo(global->GetClientInfo()); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + mReadyPromise = + Promise::Create(global, aRv, Promise::ePropagateUserInteraction); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<ServiceWorkerContainer> self = this; + RefPtr<Promise> outer = mReadyPromise; + + if (!mActor) { + mReadyPromise->MaybeReject( + CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return mReadyPromise; + } + + mActor->SendGetReady( + clientInfo.ref().ToIPC(), + [self, + outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult:: + TCopyableErrorResult) { + // application layer error + CopyableErrorResult rv(aResult.get_CopyableErrorResult()); + MOZ_DIAGNOSTIC_ASSERT(rv.Failed()); + outer->MaybeReject(std::move(rv)); + return; + } + // success + const auto& ipcDesc = + aResult.get_IPCServiceWorkerRegistrationDescriptor(); + ErrorResult rv; + nsIGlobalObject* global = self->GetGlobalIfValid(rv); + if (rv.Failed()) { + outer->MaybeReject(std::move(rv)); + return; + } + RefPtr<ServiceWorkerRegistration> reg = + global->GetOrCreateServiceWorkerRegistration( + ServiceWorkerRegistrationDescriptor(ipcDesc)); + NS_ENSURE_TRUE_VOID(reg); + + // Don't resolve the ready promise until the registration has + // reached the right version. This ensures that the active + // worker property is set correctly on the registration. + reg->WhenVersionReached(ipcDesc.version(), [outer, reg](bool aResult) { + outer->MaybeResolve(reg); + }); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeReject(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + }); + + return mReadyPromise; +} + +// Testing only. +void ServiceWorkerContainer::GetScopeForUrl(const nsAString& aUrl, + nsString& aScope, + ErrorResult& aRv) { + nsCOMPtr<nsIServiceWorkerManager> swm = + mozilla::components::ServiceWorkerManager::Service(); + if (!swm) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = StoragePrincipalHelper::GetPrincipal( + window, + StaticPrefs::privacy_partition_serviceWorkers() + ? StoragePrincipalHelper::eForeignPartitionedPrincipal + : StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(principal)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + aRv = swm->GetScopeForUrl(principal, aUrl, aScope); +} + +nsIGlobalObject* ServiceWorkerContainer::GetGlobalIfValid( + ErrorResult& aRv, + const std::function<void(Document*)>&& aStorageFailureCB) const { + // For now we require a window since ServiceWorkerContainer is + // not exposed on worker globals yet. The main thing we need + // to fix here to support that is the storage access check via + // the nsIGlobalObject. + nsPIDOMWindowInner* window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<Document> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // Don't allow a service worker to access service worker registrations + // from a window with storage disabled. If these windows can access + // the registration it increases the chance they can bypass the storage + // block via postMessage(), etc. + auto storageAllowed = StorageAllowedForWindow(window); + if (NS_WARN_IF(storageAllowed != StorageAccess::eAllow && + (!StaticPrefs::privacy_partition_serviceWorkers() || + !StoragePartitioningEnabled(storageAllowed, + doc->CookieJarSettings())))) { + if (aStorageFailureCB) { + aStorageFailureCB(doc); + } + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // Don't allow service workers when the document is chrome. + if (NS_WARN_IF(doc->NodePrincipal()->IsSystemPrincipal())) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + return window->AsGlobal(); +} + +void ServiceWorkerContainer::EnqueueReceivedMessageDispatch( + RefPtr<ReceivedMessage> aMessage) { + if (nsPIDOMWindowInner* const window = GetOwner()) { + if (auto* const target = window->EventTargetFor(TaskCategory::Other)) { + target->Dispatch(NewRunnableMethod<RefPtr<ReceivedMessage>>( + "ServiceWorkerContainer::DispatchMessage", this, + &ServiceWorkerContainer::DispatchMessage, std::move(aMessage))); + } + } +} + +template <typename F> +void ServiceWorkerContainer::RunWithJSContext(F&& aCallable) { + nsCOMPtr<nsIGlobalObject> globalObject; + if (nsPIDOMWindowInner* const window = GetOwner()) { + globalObject = do_QueryInterface(window); + } + + // If AutoJSAPI::Init() fails then either global is nullptr or not + // in a usable state. + AutoJSAPI jsapi; + if (!jsapi.Init(globalObject)) { + return; + } + + aCallable(jsapi.cx(), globalObject); +} + +void ServiceWorkerContainer::DispatchMessage(RefPtr<ReceivedMessage> aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + + // When dispatching a message, either DOMContentLoaded has already + // been fired, or someone called startMessages() or set onmessage. + // Either way, a global object is supposed to be present. If it's + // not, we'd fail to initialize the JS API and exit. + RunWithJSContext([this, message = std::move(aMessage)]( + JSContext* const aCx, nsIGlobalObject* const aGlobal) { + ErrorResult result; + bool deserializationFailed = false; + RootedDictionary<MessageEventInit> init(aCx); + auto res = FillInMessageEventInit(aCx, aGlobal, *message, init, result); + if (res.isErr()) { + deserializationFailed = res.unwrapErr(); + MOZ_ASSERT_IF(deserializationFailed, init.mData.isNull()); + MOZ_ASSERT_IF(deserializationFailed, init.mPorts.IsEmpty()); + MOZ_ASSERT_IF(deserializationFailed, !init.mOrigin.IsEmpty()); + MOZ_ASSERT_IF(deserializationFailed, !init.mSource.IsNull()); + result.SuppressException(); + + if (!deserializationFailed && result.MaybeSetPendingException(aCx)) { + return; + } + } + + RefPtr<MessageEvent> event = MessageEvent::Constructor( + this, deserializationFailed ? u"messageerror"_ns : u"message"_ns, init); + event->SetTrusted(true); + + result = NS_OK; + DispatchEvent(*event, result); + if (result.Failed()) { + result.SuppressException(); + } + }); +} + +namespace { + +nsresult FillInOriginNoSuffix(const ServiceWorkerDescriptor& aServiceWorker, + nsString& aOrigin) { + using mozilla::ipc::PrincipalInfoToPrincipal; + + nsresult rv; + + auto principalOrErr = + PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo()); + if (NS_WARN_IF(principalOrErr.isErr())) { + return principalOrErr.unwrapErr(); + } + + nsAutoCString originUTF8; + rv = principalOrErr.unwrap()->GetOriginNoSuffix(originUTF8); + if (NS_FAILED(rv)) { + return rv; + } + + CopyUTF8toUTF16(originUTF8, aOrigin); + return NS_OK; +} + +} // namespace + +Result<Ok, bool> ServiceWorkerContainer::FillInMessageEventInit( + JSContext* const aCx, nsIGlobalObject* const aGlobal, + ReceivedMessage& aMessage, MessageEventInit& aInit, ErrorResult& aRv) { + // Determining the source and origin should preceed attempting deserialization + // because on a "messageerror" event (i.e. when deserialization fails), the + // dispatched message needs to contain such an origin and source, per spec: + // + // "If this throws an exception, catch it, fire an event named messageerror + // at destination, using MessageEvent, with the origin attribute initialized + // to origin and the source attribute initialized to source, and then abort + // these steps." - 6.4 of postMessage + // See: https://w3c.github.io/ServiceWorker/#service-worker-postmessage + const RefPtr<ServiceWorker> serviceWorkerInstance = + aGlobal->GetOrCreateServiceWorker(aMessage.mServiceWorker); + if (serviceWorkerInstance) { + aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance; + } + + const nsresult rv = + FillInOriginNoSuffix(aMessage.mServiceWorker, aInit.mOrigin); + if (NS_FAILED(rv)) { + return Err(false); + } + + JS::Rooted<JS::Value> messageData(aCx); + aMessage.mClonedData.Read(aCx, &messageData, aRv); + if (aRv.Failed()) { + return Err(true); + } + + aInit.mData = messageData; + + if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) { + xpc::Throw(aCx, NS_ERROR_OUT_OF_MEMORY); + return Err(false); + } + + return Ok(); +} + +void ServiceWorkerContainer::Shutdown() { + if (mShutdown) { + return; + } + mShutdown = true; + + if (mActor) { + mActor->RevokeOwner(this); + mActor->MaybeStartTeardown(); + mActor = nullptr; + } +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerContainer.h b/dom/serviceworkers/ServiceWorkerContainer.h new file mode 100644 index 0000000000..3a5dd5fa5d --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainer.h @@ -0,0 +1,143 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkercontainer_h__ +#define mozilla_dom_serviceworkercontainer_h__ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/ServiceWorkerUtils.h" + +class nsIGlobalWindow; + +namespace mozilla::dom { + +class ClientPostMessageArgs; +struct MessageEventInit; +class Promise; +struct RegistrationOptions; +class ServiceWorker; +class ServiceWorkerContainerChild; + +// Lightweight serviceWorker APIs collection. +class ServiceWorkerContainer final : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerContainer, + DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(controllerchange) + IMPL_EVENT_HANDLER(messageerror) + + // Almost a manual expansion of IMPL_EVENT_HANDLER(message), but + // with the additional StartMessages() when setting the handler, as + // required by the spec. + inline mozilla::dom::EventHandlerNonNull* GetOnmessage() { + return GetEventHandler(nsGkAtoms::onmessage); + } + inline void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback) { + SetEventHandler(nsGkAtoms::onmessage, aCallback); + StartMessages(); + } + + static bool IsEnabled(JSContext* aCx, JSObject* aGlobal); + + static already_AddRefed<ServiceWorkerContainer> Create( + nsIGlobalObject* aGlobal); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + already_AddRefed<Promise> Register(const nsAString& aScriptURL, + const RegistrationOptions& aOptions, + const CallerType aCallerType, + ErrorResult& aRv); + + already_AddRefed<ServiceWorker> GetController(); + + already_AddRefed<Promise> GetRegistration(const nsAString& aDocumentURL, + ErrorResult& aRv); + + already_AddRefed<Promise> GetRegistrations(ErrorResult& aRv); + + void StartMessages(); + + Promise* GetReady(ErrorResult& aRv); + + // Testing only. + void GetScopeForUrl(const nsAString& aUrl, nsString& aScope, + ErrorResult& aRv); + + // DOMEventTargetHelper + void DisconnectFromOwner() override; + + // Invalidates |mControllerWorker| and dispatches a "controllerchange" + // event. + void ControllerChanged(ErrorResult& aRv); + + void ReceiveMessage(const ClientPostMessageArgs& aArgs); + + void RevokeActor(ServiceWorkerContainerChild* aActor); + + private: + explicit ServiceWorkerContainer(nsIGlobalObject* aGlobal); + + ~ServiceWorkerContainer(); + + // Utility method to get the global if its present and if certain + // additional validaty checks pass. One of these additional checks + // verifies the global can access storage. Since storage access can + // vary based on user settings we want to often provide some error + // message if the storage check fails. This method takes an optional + // callback that can be used to report the storage failure to the + // devtools console. + nsIGlobalObject* GetGlobalIfValid( + ErrorResult& aRv, + const std::function<void(Document*)>&& aStorageFailureCB = nullptr) const; + + struct ReceivedMessage; + + // Dispatch a Runnable that dispatches the given message on this + // object. When the owner of this object is a Window, the Runnable + // is dispatched on the corresponding TabGroup. + void EnqueueReceivedMessageDispatch(RefPtr<ReceivedMessage> aMessage); + + template <typename F> + void RunWithJSContext(F&& aCallable); + + void DispatchMessage(RefPtr<ReceivedMessage> aMessage); + + // When it fails, returning boolean means whether it's because deserailization + // failed or not. + static Result<Ok, bool> FillInMessageEventInit(JSContext* aCx, + nsIGlobalObject* aGlobal, + ReceivedMessage& aMessage, + MessageEventInit& aInit, + ErrorResult& aRv); + + void Shutdown(); + + RefPtr<ServiceWorkerContainerChild> mActor; + bool mShutdown; + + // This only changes when a worker hijacks everything in its scope by calling + // claim. + RefPtr<ServiceWorker> mControllerWorker; + + RefPtr<Promise> mReadyPromise; + MozPromiseRequestHolder<ServiceWorkerRegistrationPromise> mReadyPromiseHolder; + + // Set after StartMessages() has been called. + bool mMessagesStarted = false; + + // Queue holding messages posted from service worker as long as + // StartMessages() hasn't been called. + nsTArray<RefPtr<ReceivedMessage>> mPendingMessages; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_serviceworkercontainer_h__ */ diff --git a/dom/serviceworkers/ServiceWorkerContainerChild.cpp b/dom/serviceworkers/ServiceWorkerContainerChild.cpp new file mode 100644 index 0000000000..1093bca91a --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainerChild.cpp @@ -0,0 +1,70 @@ +/* -*- 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/PServiceWorkerContainerChild.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" + +#include "ServiceWorkerContainer.h" +#include "ServiceWorkerContainerChild.h" + +namespace mozilla::dom { + +void ServiceWorkerContainerChild::ActorDestroy(ActorDestroyReason aReason) { + mIPCWorkerRef = nullptr; + + if (mOwner) { + mOwner->RevokeActor(this); + MOZ_DIAGNOSTIC_ASSERT(!mOwner); + } +} + +// static +already_AddRefed<ServiceWorkerContainerChild> +ServiceWorkerContainerChild::Create() { + RefPtr actor = new ServiceWorkerContainerChild; + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_DIAGNOSTIC_ASSERT(workerPrivate); + + RefPtr<IPCWorkerRefHelper<ServiceWorkerContainerChild>> helper = + new IPCWorkerRefHelper<ServiceWorkerContainerChild>(actor); + + actor->mIPCWorkerRef = IPCWorkerRef::Create( + workerPrivate, "ServiceWorkerContainerChild", + [helper] { helper->Actor()->MaybeStartTeardown(); }); + if (NS_WARN_IF(!actor->mIPCWorkerRef)) { + return nullptr; + } + } + + return actor.forget(); +} + +ServiceWorkerContainerChild::ServiceWorkerContainerChild() + : mOwner(nullptr), mTeardownStarted(false) {} + +void ServiceWorkerContainerChild::SetOwner(ServiceWorkerContainer* aOwner) { + MOZ_DIAGNOSTIC_ASSERT(!mOwner); + MOZ_DIAGNOSTIC_ASSERT(aOwner); + mOwner = aOwner; +} + +void ServiceWorkerContainerChild::RevokeOwner(ServiceWorkerContainer* aOwner) { + MOZ_DIAGNOSTIC_ASSERT(mOwner); + MOZ_DIAGNOSTIC_ASSERT(aOwner == mOwner); + mOwner = nullptr; +} + +void ServiceWorkerContainerChild::MaybeStartTeardown() { + if (mTeardownStarted) { + return; + } + mTeardownStarted = true; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerContainerChild.h b/dom/serviceworkers/ServiceWorkerContainerChild.h new file mode 100644 index 0000000000..4d9df91f0b --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainerChild.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkercontainerchild_h__ +#define mozilla_dom_serviceworkercontainerchild_h__ + +#include "mozilla/dom/PServiceWorkerContainerChild.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +class ServiceWorkerContainer; + +class IPCWorkerRef; + +class ServiceWorkerContainerChild final : public PServiceWorkerContainerChild { + RefPtr<IPCWorkerRef> mIPCWorkerRef; + ServiceWorkerContainer* mOwner; + bool mTeardownStarted; + + ServiceWorkerContainerChild(); + + ~ServiceWorkerContainerChild() = default; + + // PServiceWorkerContainerChild + void ActorDestroy(ActorDestroyReason aReason) override; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerContainerChild, override); + + static already_AddRefed<ServiceWorkerContainerChild> Create(); + + void SetOwner(ServiceWorkerContainer* aOwner); + + void RevokeOwner(ServiceWorkerContainer* aOwner); + + void MaybeStartTeardown(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkercontainerchild_h__ diff --git a/dom/serviceworkers/ServiceWorkerContainerParent.cpp b/dom/serviceworkers/ServiceWorkerContainerParent.cpp new file mode 100644 index 0000000000..664586d5e8 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainerParent.cpp @@ -0,0 +1,129 @@ +/* -*- 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 "ServiceWorkerContainerParent.h" + +#include "ServiceWorkerContainerProxy.h" +#include "mozilla/dom/ClientInfo.h" + +namespace mozilla::dom { + +using mozilla::ipc::IPCResult; + +void ServiceWorkerContainerParent::ActorDestroy(ActorDestroyReason aReason) { + if (mProxy) { + mProxy->RevokeActor(this); + mProxy = nullptr; + } +} + +IPCResult ServiceWorkerContainerParent::RecvTeardown() { + Unused << Send__delete__(this); + return IPC_OK(); +} + +IPCResult ServiceWorkerContainerParent::RecvRegister( + const IPCClientInfo& aClientInfo, const nsACString& aScopeURL, + const nsACString& aScriptURL, + const ServiceWorkerUpdateViaCache& aUpdateViaCache, + RegisterResolver&& aResolver) { + if (!mProxy) { + aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return IPC_OK(); + } + + mProxy + ->Register(ClientInfo(aClientInfo), aScopeURL, aScriptURL, + aUpdateViaCache) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) { + aResolver(aDescriptor.ToIPC()); + }, + [aResolver](const CopyableErrorResult& aResult) { + aResolver(aResult); + }); + + return IPC_OK(); +} + +IPCResult ServiceWorkerContainerParent::RecvGetRegistration( + const IPCClientInfo& aClientInfo, const nsACString& aURL, + GetRegistrationResolver&& aResolver) { + if (!mProxy) { + aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return IPC_OK(); + } + + mProxy->GetRegistration(ClientInfo(aClientInfo), aURL) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) { + aResolver(aDescriptor.ToIPC()); + }, + [aResolver](const CopyableErrorResult& aResult) { + aResolver(aResult); + }); + + return IPC_OK(); +} + +IPCResult ServiceWorkerContainerParent::RecvGetRegistrations( + const IPCClientInfo& aClientInfo, GetRegistrationsResolver&& aResolver) { + if (!mProxy) { + aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return IPC_OK(); + } + + mProxy->GetRegistrations(ClientInfo(aClientInfo)) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver]( + const nsTArray<ServiceWorkerRegistrationDescriptor>& aList) { + IPCServiceWorkerRegistrationDescriptorList ipcList; + for (auto& desc : aList) { + ipcList.values().AppendElement(desc.ToIPC()); + } + aResolver(std::move(ipcList)); + }, + [aResolver](const CopyableErrorResult& aResult) { + aResolver(aResult); + }); + + return IPC_OK(); +} + +IPCResult ServiceWorkerContainerParent::RecvGetReady( + const IPCClientInfo& aClientInfo, GetReadyResolver&& aResolver) { + if (!mProxy) { + aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return IPC_OK(); + } + + mProxy->GetReady(ClientInfo(aClientInfo)) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) { + aResolver(aDescriptor.ToIPC()); + }, + [aResolver](const CopyableErrorResult& aResult) { + aResolver(aResult); + }); + + return IPC_OK(); +} + +ServiceWorkerContainerParent::ServiceWorkerContainerParent() = default; + +ServiceWorkerContainerParent::~ServiceWorkerContainerParent() { + MOZ_DIAGNOSTIC_ASSERT(!mProxy); +} + +void ServiceWorkerContainerParent::Init() { + mProxy = new ServiceWorkerContainerProxy(this); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerContainerParent.h b/dom/serviceworkers/ServiceWorkerContainerParent.h new file mode 100644 index 0000000000..787a026c18 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainerParent.h @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkercontainerparent_h__ +#define mozilla_dom_serviceworkercontainerparent_h__ + +#include "mozilla/dom/PServiceWorkerContainerParent.h" + +namespace mozilla::dom { + +class IPCServiceWorkerDescriptor; +class ServiceWorkerContainerProxy; + +class ServiceWorkerContainerParent final + : public PServiceWorkerContainerParent { + RefPtr<ServiceWorkerContainerProxy> mProxy; + + ~ServiceWorkerContainerParent(); + + // PServiceWorkerContainerParent + void ActorDestroy(ActorDestroyReason aReason) override; + + mozilla::ipc::IPCResult RecvTeardown() override; + + mozilla::ipc::IPCResult RecvRegister( + const IPCClientInfo& aClientInfo, const nsACString& aScopeURL, + const nsACString& aScriptURL, + const ServiceWorkerUpdateViaCache& aUpdateViaCache, + RegisterResolver&& aResolver) override; + + mozilla::ipc::IPCResult RecvGetRegistration( + const IPCClientInfo& aClientInfo, const nsACString& aURL, + GetRegistrationResolver&& aResolver) override; + + mozilla::ipc::IPCResult RecvGetRegistrations( + const IPCClientInfo& aClientInfo, + GetRegistrationsResolver&& aResolver) override; + + mozilla::ipc::IPCResult RecvGetReady(const IPCClientInfo& aClientInfo, + GetReadyResolver&& aResolver) override; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerContainerParent, override); + + ServiceWorkerContainerParent(); + + void Init(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkercontainerparent_h__ diff --git a/dom/serviceworkers/ServiceWorkerContainerProxy.cpp b/dom/serviceworkers/ServiceWorkerContainerProxy.cpp new file mode 100644 index 0000000000..71a853e1ee --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainerProxy.cpp @@ -0,0 +1,153 @@ +/* -*- 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 "ServiceWorkerContainerProxy.h" + +#include "mozilla/dom/ServiceWorkerContainerParent.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" + +namespace mozilla::dom { + +using mozilla::ipc::AssertIsOnBackgroundThread; + +ServiceWorkerContainerProxy::~ServiceWorkerContainerProxy() { + // Any thread + MOZ_DIAGNOSTIC_ASSERT(!mActor); +} + +ServiceWorkerContainerProxy::ServiceWorkerContainerProxy( + ServiceWorkerContainerParent* aActor) + : mActor(aActor) { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mActor); + + // The container does not directly listen for updates, so we don't need + // to immediately initialize. The controllerchange event comes via the + // ClientSource associated with the ServiceWorkerContainer's bound global. +} + +void ServiceWorkerContainerProxy::RevokeActor( + ServiceWorkerContainerParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor = nullptr; +} + +RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerContainerProxy::Register( + const ClientInfo& aClientInfo, const nsACString& aScopeURL, + const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationPromise::Private> promise = + new ServiceWorkerRegistrationPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, + [aClientInfo, aScopeURL = nsCString(aScopeURL), + aScriptURL = nsCString(aScriptURL), aUpdateViaCache, promise]() mutable { + auto scopeExit = MakeScopeExit( + [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + swm->Register(aClientInfo, aScopeURL, aScriptURL, aUpdateViaCache) + ->ChainTo(promise.forget(), __func__); + + scopeExit.release(); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +RefPtr<ServiceWorkerRegistrationPromise> +ServiceWorkerContainerProxy::GetRegistration(const ClientInfo& aClientInfo, + const nsACString& aURL) { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationPromise::Private> promise = + new ServiceWorkerRegistrationPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [aClientInfo, aURL = nsCString(aURL), promise]() mutable { + auto scopeExit = MakeScopeExit( + [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + swm->GetRegistration(aClientInfo, aURL) + ->ChainTo(promise.forget(), __func__); + + scopeExit.release(); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +RefPtr<ServiceWorkerRegistrationListPromise> +ServiceWorkerContainerProxy::GetRegistrations(const ClientInfo& aClientInfo) { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationListPromise::Private> promise = + new ServiceWorkerRegistrationListPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [aClientInfo, promise]() mutable { + auto scopeExit = MakeScopeExit( + [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + swm->GetRegistrations(aClientInfo)->ChainTo(promise.forget(), __func__); + + scopeExit.release(); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerContainerProxy::GetReady( + const ClientInfo& aClientInfo) { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationPromise::Private> promise = + new ServiceWorkerRegistrationPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [aClientInfo, promise]() mutable { + auto scopeExit = MakeScopeExit( + [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + swm->WhenReady(aClientInfo)->ChainTo(promise.forget(), __func__); + + scopeExit.release(); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerContainerProxy.h b/dom/serviceworkers/ServiceWorkerContainerProxy.h new file mode 100644 index 0000000000..b380465a3e --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerContainerProxy.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef moz_dom_ServiceWorkerContainerProxy_h +#define moz_dom_ServiceWorkerContainerProxy_h + +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/RefPtr.h" + +namespace mozilla::dom { + +class ServiceWorkerContainerParent; + +class ServiceWorkerContainerProxy final { + // Background thread only + RefPtr<ServiceWorkerContainerParent> mActor; + + ~ServiceWorkerContainerProxy(); + + public: + explicit ServiceWorkerContainerProxy(ServiceWorkerContainerParent* aActor); + + void RevokeActor(ServiceWorkerContainerParent* aActor); + + RefPtr<ServiceWorkerRegistrationPromise> Register( + const ClientInfo& aClientInfo, const nsACString& aScopeURL, + const nsACString& aScriptURL, + ServiceWorkerUpdateViaCache aUpdateViaCache); + + RefPtr<ServiceWorkerRegistrationPromise> GetRegistration( + const ClientInfo& aClientInfo, const nsACString& aURL); + + RefPtr<ServiceWorkerRegistrationListPromise> GetRegistrations( + const ClientInfo& aClientInfo); + + RefPtr<ServiceWorkerRegistrationPromise> GetReady( + const ClientInfo& aClientInfo); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerContainerProxy); +}; + +} // namespace mozilla::dom + +#endif // moz_dom_ServiceWorkerContainerProxy_h diff --git a/dom/serviceworkers/ServiceWorkerDescriptor.cpp b/dom/serviceworkers/ServiceWorkerDescriptor.cpp new file mode 100644 index 0000000000..898f271fea --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerDescriptor.cpp @@ -0,0 +1,141 @@ +/* -*- 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 "ServiceWorkerDescriptor.h" +#include "mozilla/dom/IPCServiceWorkerDescriptor.h" +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" + +namespace mozilla::dom { + +using mozilla::ipc::PrincipalInfo; +using mozilla::ipc::PrincipalInfoToPrincipal; + +ServiceWorkerDescriptor::ServiceWorkerDescriptor( + uint64_t aId, uint64_t aRegistrationId, uint64_t aRegistrationVersion, + nsIPrincipal* aPrincipal, const nsACString& aScope, + const nsACString& aScriptURL, ServiceWorkerState aState) + : mData(MakeUnique<IPCServiceWorkerDescriptor>()) { + MOZ_ALWAYS_SUCCEEDS( + PrincipalToPrincipalInfo(aPrincipal, &mData->principalInfo())); + + mData->id() = aId; + mData->registrationId() = aRegistrationId; + mData->registrationVersion() = aRegistrationVersion; + mData->scope() = aScope; + mData->scriptURL() = aScriptURL; + mData->state() = aState; + // Set HandlesFetch as true in default + mData->handlesFetch() = true; +} + +ServiceWorkerDescriptor::ServiceWorkerDescriptor( + uint64_t aId, uint64_t aRegistrationId, uint64_t aRegistrationVersion, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const nsACString& aScope, + const nsACString& aScriptURL, ServiceWorkerState aState) + : mData(MakeUnique<IPCServiceWorkerDescriptor>( + aId, aRegistrationId, aRegistrationVersion, aPrincipalInfo, + nsCString(aScriptURL), nsCString(aScope), aState, true)) {} + +ServiceWorkerDescriptor::ServiceWorkerDescriptor( + const IPCServiceWorkerDescriptor& aDescriptor) + : mData(MakeUnique<IPCServiceWorkerDescriptor>(aDescriptor)) {} + +ServiceWorkerDescriptor::ServiceWorkerDescriptor( + const ServiceWorkerDescriptor& aRight) { + operator=(aRight); +} + +ServiceWorkerDescriptor& ServiceWorkerDescriptor::operator=( + const ServiceWorkerDescriptor& aRight) { + if (this == &aRight) { + return *this; + } + mData.reset(); + mData = MakeUnique<IPCServiceWorkerDescriptor>(*aRight.mData); + return *this; +} + +ServiceWorkerDescriptor::ServiceWorkerDescriptor( + ServiceWorkerDescriptor&& aRight) + : mData(std::move(aRight.mData)) {} + +ServiceWorkerDescriptor& ServiceWorkerDescriptor::operator=( + ServiceWorkerDescriptor&& aRight) { + mData.reset(); + mData = std::move(aRight.mData); + return *this; +} + +ServiceWorkerDescriptor::~ServiceWorkerDescriptor() = default; + +bool ServiceWorkerDescriptor::operator==( + const ServiceWorkerDescriptor& aRight) const { + return *mData == *aRight.mData; +} + +uint64_t ServiceWorkerDescriptor::Id() const { return mData->id(); } + +uint64_t ServiceWorkerDescriptor::RegistrationId() const { + return mData->registrationId(); +} + +uint64_t ServiceWorkerDescriptor::RegistrationVersion() const { + return mData->registrationVersion(); +} + +const mozilla::ipc::PrincipalInfo& ServiceWorkerDescriptor::PrincipalInfo() + const { + return mData->principalInfo(); +} + +Result<nsCOMPtr<nsIPrincipal>, nsresult> ServiceWorkerDescriptor::GetPrincipal() + const { + AssertIsOnMainThread(); + return PrincipalInfoToPrincipal(mData->principalInfo()); +} + +const nsCString& ServiceWorkerDescriptor::Scope() const { + return mData->scope(); +} + +const nsCString& ServiceWorkerDescriptor::ScriptURL() const { + return mData->scriptURL(); +} + +ServiceWorkerState ServiceWorkerDescriptor::State() const { + return mData->state(); +} + +void ServiceWorkerDescriptor::SetState(ServiceWorkerState aState) { + mData->state() = aState; +} + +void ServiceWorkerDescriptor::SetRegistrationVersion(uint64_t aVersion) { + MOZ_DIAGNOSTIC_ASSERT(aVersion > mData->registrationVersion()); + mData->registrationVersion() = aVersion; +} + +bool ServiceWorkerDescriptor::HandlesFetch() const { + return mData->handlesFetch(); +} + +void ServiceWorkerDescriptor::SetHandlesFetch(bool aHandlesFetch) { + mData->handlesFetch() = aHandlesFetch; +} + +bool ServiceWorkerDescriptor::Matches( + const ServiceWorkerDescriptor& aDescriptor) const { + return Id() == aDescriptor.Id() && Scope() == aDescriptor.Scope() && + ScriptURL() == aDescriptor.ScriptURL() && + PrincipalInfo() == aDescriptor.PrincipalInfo(); +} + +const IPCServiceWorkerDescriptor& ServiceWorkerDescriptor::ToIPC() const { + return *mData; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerDescriptor.h b/dom/serviceworkers/ServiceWorkerDescriptor.h new file mode 100644 index 0000000000..b85890089b --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerDescriptor.h @@ -0,0 +1,100 @@ +/* -*- 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/. */ +#ifndef _mozilla_dom_ServiceWorkerDescriptor_h +#define _mozilla_dom_ServiceWorkerDescriptor_h + +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +class nsIPrincipal; + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class IPCServiceWorkerDescriptor; +enum class ServiceWorkerState : uint8_t; + +// This class represents a snapshot of a particular ServiceWorkerInfo object. +// It is threadsafe and can be transferred across processes. This is useful +// because most of its values are immutable and can be relied upon to be +// accurate. Currently the only variable field is the ServiceWorkerState. +class ServiceWorkerDescriptor final { + // This class is largely a wrapper around an IPDL generated struct. We + // need the wrapper class since IPDL generated code includes windows.h + // which is in turn incompatible with bindings code. + UniquePtr<IPCServiceWorkerDescriptor> mData; + + public: + ServiceWorkerDescriptor(uint64_t aId, uint64_t aRegistrationId, + uint64_t aRegistrationVersion, + nsIPrincipal* aPrincipal, const nsACString& aScope, + const nsACString& aScriptURL, + ServiceWorkerState aState); + + ServiceWorkerDescriptor(uint64_t aId, uint64_t aRegistrationId, + uint64_t aRegistrationVersion, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsACString& aScope, + const nsACString& aScriptURL, + ServiceWorkerState aState); + + explicit ServiceWorkerDescriptor( + const IPCServiceWorkerDescriptor& aDescriptor); + + ServiceWorkerDescriptor(const ServiceWorkerDescriptor& aRight); + + ServiceWorkerDescriptor& operator=(const ServiceWorkerDescriptor& aRight); + + ServiceWorkerDescriptor(ServiceWorkerDescriptor&& aRight); + + ServiceWorkerDescriptor& operator=(ServiceWorkerDescriptor&& aRight); + + ~ServiceWorkerDescriptor(); + + bool operator==(const ServiceWorkerDescriptor& aRight) const; + + uint64_t Id() const; + + uint64_t RegistrationId() const; + + uint64_t RegistrationVersion() const; + + const mozilla::ipc::PrincipalInfo& PrincipalInfo() const; + + Result<nsCOMPtr<nsIPrincipal>, nsresult> GetPrincipal() const; + + const nsCString& Scope() const; + + const nsCString& ScriptURL() const; + + ServiceWorkerState State() const; + + void SetState(ServiceWorkerState aState); + + void SetRegistrationVersion(uint64_t aVersion); + + bool HandlesFetch() const; + + void SetHandlesFetch(bool aHandlesFetch); + + // Try to determine if two workers match each other. This is less strict + // than an operator==() call since it ignores mutable values like State(). + bool Matches(const ServiceWorkerDescriptor& aDescriptor) const; + + // Expose the underlying IPC type so that it can be passed via IPC. + const IPCServiceWorkerDescriptor& ToIPC() const; +}; + +} // namespace dom +} // namespace mozilla + +#endif // _mozilla_dom_ServiceWorkerDescriptor_h diff --git a/dom/serviceworkers/ServiceWorkerEvents.cpp b/dom/serviceworkers/ServiceWorkerEvents.cpp new file mode 100644 index 0000000000..5c4b55ebcf --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerEvents.cpp @@ -0,0 +1,1278 @@ +/* -*- 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 "ServiceWorkerEvents.h" + +#include <utility> + +#include "ServiceWorker.h" +#include "ServiceWorkerManager.h" +#include "js/Conversions.h" +#include "js/Exception.h" // JS::ExceptionStack, JS::StealPendingExceptionStack +#include "js/TypeDecls.h" +#include "mozilla/Encoding.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/BodyUtil.h" +#include "mozilla/dom/Client.h" +#include "mozilla/dom/EventBinding.h" +#include "mozilla/dom/FetchEventBinding.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/PushEventBinding.h" +#include "mozilla/dom/PushMessageDataBinding.h" +#include "mozilla/dom/PushUtil.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ServiceWorkerOp.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/Telemetry.h" +#include "nsComponentManagerUtils.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsIConsoleReportCollector.h" +#include "nsINetworkInterceptController.h" +#include "nsIScriptError.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsSerializationHelper.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "xpcpublic.h" + +using namespace mozilla; +using namespace mozilla::dom; + +namespace { + +void AsyncLog(nsIInterceptedChannel* aInterceptedChannel, + const nsACString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, + uint32_t aRespondWithColumnNumber, const nsACString& aMessageName, + const nsTArray<nsString>& aParams) { + MOZ_ASSERT(aInterceptedChannel); + nsCOMPtr<nsIConsoleReportCollector> reporter = + aInterceptedChannel->GetConsoleReportCollector(); + if (reporter) { + reporter->AddConsoleReport(nsIScriptError::errorFlag, + "Service Worker Interception"_ns, + nsContentUtils::eDOM_PROPERTIES, + aRespondWithScriptSpec, aRespondWithLineNumber, + aRespondWithColumnNumber, aMessageName, aParams); + } +} + +template <typename... Params> +void AsyncLog(nsIInterceptedChannel* aInterceptedChannel, + const nsACString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, + uint32_t aRespondWithColumnNumber, + // We have to list one explicit string so that calls with an + // nsTArray of params won't end up in here. + const nsACString& aMessageName, const nsAString& aFirstParam, + Params&&... aParams) { + nsTArray<nsString> paramsList(sizeof...(Params) + 1); + StringArrayAppender::Append(paramsList, sizeof...(Params) + 1, aFirstParam, + std::forward<Params>(aParams)...); + AsyncLog(aInterceptedChannel, aRespondWithScriptSpec, aRespondWithLineNumber, + aRespondWithColumnNumber, aMessageName, paramsList); +} + +} // anonymous namespace + +namespace mozilla::dom { + +CancelChannelRunnable::CancelChannelRunnable( + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + nsresult aStatus) + : Runnable("dom::CancelChannelRunnable"), + mChannel(aChannel), + mRegistration(aRegistration), + mStatus(aStatus) {} + +NS_IMETHODIMP +CancelChannelRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + mChannel->CancelInterception(mStatus); + mRegistration->MaybeScheduleUpdate(); + return NS_OK; +} + +FetchEvent::FetchEvent(EventTarget* aOwner) + : ExtendableEvent(aOwner), + mPreventDefaultLineNumber(0), + mPreventDefaultColumnNumber(0), + mWaitToRespond(false) {} + +FetchEvent::~FetchEvent() = default; + +void FetchEvent::PostInit( + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + const nsACString& aScriptSpec) { + mChannel = aChannel; + mRegistration = aRegistration; + mScriptSpec.Assign(aScriptSpec); +} + +void FetchEvent::PostInit(const nsACString& aScriptSpec, + RefPtr<FetchEventOp> aRespondWithHandler) { + MOZ_ASSERT(aRespondWithHandler); + + mScriptSpec.Assign(aScriptSpec); + mRespondWithHandler = std::move(aRespondWithHandler); +} + +/*static*/ +already_AddRefed<FetchEvent> FetchEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const FetchEventInit& aOptions) { + RefPtr<EventTarget> owner = do_QueryObject(aGlobal.GetAsSupports()); + MOZ_ASSERT(owner); + RefPtr<FetchEvent> e = new FetchEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aOptions.mComposed); + e->mRequest = aOptions.mRequest; + e->mClientId = aOptions.mClientId; + e->mResultingClientId = aOptions.mResultingClientId; + RefPtr<nsIGlobalObject> global = do_QueryObject(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + ErrorResult rv; + e->mHandled = Promise::Create(global, rv); + if (rv.Failed()) { + rv.SuppressException(); + return nullptr; + } + e->mPreloadResponse = Promise::Create(global, rv); + if (rv.Failed()) { + rv.SuppressException(); + return nullptr; + } + return e.forget(); +} + +namespace { + +struct RespondWithClosure { + nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + const nsString mRequestURL; + const nsCString mRespondWithScriptSpec; + const uint32_t mRespondWithLineNumber; + const uint32_t mRespondWithColumnNumber; + + RespondWithClosure( + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + const nsAString& aRequestURL, const nsACString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, uint32_t aRespondWithColumnNumber) + : mInterceptedChannel(aChannel), + mRegistration(aRegistration), + mRequestURL(aRequestURL), + mRespondWithScriptSpec(aRespondWithScriptSpec), + mRespondWithLineNumber(aRespondWithLineNumber), + mRespondWithColumnNumber(aRespondWithColumnNumber) {} +}; + +class FinishResponse final : public Runnable { + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + + public: + explicit FinishResponse( + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel) + : Runnable("dom::FinishResponse"), mChannel(aChannel) {} + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = mChannel->FinishSynthesizedResponse(); + if (NS_WARN_IF(NS_FAILED(rv))) { + mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED); + return NS_OK; + } + + return rv; + } +}; + +class BodyCopyHandle final : public nsIInterceptedBodyCallback { + UniquePtr<RespondWithClosure> mClosure; + + ~BodyCopyHandle() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit BodyCopyHandle(UniquePtr<RespondWithClosure>&& aClosure) + : mClosure(std::move(aClosure)) {} + + NS_IMETHOD + BodyComplete(nsresult aRv) override { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIRunnable> event; + if (NS_WARN_IF(NS_FAILED(aRv))) { + ::AsyncLog( + mClosure->mInterceptedChannel, mClosure->mRespondWithScriptSpec, + mClosure->mRespondWithLineNumber, mClosure->mRespondWithColumnNumber, + "InterceptionFailedWithURL"_ns, mClosure->mRequestURL); + event = new CancelChannelRunnable(mClosure->mInterceptedChannel, + mClosure->mRegistration, + NS_ERROR_INTERCEPTION_FAILED); + } else { + event = new FinishResponse(mClosure->mInterceptedChannel); + } + + mClosure.reset(); + + event->Run(); + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(BodyCopyHandle, nsIInterceptedBodyCallback) + +class StartResponse final : public Runnable { + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + SafeRefPtr<InternalResponse> mInternalResponse; + ChannelInfo mWorkerChannelInfo; + const nsCString mScriptSpec; + const nsCString mResponseURLSpec; + UniquePtr<RespondWithClosure> mClosure; + + public: + StartResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + SafeRefPtr<InternalResponse> aInternalResponse, + const ChannelInfo& aWorkerChannelInfo, + const nsACString& aScriptSpec, + const nsACString& aResponseURLSpec, + UniquePtr<RespondWithClosure>&& aClosure) + : Runnable("dom::StartResponse"), + mChannel(aChannel), + mInternalResponse(std::move(aInternalResponse)), + mWorkerChannelInfo(aWorkerChannelInfo), + mScriptSpec(aScriptSpec), + mResponseURLSpec(aResponseURLSpec), + mClosure(std::move(aClosure)) {} + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIChannel> underlyingChannel; + nsresult rv = mChannel->GetChannel(getter_AddRefs(underlyingChannel)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(underlyingChannel, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->LoadInfo(); + + if (!CSPPermitsResponse(loadInfo)) { + mChannel->CancelInterception(NS_ERROR_CONTENT_BLOCKED); + return NS_OK; + } + + ChannelInfo channelInfo; + if (mInternalResponse->GetChannelInfo().IsInitialized()) { + channelInfo = mInternalResponse->GetChannelInfo(); + } else { + // We are dealing with a synthesized response here, so fall back to the + // channel info for the worker script. + channelInfo = mWorkerChannelInfo; + } + rv = mChannel->SetChannelInfo(&channelInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED); + return NS_OK; + } + + rv = mChannel->SynthesizeStatus( + mInternalResponse->GetUnfilteredStatus(), + mInternalResponse->GetUnfilteredStatusText()); + if (NS_WARN_IF(NS_FAILED(rv))) { + mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED); + return NS_OK; + } + + AutoTArray<InternalHeaders::Entry, 5> entries; + mInternalResponse->UnfilteredHeaders()->GetEntries(entries); + for (uint32_t i = 0; i < entries.Length(); ++i) { + mChannel->SynthesizeHeader(entries[i].mName, entries[i].mValue); + } + + auto castLoadInfo = static_cast<mozilla::net::LoadInfo*>(loadInfo.get()); + castLoadInfo->SynthesizeServiceWorkerTainting( + mInternalResponse->GetTainting()); + + // Get the preferred alternative data type of outter channel + nsAutoCString preferredAltDataType(""_ns); + nsCOMPtr<nsICacheInfoChannel> outerChannel = + do_QueryInterface(underlyingChannel); + if (outerChannel && + !outerChannel->PreferredAlternativeDataTypes().IsEmpty()) { + // TODO: handle multiple types properly. + preferredAltDataType.Assign( + outerChannel->PreferredAlternativeDataTypes()[0].type()); + } + + // Get the alternative data type saved in the InternalResponse + nsAutoCString altDataType; + nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel = + mInternalResponse->TakeCacheInfoChannel().get(); + if (cacheInfoChannel) { + cacheInfoChannel->GetAlternativeDataType(altDataType); + } + + nsCOMPtr<nsIInputStream> body; + if (preferredAltDataType.Equals(altDataType)) { + body = mInternalResponse->TakeAlternativeBody(); + } + if (!body) { + mInternalResponse->GetUnfilteredBody(getter_AddRefs(body)); + } else { + Telemetry::ScalarAdd(Telemetry::ScalarID::SW_ALTERNATIVE_BODY_USED_COUNT, + 1); + } + + RefPtr<BodyCopyHandle> copyHandle; + copyHandle = new BodyCopyHandle(std::move(mClosure)); + + rv = mChannel->StartSynthesizedResponse(body, copyHandle, cacheInfoChannel, + mResponseURLSpec, + mInternalResponse->IsRedirected()); + if (NS_WARN_IF(NS_FAILED(rv))) { + mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED); + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers( + underlyingChannel, "service-worker-synthesized-response", nullptr); + } + + return rv; + } + + bool CSPPermitsResponse(nsILoadInfo* aLoadInfo) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aLoadInfo); + nsresult rv; + nsCOMPtr<nsIURI> uri; + nsCString url = mInternalResponse->GetUnfilteredURL(); + if (url.IsEmpty()) { + // Synthetic response. The buck stops at the worker script. + url = mScriptSpec; + } + rv = NS_NewURI(getter_AddRefs(uri), url); + NS_ENSURE_SUCCESS(rv, false); + int16_t decision = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(uri, aLoadInfo, ""_ns, &decision); + NS_ENSURE_SUCCESS(rv, false); + return decision == nsIContentPolicy::ACCEPT; + } +}; + +class RespondWithHandler final : public PromiseNativeHandler { + nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + const RequestMode mRequestMode; + const RequestRedirect mRequestRedirectMode; +#ifdef DEBUG + const bool mIsClientRequest; +#endif + const nsCString mScriptSpec; + const nsString mRequestURL; + const nsCString mRequestFragment; + const nsCString mRespondWithScriptSpec; + const uint32_t mRespondWithLineNumber; + const uint32_t mRespondWithColumnNumber; + bool mRequestWasHandled; + + public: + NS_DECL_ISUPPORTS + + RespondWithHandler( + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + RequestMode aRequestMode, bool aIsClientRequest, + RequestRedirect aRedirectMode, const nsACString& aScriptSpec, + const nsAString& aRequestURL, const nsACString& aRequestFragment, + const nsACString& aRespondWithScriptSpec, uint32_t aRespondWithLineNumber, + uint32_t aRespondWithColumnNumber) + : mInterceptedChannel(aChannel), + mRegistration(aRegistration), + mRequestMode(aRequestMode), + mRequestRedirectMode(aRedirectMode) +#ifdef DEBUG + , + mIsClientRequest(aIsClientRequest) +#endif + , + mScriptSpec(aScriptSpec), + mRequestURL(aRequestURL), + mRequestFragment(aRequestFragment), + mRespondWithScriptSpec(aRespondWithScriptSpec), + mRespondWithLineNumber(aRespondWithLineNumber), + mRespondWithColumnNumber(aRespondWithColumnNumber), + mRequestWasHandled(false) { + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + void CancelRequest(nsresult aStatus); + + void AsyncLog(const nsACString& aMessageName, + const nsTArray<nsString>& aParams) { + ::AsyncLog(mInterceptedChannel, mRespondWithScriptSpec, + mRespondWithLineNumber, mRespondWithColumnNumber, aMessageName, + aParams); + } + + void AsyncLog(const nsACString& aSourceSpec, uint32_t aLine, uint32_t aColumn, + const nsACString& aMessageName, + const nsTArray<nsString>& aParams) { + ::AsyncLog(mInterceptedChannel, aSourceSpec, aLine, aColumn, aMessageName, + aParams); + } + + private: + ~RespondWithHandler() { + if (!mRequestWasHandled) { + ::AsyncLog(mInterceptedChannel, mRespondWithScriptSpec, + mRespondWithLineNumber, mRespondWithColumnNumber, + "InterceptionFailedWithURL"_ns, mRequestURL); + CancelRequest(NS_ERROR_INTERCEPTION_FAILED); + } + } +}; + +class MOZ_STACK_CLASS AutoCancel { + RefPtr<RespondWithHandler> mOwner; + nsCString mSourceSpec; + uint32_t mLine; + uint32_t mColumn; + nsCString mMessageName; + nsTArray<nsString> mParams; + + public: + AutoCancel(RespondWithHandler* aOwner, const nsString& aRequestURL) + : mOwner(aOwner), + mLine(0), + mColumn(0), + mMessageName("InterceptionFailedWithURL"_ns) { + mParams.AppendElement(aRequestURL); + } + + ~AutoCancel() { + if (mOwner) { + if (mSourceSpec.IsEmpty()) { + mOwner->AsyncLog(mMessageName, mParams); + } else { + mOwner->AsyncLog(mSourceSpec, mLine, mColumn, mMessageName, mParams); + } + mOwner->CancelRequest(NS_ERROR_INTERCEPTION_FAILED); + } + } + + // 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; } +}; + +NS_IMPL_ISUPPORTS0(RespondWithHandler) + +void RespondWithHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AutoCancel autoCancel(this, mRequestURL); + + 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, + mRequestURL, 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, + mRequestURL, 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, + mRequestURL); + return; + } + + MOZ_ASSERT_IF(mIsClientRequest, mRequestMode == RequestMode::Same_origin || + mRequestMode == RequestMode::Navigate); + + if (response->Type() == ResponseType::Opaque && + mRequestMode != RequestMode::No_cors) { + NS_ConvertASCIItoUTF16 modeString( + RequestModeValues::GetString(mRequestMode)); + + autoCancel.SetCancelMessage("BadOpaqueInterceptionRequestModeWithURL"_ns, + mRequestURL, modeString); + return; + } + + if (mRequestRedirectMode != RequestRedirect::Manual && + response->Type() == ResponseType::Opaqueredirect) { + autoCancel.SetCancelMessage("BadOpaqueRedirectInterceptionWithURL"_ns, + mRequestURL); + return; + } + + if (mRequestRedirectMode != RequestRedirect::Follow && + response->Redirected()) { + autoCancel.SetCancelMessage("BadRedirectModeInterceptionWithURL"_ns, + mRequestURL); + return; + } + + { + ErrorResult error; + bool bodyUsed = response->GetBodyUsed(error); + error.WouldReportJSException(); + if (NS_WARN_IF(error.Failed())) { + autoCancel.SetCancelErrorResult(aCx, error); + return; + } + if (NS_WARN_IF(bodyUsed)) { + autoCancel.SetCancelMessage("InterceptedUsedResponseWithURL"_ns, + mRequestURL); + 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 (mRequestMode == 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, + mRequestURL, responseURL); + return; + } + + // Propagate the URL to the content if the request mode is not "navigate". + // Note that, we only reflect the final URL if the response.redirected is + // false. We propagate all the URLs if the response.redirected is true. + nsCString responseURL; + if (mRequestMode != RequestMode::Navigate) { + responseURL = ir->GetUnfilteredURL(); + + // Similar to how we apply the request fragment to redirects automatically + // we also want to apply it automatically when propagating the response + // URL from a service worker interception. Currently response.url strips + // the fragment, so this will never conflict with an existing fragment + // on the response. In the future we will have to check for a response + // fragment and avoid overriding in that case. + if (!mRequestFragment.IsEmpty() && !responseURL.IsEmpty()) { + MOZ_ASSERT(!responseURL.Contains('#')); + responseURL.Append("#"_ns); + responseURL.Append(mRequestFragment); + } + } + + UniquePtr<RespondWithClosure> closure(new RespondWithClosure( + mInterceptedChannel, mRegistration, mRequestURL, mRespondWithScriptSpec, + mRespondWithLineNumber, mRespondWithColumnNumber)); + + nsCOMPtr<nsIRunnable> startRunnable = new StartResponse( + mInterceptedChannel, ir.clonePtr(), worker->GetChannelInfo(), mScriptSpec, + responseURL, std::move(closure)); + + 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; + } + } + + MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(startRunnable.forget())); + + MOZ_ASSERT(!closure); + autoCancel.Reset(); + mRequestWasHandled = true; +} + +void RespondWithHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + nsCString sourceSpec = mRespondWithScriptSpec; + uint32_t line = mRespondWithLineNumber; + uint32_t column = mRespondWithColumnNumber; + nsString valueString; + + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); + + ::AsyncLog(mInterceptedChannel, sourceSpec, line, column, + "InterceptionRejectedResponseWithURL"_ns, mRequestURL, + valueString); + + CancelRequest(NS_ERROR_INTERCEPTION_FAILED); +} + +void RespondWithHandler::CancelRequest(nsresult aStatus) { + nsCOMPtr<nsIRunnable> runnable = + new CancelChannelRunnable(mInterceptedChannel, mRegistration, aStatus); + // Note, this may run off the worker thread during worker termination. + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + if (worker) { + MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(runnable.forget())); + } else { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); + } + mRequestWasHandled = true; +} + +} // namespace + +void FetchEvent::RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv) { + if (!GetDispatchFlag() || mWaitToRespond) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Record where respondWith() was called in the script so we can include the + // information in any error reporting. We should be guaranteed not to get + // a file:// string here because service workers require http/https. + nsCString spec; + uint32_t line = 0; + uint32_t column = 0; + nsJSUtils::GetCallingLocation(aCx, spec, &line, &column); + + SafeRefPtr<InternalRequest> ir = mRequest->GetInternalRequest(); + + nsAutoCString requestURL; + ir->GetURL(requestURL); + + StopImmediatePropagation(); + mWaitToRespond = true; + + if (mChannel) { + RefPtr<RespondWithHandler> handler = new RespondWithHandler( + mChannel, mRegistration, mRequest->Mode(), ir->IsClientRequest(), + mRequest->Redirect(), mScriptSpec, NS_ConvertUTF8toUTF16(requestURL), + ir->GetFragment(), spec, line, column); + + aArg.AppendNativeHandler(handler); + // mRespondWithHandler can be nullptr for self-dispatched FetchEvent. + } else if (mRespondWithHandler) { + mRespondWithHandler->RespondWithCalledAt(spec, line, column); + aArg.AppendNativeHandler(mRespondWithHandler); + mRespondWithHandler = nullptr; + } + + if (!WaitOnPromise(aArg)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + } +} + +void FetchEvent::PreventDefault(JSContext* aCx, CallerType aCallerType) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aCallerType != CallerType::System, + "Since when do we support system-principal service workers?"); + + if (mPreventDefaultScriptSpec.IsEmpty()) { + // Note when the FetchEvent might have been canceled by script, but don't + // actually log the location until we are sure it matters. This is + // determined in ServiceWorkerPrivate.cpp. We only remember the first + // call to preventDefault() as its the most likely to have actually canceled + // the event. + nsJSUtils::GetCallingLocation(aCx, mPreventDefaultScriptSpec, + &mPreventDefaultLineNumber, + &mPreventDefaultColumnNumber); + } + + Event::PreventDefault(aCx, aCallerType); +} + +void FetchEvent::ReportCanceled() { + MOZ_ASSERT(!mPreventDefaultScriptSpec.IsEmpty()); + + SafeRefPtr<InternalRequest> ir = mRequest->GetInternalRequest(); + nsAutoCString url; + ir->GetURL(url); + + // The variadic template provided by StringArrayAppender requires exactly + // an nsString. + NS_ConvertUTF8toUTF16 requestURL(url); + // nsString requestURL; + // CopyUTF8toUTF16(url, requestURL); + + if (mChannel) { + ::AsyncLog(mChannel.get(), mPreventDefaultScriptSpec, + mPreventDefaultLineNumber, mPreventDefaultColumnNumber, + "InterceptionCanceledWithURL"_ns, requestURL); + // mRespondWithHandler could be nullptr for self-dispatched FetchEvent. + } else if (mRespondWithHandler) { + mRespondWithHandler->ReportCanceled(mPreventDefaultScriptSpec, + mPreventDefaultLineNumber, + mPreventDefaultColumnNumber); + mRespondWithHandler = nullptr; + } +} + +namespace { + +class WaitUntilHandler final : public PromiseNativeHandler { + WorkerPrivate* mWorkerPrivate; + const nsCString mScope; + nsString mSourceSpec; + uint32_t mLine; + uint32_t mColumn; + nsString mRejectValue; + + ~WaitUntilHandler() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + WaitUntilHandler(WorkerPrivate* aWorkerPrivate, JSContext* aCx) + : mWorkerPrivate(aWorkerPrivate), + mScope(mWorkerPrivate->ServiceWorkerScope()), + mLine(0), + mColumn(0) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + // Save the location of the waitUntil() call itself as a fallback + // in case the rejection value does not contain any location info. + nsJSUtils::GetCallingLocation(aCx, mSourceSpec, &mLine, &mColumn); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValu, + ErrorResult& aRve) override { + // do nothing, we are only here to report errors + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + mWorkerPrivate->AssertIsOnWorkerThread(); + + nsString spec; + uint32_t line = 0; + uint32_t column = 0; + nsContentUtils::ExtractErrorValues(aCx, aValue, spec, &line, &column, + mRejectValue); + + // only use the extracted location if we found one + if (!spec.IsEmpty()) { + mSourceSpec = spec; + mLine = line; + mColumn = column; + } + + MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread( + NewRunnableMethod("WaitUntilHandler::ReportOnMainThread", this, + &WaitUntilHandler::ReportOnMainThread))); + } + + void ReportOnMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + + // TODO: Make the error message a localized string. (bug 1222720) + nsString message; + message.AppendLiteral( + "Service worker event waitUntil() was passed a " + "promise that rejected with '"); + message.Append(mRejectValue); + message.AppendLiteral("'."); + + // Note, there is a corner case where this won't report to the window + // that triggered the error. Consider a navigation fetch event that + // rejects waitUntil() without holding respondWith() open. In this case + // there is no controlling document yet, the window did call .register() + // because there is no documeny yet, and the navigation is no longer + // being intercepted. + + swm->ReportToAllClients(mScope, message, mSourceSpec, u""_ns, mLine, + mColumn, nsIScriptError::errorFlag); + } +}; + +NS_IMPL_ISUPPORTS0(WaitUntilHandler) + +} // anonymous namespace + +ExtendableEvent::ExtensionsHandler::~ExtensionsHandler() { + MOZ_ASSERT(!mExtendableEvent); +} + +bool ExtendableEvent::ExtensionsHandler::GetDispatchFlag() const { + // mExtendableEvent should set itself as nullptr in its destructor, and we + // can't be dispatching an event that doesn't exist, so this should work for + // as long as it's not needed to determine whether the event is still alive, + // which seems unlikely. + if (!mExtendableEvent) { + return false; + } + + return mExtendableEvent->GetDispatchFlag(); +} + +void ExtendableEvent::ExtensionsHandler::SetExtendableEvent( + const ExtendableEvent* const aExtendableEvent) { + mExtendableEvent = aExtendableEvent; +} + +NS_IMPL_ADDREF_INHERITED(FetchEvent, ExtendableEvent) +NS_IMPL_RELEASE_INHERITED(FetchEvent, ExtendableEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchEvent) +NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FetchEvent, ExtendableEvent, mRequest, + mHandled, mPreloadResponse) + +ExtendableEvent::ExtendableEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) {} + +bool ExtendableEvent::WaitOnPromise(Promise& aPromise) { + if (!mExtensionsHandler) { + return false; + } + return mExtensionsHandler->WaitOnPromise(aPromise); +} + +void ExtendableEvent::SetKeepAliveHandler( + ExtensionsHandler* aExtensionsHandler) { + MOZ_ASSERT(!mExtensionsHandler); + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + mExtensionsHandler = aExtensionsHandler; + mExtensionsHandler->SetExtendableEvent(this); +} + +void ExtendableEvent::WaitUntil(JSContext* aCx, Promise& aPromise, + ErrorResult& aRv) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (!WaitOnPromise(aPromise)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Append our handler to each waitUntil promise separately so we + // can record the location in script where waitUntil was called. + RefPtr<WaitUntilHandler> handler = + new WaitUntilHandler(GetCurrentThreadWorkerPrivate(), aCx); + aPromise.AppendNativeHandler(handler); +} + +NS_IMPL_ADDREF_INHERITED(ExtendableEvent, Event) +NS_IMPL_RELEASE_INHERITED(ExtendableEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtendableEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +namespace { +nsresult ExtractBytesFromUSVString(const nsAString& aStr, + nsTArray<uint8_t>& aBytes) { + MOZ_ASSERT(aBytes.IsEmpty()); + auto encoder = UTF_8_ENCODING->NewEncoder(); + CheckedInt<size_t> needed = + encoder->MaxBufferLengthFromUTF16WithoutReplacement(aStr.Length()); + if (NS_WARN_IF(!needed.isValid() || + !aBytes.SetLength(needed.value(), fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + uint32_t result; + size_t read; + size_t written; + // Do not use structured binding lest deal with [-Werror=unused-variable] + std::tie(result, read, written) = + encoder->EncodeFromUTF16WithoutReplacement(aStr, aBytes, true); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == aStr.Length()); + aBytes.TruncateLength(written); + return NS_OK; +} + +nsresult ExtractBytesFromData( + const OwningArrayBufferViewOrArrayBufferOrUSVString& aDataInit, + nsTArray<uint8_t>& aBytes) { + if (aDataInit.IsArrayBufferView()) { + const ArrayBufferView& view = aDataInit.GetAsArrayBufferView(); + if (NS_WARN_IF(!PushUtil::CopyArrayBufferViewToArray(view, aBytes))) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; + } + if (aDataInit.IsArrayBuffer()) { + const ArrayBuffer& buffer = aDataInit.GetAsArrayBuffer(); + if (NS_WARN_IF(!PushUtil::CopyArrayBufferToArray(buffer, aBytes))) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; + } + if (aDataInit.IsUSVString()) { + return ExtractBytesFromUSVString(aDataInit.GetAsUSVString(), aBytes); + } + MOZ_ASSERT_UNREACHABLE("Unexpected push message data"); + return NS_ERROR_FAILURE; +} +} // namespace + +PushMessageData::PushMessageData(nsIGlobalObject* aOwner, + nsTArray<uint8_t>&& aBytes) + : mOwner(aOwner), mBytes(std::move(aBytes)) {} + +PushMessageData::~PushMessageData() = default; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushMessageData, mOwner) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PushMessageData) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PushMessageData) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushMessageData) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* PushMessageData::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::PushMessageData_Binding::Wrap(aCx, this, aGivenProto); +} + +void PushMessageData::Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) { + if (NS_FAILED(EnsureDecodedText())) { + aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + BodyUtil::ConsumeJson(cx, aRetval, mDecodedText, aRv); +} + +void PushMessageData::Text(nsAString& aData) { + if (NS_SUCCEEDED(EnsureDecodedText())) { + aData = mDecodedText; + } +} + +void PushMessageData::ArrayBuffer(JSContext* cx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + uint8_t* data = GetContentsCopy(); + if (data) { + BodyUtil::ConsumeArrayBuffer(cx, aRetval, mBytes.Length(), data, aRv); + } +} + +already_AddRefed<mozilla::dom::Blob> PushMessageData::Blob(ErrorResult& aRv) { + uint8_t* data = GetContentsCopy(); + if (data) { + RefPtr<mozilla::dom::Blob> blob = + BodyUtil::ConsumeBlob(mOwner, u""_ns, mBytes.Length(), data, aRv); + if (blob) { + return blob.forget(); + } + } + return nullptr; +} + +nsresult PushMessageData::EnsureDecodedText() { + if (mBytes.IsEmpty() || !mDecodedText.IsEmpty()) { + return NS_OK; + } + nsresult rv = BodyUtil::ConsumeText( + mBytes.Length(), reinterpret_cast<uint8_t*>(mBytes.Elements()), + mDecodedText); + if (NS_WARN_IF(NS_FAILED(rv))) { + mDecodedText.Truncate(); + return rv; + } + return NS_OK; +} + +uint8_t* PushMessageData::GetContentsCopy() { + uint32_t length = mBytes.Length(); + void* data = malloc(length); + if (!data) { + return nullptr; + } + memcpy(data, mBytes.Elements(), length); + return reinterpret_cast<uint8_t*>(data); +} + +PushEvent::PushEvent(EventTarget* aOwner) : ExtendableEvent(aOwner) {} + +already_AddRefed<PushEvent> PushEvent::Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const PushEventInit& aOptions, ErrorResult& aRv) { + RefPtr<PushEvent> e = new PushEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aOptions.mComposed); + if (aOptions.mData.WasPassed()) { + nsTArray<uint8_t> bytes; + nsresult rv = ExtractBytesFromData(aOptions.mData.Value(), bytes); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return nullptr; + } + e->mData = new PushMessageData(aOwner->GetOwnerGlobal(), std::move(bytes)); + } + return e.forget(); +} + +NS_IMPL_ADDREF_INHERITED(PushEvent, ExtendableEvent) +NS_IMPL_RELEASE_INHERITED(PushEvent, ExtendableEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushEvent) +NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PushEvent, ExtendableEvent, mData) + +JSObject* PushEvent::WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::PushEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +ExtendableMessageEvent::ExtendableMessageEvent(EventTarget* aOwner) + : ExtendableEvent(aOwner), mData(JS::UndefinedValue()) { + mozilla::HoldJSObjects(this); +} + +ExtendableMessageEvent::~ExtendableMessageEvent() { DropJSObjects(this); } + +void ExtendableMessageEvent::GetData(JSContext* aCx, + JS::MutableHandle<JS::Value> aData, + ErrorResult& aRv) { + aData.set(mData); + if (!JS_WrapValue(aCx, aData)) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +void ExtendableMessageEvent::GetSource( + Nullable<OwningClientOrServiceWorkerOrMessagePort>& aValue) const { + if (mClient) { + aValue.SetValue().SetAsClient() = mClient; + } else if (mServiceWorker) { + aValue.SetValue().SetAsServiceWorker() = mServiceWorker; + } else if (mMessagePort) { + aValue.SetValue().SetAsMessagePort() = mMessagePort; + } else { + // nullptr source is possible for manually constructed event + aValue.SetNull(); + } +} + +/* static */ +already_AddRefed<ExtendableMessageEvent> ExtendableMessageEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const ExtendableMessageEventInit& aOptions) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(t, aType, aOptions); +} + +/* static */ +already_AddRefed<ExtendableMessageEvent> ExtendableMessageEvent::Constructor( + mozilla::dom::EventTarget* aEventTarget, const nsAString& aType, + const ExtendableMessageEventInit& aOptions) { + RefPtr<ExtendableMessageEvent> event = + new ExtendableMessageEvent(aEventTarget); + + event->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + bool trusted = event->Init(aEventTarget); + event->SetTrusted(trusted); + + event->mData = aOptions.mData; + event->mOrigin = aOptions.mOrigin; + event->mLastEventId = aOptions.mLastEventId; + + if (!aOptions.mSource.IsNull()) { + if (aOptions.mSource.Value().IsClient()) { + event->mClient = aOptions.mSource.Value().GetAsClient(); + } else if (aOptions.mSource.Value().IsServiceWorker()) { + event->mServiceWorker = aOptions.mSource.Value().GetAsServiceWorker(); + } else if (aOptions.mSource.Value().IsMessagePort()) { + event->mMessagePort = aOptions.mSource.Value().GetAsMessagePort(); + } + } + + event->mPorts.AppendElements(aOptions.mPorts); + return event.forget(); +} + +void ExtendableMessageEvent::GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts) { + aPorts = mPorts.Clone(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(ExtendableMessageEvent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ExtendableMessageEvent, Event) + tmp->mData.setUndefined(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mClient) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServiceWorker) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPorts) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ExtendableMessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClient) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorker) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPorts) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ExtendableMessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtendableMessageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_ADDREF_INHERITED(ExtendableMessageEvent, Event) +NS_IMPL_RELEASE_INHERITED(ExtendableMessageEvent, Event) + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerEvents.h b/dom/serviceworkers/ServiceWorkerEvents.h new file mode 100644 index 0000000000..2003c8afe9 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerEvents.h @@ -0,0 +1,309 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerevents_h__ +#define mozilla_dom_serviceworkerevents_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ExtendableEventBinding.h" +#include "mozilla/dom/ExtendableMessageEventBinding.h" +#include "mozilla/dom/FetchEventBinding.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/WorkerCommon.h" + +#include "nsProxyRelease.h" +#include "nsContentUtils.h" + +class nsIInterceptedChannel; + +namespace mozilla::dom { + +class Blob; +class Client; +class FetchEventOp; +class MessagePort; +struct PushEventInit; +class Request; +class ResponseOrPromise; +class ServiceWorker; +class ServiceWorkerRegistrationInfo; + +// Defined in ServiceWorker.cpp +bool ServiceWorkerVisible(JSContext* aCx, JSObject* aObj); + +class CancelChannelRunnable final : public Runnable { + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + const nsresult mStatus; + + public: + CancelChannelRunnable( + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + nsresult aStatus); + + NS_IMETHOD Run() override; +}; + +enum ExtendableEventResult { Rejected = 0, Resolved }; + +class ExtendableEventCallback { + public: + virtual void FinishedWithResult(ExtendableEventResult aResult) = 0; + + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING +}; + +class ExtendableEvent : public Event { + public: + class ExtensionsHandler { + friend class ExtendableEvent; + + public: + virtual bool WaitOnPromise(Promise& aPromise) = 0; + + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + protected: + virtual ~ExtensionsHandler(); + + // Also returns false if the owning ExtendableEvent is destroyed. + bool GetDispatchFlag() const; + + private: + // Only the owning ExtendableEvent is allowed to set this data. + void SetExtendableEvent(const ExtendableEvent* const aExtendableEvent); + + MOZ_NON_OWNING_REF const ExtendableEvent* mExtendableEvent = nullptr; + }; + + private: + RefPtr<ExtensionsHandler> mExtensionsHandler; + + protected: + bool GetDispatchFlag() const { return mEvent->mFlags.mIsBeingDispatched; } + + bool WaitOnPromise(Promise& aPromise); + + explicit ExtendableEvent(mozilla::dom::EventTarget* aOwner); + + ~ExtendableEvent() { + if (mExtensionsHandler) { + mExtensionsHandler->SetExtendableEvent(nullptr); + } + }; + + public: + NS_DECL_ISUPPORTS_INHERITED + + void SetKeepAliveHandler(ExtensionsHandler* aExtensionsHandler); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return mozilla::dom::ExtendableEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + static already_AddRefed<ExtendableEvent> Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const EventInit& aOptions) { + RefPtr<ExtendableEvent> e = new ExtendableEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aOptions.mComposed); + return e.forget(); + } + + static already_AddRefed<ExtendableEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const EventInit& aOptions) { + nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(target, aType, aOptions); + } + + void WaitUntil(JSContext* aCx, Promise& aPromise, ErrorResult& aRv); + + virtual ExtendableEvent* AsExtendableEvent() override { return this; } +}; + +class FetchEvent final : public ExtendableEvent { + RefPtr<FetchEventOp> mRespondWithHandler; + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + RefPtr<Request> mRequest; + RefPtr<Promise> mHandled; + RefPtr<Promise> mPreloadResponse; + nsCString mScriptSpec; + nsCString mPreventDefaultScriptSpec; + nsString mClientId; + nsString mResultingClientId; + uint32_t mPreventDefaultLineNumber; + uint32_t mPreventDefaultColumnNumber; + bool mWaitToRespond; + + protected: + explicit FetchEvent(EventTarget* aOwner); + ~FetchEvent(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FetchEvent, ExtendableEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return FetchEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void PostInit( + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + const nsACString& aScriptSpec); + + void PostInit(const nsACString& aScriptSpec, + RefPtr<FetchEventOp> aRespondWithHandler); + + static already_AddRefed<FetchEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const FetchEventInit& aOptions); + + bool WaitToRespond() const { return mWaitToRespond; } + + Request* Request_() const { + MOZ_ASSERT(mRequest); + return mRequest; + } + + void GetClientId(nsAString& aClientId) const { aClientId = mClientId; } + + void GetResultingClientId(nsAString& aResultingClientId) const { + aResultingClientId = mResultingClientId; + } + + Promise* Handled() const { return mHandled; } + + Promise* PreloadResponse() const { return mPreloadResponse; } + + void RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv); + + // Pull in the Event version of PreventDefault so we don't get + // shadowing warnings. + using Event::PreventDefault; + void PreventDefault(JSContext* aCx, CallerType aCallerType) override; + + void ReportCanceled(); +}; + +class PushMessageData final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PushMessageData) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsIGlobalObject* GetParentObject() const { return mOwner; } + + void Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv); + void Text(nsAString& aData); + void ArrayBuffer(JSContext* cx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + already_AddRefed<mozilla::dom::Blob> Blob(ErrorResult& aRv); + + PushMessageData(nsIGlobalObject* aOwner, nsTArray<uint8_t>&& aBytes); + + private: + nsCOMPtr<nsIGlobalObject> mOwner; + nsTArray<uint8_t> mBytes; + nsString mDecodedText; + ~PushMessageData(); + + nsresult EnsureDecodedText(); + uint8_t* GetContentsCopy(); +}; + +class PushEvent final : public ExtendableEvent { + RefPtr<PushMessageData> mData; + + protected: + explicit PushEvent(mozilla::dom::EventTarget* aOwner); + ~PushEvent() = default; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PushEvent, ExtendableEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<PushEvent> Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const PushEventInit& aOptions, ErrorResult& aRv); + + static already_AddRefed<PushEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const PushEventInit& aOptions, + ErrorResult& aRv) { + nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(owner, aType, aOptions, aRv); + } + + PushMessageData* GetData() const { return mData; } +}; + +class ExtendableMessageEvent final : public ExtendableEvent { + JS::Heap<JS::Value> mData; + nsString mOrigin; + nsString mLastEventId; + RefPtr<Client> mClient; + RefPtr<ServiceWorker> mServiceWorker; + RefPtr<MessagePort> mMessagePort; + nsTArray<RefPtr<MessagePort>> mPorts; + + protected: + explicit ExtendableMessageEvent(EventTarget* aOwner); + ~ExtendableMessageEvent(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ExtendableMessageEvent, + ExtendableEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return mozilla::dom::ExtendableMessageEvent_Binding::Wrap(aCx, this, + aGivenProto); + } + + static already_AddRefed<ExtendableMessageEvent> Constructor( + mozilla::dom::EventTarget* aOwner, const nsAString& aType, + const ExtendableMessageEventInit& aOptions); + + static already_AddRefed<ExtendableMessageEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const ExtendableMessageEventInit& aOptions); + + void GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aData, + ErrorResult& aRv); + + void GetSource( + Nullable<OwningClientOrServiceWorkerOrMessagePort>& aValue) const; + + void GetOrigin(nsAString& aOrigin) const { aOrigin = mOrigin; } + + void GetLastEventId(nsAString& aLastEventId) const { + aLastEventId = mLastEventId; + } + + void GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_serviceworkerevents_h__ */ diff --git a/dom/serviceworkers/ServiceWorkerIPCUtils.h b/dom/serviceworkers/ServiceWorkerIPCUtils.h new file mode 100644 index 0000000000..bd867f877a --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerIPCUtils.h @@ -0,0 +1,35 @@ +/* -*- 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/. */ +#ifndef _mozilla_dom_ServiceWorkerIPCUtils_h +#define _mozilla_dom_ServiceWorkerIPCUtils_h + +#include "ipc/EnumSerializer.h" + +// Undo X11/X.h's definition of None +#undef None + +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/dom/ServiceWorkerRegistrationBinding.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::ServiceWorkerState> + : public ContiguousEnumSerializer< + mozilla::dom::ServiceWorkerState, + mozilla::dom::ServiceWorkerState::Parsed, + mozilla::dom::ServiceWorkerState::EndGuard_> {}; + +template <> +struct ParamTraits<mozilla::dom::ServiceWorkerUpdateViaCache> + : public ContiguousEnumSerializer< + mozilla::dom::ServiceWorkerUpdateViaCache, + mozilla::dom::ServiceWorkerUpdateViaCache::Imports, + mozilla::dom::ServiceWorkerUpdateViaCache::EndGuard_> {}; + +} // namespace IPC + +#endif // _mozilla_dom_ServiceWorkerIPCUtils_h diff --git a/dom/serviceworkers/ServiceWorkerInfo.cpp b/dom/serviceworkers/ServiceWorkerInfo.cpp new file mode 100644 index 0000000000..9998cfed6b --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerInfo.cpp @@ -0,0 +1,286 @@ +/* -*- 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 "ServiceWorkerInfo.h" + +#include "ServiceWorkerUtils.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerScriptCache.h" +#include "mozilla/dom/ClientIPCTypes.h" +#include "mozilla/dom/ClientState.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/WorkerPrivate.h" + +namespace mozilla::dom { + +using mozilla::ipc::PrincipalInfo; + +static_assert(nsIServiceWorkerInfo::STATE_PARSED == + static_cast<uint16_t>(ServiceWorkerState::Parsed), + "ServiceWorkerState enumeration value should match state values " + "from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_INSTALLING == + static_cast<uint16_t>(ServiceWorkerState::Installing), + "ServiceWorkerState enumeration value should match state values " + "from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_INSTALLED == + static_cast<uint16_t>(ServiceWorkerState::Installed), + "ServiceWorkerState enumeration value should match state values " + "from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_ACTIVATING == + static_cast<uint16_t>(ServiceWorkerState::Activating), + "ServiceWorkerState enumeration value should match state values " + "from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_ACTIVATED == + static_cast<uint16_t>(ServiceWorkerState::Activated), + "ServiceWorkerState enumeration value should match state values " + "from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_REDUNDANT == + static_cast<uint16_t>(ServiceWorkerState::Redundant), + "ServiceWorkerState enumeration value should match state values " + "from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_UNKNOWN == + ServiceWorkerStateValues::Count, + "ServiceWorkerState enumeration value should match state values " + "from nsIServiceWorkerInfo."); + +NS_IMPL_ISUPPORTS(ServiceWorkerInfo, nsIServiceWorkerInfo) + +NS_IMETHODIMP +ServiceWorkerInfo::GetId(nsAString& aId) { + MOZ_ASSERT(NS_IsMainThread()); + aId = mWorkerPrivateId; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetScriptSpec(nsAString& aScriptSpec) { + MOZ_ASSERT(NS_IsMainThread()); + CopyUTF8toUTF16(mDescriptor.ScriptURL(), aScriptSpec); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetCacheName(nsAString& aCacheName) { + MOZ_ASSERT(NS_IsMainThread()); + aCacheName = mCacheName; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetState(uint16_t* aState) { + MOZ_ASSERT(aState); + MOZ_ASSERT(NS_IsMainThread()); + *aState = static_cast<uint16_t>(State()); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetDebugger(nsIWorkerDebugger** aResult) { + if (NS_WARN_IF(!aResult)) { + return NS_ERROR_FAILURE; + } + + return mServiceWorkerPrivate->GetDebugger(aResult); +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetHandlesFetchEvents(bool* aValue) { + MOZ_ASSERT(aValue); + MOZ_ASSERT(NS_IsMainThread()); + + if (mHandlesFetch == Unknown) { + return NS_ERROR_FAILURE; + } + + *aValue = HandlesFetch(); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetInstalledTime(PRTime* _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(_retval); + *_retval = mInstalledTime; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetActivatedTime(PRTime* _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(_retval); + *_retval = mActivatedTime; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetRedundantTime(PRTime* _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(_retval); + *_retval = mRedundantTime; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetNavigationFaultCount(uint32_t* aNavigationFaultCount) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aNavigationFaultCount); + *aNavigationFaultCount = mNavigationFaultCount; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetTestingInjectCancellation( + nsresult* aTestingInjectCancellation) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTestingInjectCancellation); + *aTestingInjectCancellation = mTestingInjectCancellation; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::SetTestingInjectCancellation( + nsresult aTestingInjectCancellation) { + MOZ_ASSERT(NS_IsMainThread()); + mTestingInjectCancellation = aTestingInjectCancellation; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::AttachDebugger() { + return mServiceWorkerPrivate->AttachDebugger(); +} + +NS_IMETHODIMP +ServiceWorkerInfo::DetachDebugger() { + return mServiceWorkerPrivate->DetachDebugger(); +} + +void ServiceWorkerInfo::UpdateState(ServiceWorkerState aState) { + MOZ_ASSERT(NS_IsMainThread()); +#ifdef DEBUG + // Any state can directly transition to redundant, but everything else is + // ordered. + if (aState != ServiceWorkerState::Redundant) { + MOZ_ASSERT_IF(State() == ServiceWorkerState::EndGuard_, + aState == ServiceWorkerState::Installing); + MOZ_ASSERT_IF(State() == ServiceWorkerState::Installing, + aState == ServiceWorkerState::Installed); + MOZ_ASSERT_IF(State() == ServiceWorkerState::Installed, + aState == ServiceWorkerState::Activating); + MOZ_ASSERT_IF(State() == ServiceWorkerState::Activating, + aState == ServiceWorkerState::Activated); + } + // Activated can only go to redundant. + MOZ_ASSERT_IF(State() == ServiceWorkerState::Activated, + aState == ServiceWorkerState::Redundant); +#endif + // Flush any pending functional events to the worker when it transitions to + // the activated state. + // TODO: Do we care that these events will race with the propagation of the + // state change? + if (State() != aState) { + mServiceWorkerPrivate->UpdateState(aState); + } + mDescriptor.SetState(aState); + if (State() == ServiceWorkerState::Redundant) { + serviceWorkerScriptCache::PurgeCache(mPrincipal, mCacheName); + mServiceWorkerPrivate->NoteDeadServiceWorkerInfo(); + } +} + +ServiceWorkerInfo::ServiceWorkerInfo(nsIPrincipal* aPrincipal, + const nsACString& aScope, + uint64_t aRegistrationId, + uint64_t aRegistrationVersion, + const nsACString& aScriptSpec, + const nsAString& aCacheName, + nsLoadFlags aImportsLoadFlags) + : mPrincipal(aPrincipal), + mDescriptor(GetNextID(), aRegistrationId, aRegistrationVersion, + aPrincipal, aScope, aScriptSpec, ServiceWorkerState::Parsed), + mCacheName(aCacheName), + mWorkerPrivateId(ComputeWorkerPrivateId()), + mImportsLoadFlags(aImportsLoadFlags), + mCreationTime(PR_Now()), + mCreationTimeStamp(TimeStamp::Now()), + mInstalledTime(0), + mActivatedTime(0), + mRedundantTime(0), + mServiceWorkerPrivate(new ServiceWorkerPrivate(this)), + mSkipWaitingFlag(false), + mHandlesFetch(Unknown), + mNavigationFaultCount(0), + mTestingInjectCancellation(NS_OK) { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); + MOZ_ASSERT(mPrincipal); + // cache origin attributes so we can use them off main thread + mOriginAttributes = mPrincipal->OriginAttributesRef(); + MOZ_ASSERT(!mDescriptor.ScriptURL().IsEmpty()); + MOZ_ASSERT(!mCacheName.IsEmpty()); + MOZ_ASSERT(!mWorkerPrivateId.IsEmpty()); + + // Scripts of a service worker should always be loaded bypass service workers. + // Otherwise, we might not be able to update a service worker correctly, if + // there is a service worker generating the script. + MOZ_DIAGNOSTIC_ASSERT(mImportsLoadFlags & + nsIChannel::LOAD_BYPASS_SERVICE_WORKER); +} + +ServiceWorkerInfo::~ServiceWorkerInfo() { + MOZ_ASSERT(mServiceWorkerPrivate); + mServiceWorkerPrivate->NoteDeadServiceWorkerInfo(); +} + +static uint64_t gServiceWorkerInfoCurrentID = 0; + +uint64_t ServiceWorkerInfo::GetNextID() const { + return ++gServiceWorkerInfoCurrentID; +} + +void ServiceWorkerInfo::PostMessage(RefPtr<ServiceWorkerCloneData>&& aData, + const ClientInfo& aClientInfo, + const ClientState& aClientState) { + mServiceWorkerPrivate->SendMessageEvent( + std::move(aData), + ClientInfoAndState(aClientInfo.ToIPC(), aClientState.ToIPC())); +} + +void ServiceWorkerInfo::UpdateInstalledTime() { + MOZ_ASSERT(State() == ServiceWorkerState::Installed); + MOZ_ASSERT(mInstalledTime == 0); + + mInstalledTime = + mCreationTime + + static_cast<PRTime>( + (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds()); +} + +void ServiceWorkerInfo::UpdateActivatedTime() { + MOZ_ASSERT(State() == ServiceWorkerState::Activated); + MOZ_ASSERT(mActivatedTime == 0); + + mActivatedTime = + mCreationTime + + static_cast<PRTime>( + (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds()); +} + +void ServiceWorkerInfo::UpdateRedundantTime() { + MOZ_ASSERT(State() == ServiceWorkerState::Redundant); + MOZ_ASSERT(mRedundantTime == 0); + + mRedundantTime = + mCreationTime + + static_cast<PRTime>( + (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds()); +} + +void ServiceWorkerInfo::SetRegistrationVersion(uint64_t aVersion) { + mDescriptor.SetRegistrationVersion(aVersion); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerInfo.h b/dom/serviceworkers/ServiceWorkerInfo.h new file mode 100644 index 0000000000..03a9eb6aff --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerInfo.h @@ -0,0 +1,183 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerinfo_h +#define mozilla_dom_serviceworkerinfo_h + +#include "MainThreadUtils.h" +#include "mozilla/dom/ServiceWorkerBinding.h" // For ServiceWorkerState +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/TimeStamp.h" +#include "nsIServiceWorkerManager.h" + +namespace mozilla::dom { + +class ClientInfoAndState; +class ClientState; +class ServiceWorkerCloneData; +class ServiceWorkerPrivate; + +/* + * Wherever the spec treats a worker instance and a description of said worker + * as the same thing; i.e. "Resolve foo with + * _GetNewestWorker(serviceWorkerRegistration)", we represent the description + * by this class and spawn a ServiceWorker in the right global when required. + */ +class ServiceWorkerInfo final : public nsIServiceWorkerInfo { + private: + nsCOMPtr<nsIPrincipal> mPrincipal; + ServiceWorkerDescriptor mDescriptor; + const nsString mCacheName; + OriginAttributes mOriginAttributes; + const nsString mWorkerPrivateId; + + // This LoadFlags is only applied to imported scripts, since the main script + // has already been downloaded when performing the bytecheck. This LoadFlag is + // composed of three parts: + // 1. nsIChannel::LOAD_BYPASS_SERVICE_WORKER + // 2. (Optional) nsIRequest::VALIDATE_ALWAYS + // depends on ServiceWorkerUpdateViaCache of its registration. + // 3. (optional) nsIRequest::LOAD_BYPASS_CACHE + // depends on whether the update timer is expired. + const nsLoadFlags mImportsLoadFlags; + + // Timestamp to track SW's state + PRTime mCreationTime; + TimeStamp mCreationTimeStamp; + + // The time of states are 0, if SW has not reached that state yet. Besides, we + // update each of them after UpdateState() is called in SWRegistrationInfo. + PRTime mInstalledTime; + PRTime mActivatedTime; + PRTime mRedundantTime; + + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + bool mSkipWaitingFlag; + + enum { Unknown, Enabled, Disabled } mHandlesFetch; + + uint32_t mNavigationFaultCount; + + // Testing helper to trigger fetch event cancellation when not NS_OK. + // See `nsIServiceWorkerInfo::testingInjectCancellation`. + nsresult mTestingInjectCancellation; + + ~ServiceWorkerInfo(); + + // Generates a unique id for the service worker, with zero being treated as + // invalid. + uint64_t GetNextID() const; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVICEWORKERINFO + + void PostMessage(RefPtr<ServiceWorkerCloneData>&& aData, + const ClientInfo& aClientInfo, + const ClientState& aClientState); + + class ServiceWorkerPrivate* WorkerPrivate() const { + MOZ_ASSERT(mServiceWorkerPrivate); + return mServiceWorkerPrivate; + } + + nsIPrincipal* Principal() const { return mPrincipal; } + + const nsCString& ScriptSpec() const { return mDescriptor.ScriptURL(); } + + const nsCString& Scope() const { return mDescriptor.Scope(); } + + bool SkipWaitingFlag() const { + MOZ_ASSERT(NS_IsMainThread()); + return mSkipWaitingFlag; + } + + void SetSkipWaitingFlag() { + MOZ_ASSERT(NS_IsMainThread()); + mSkipWaitingFlag = true; + } + + void ReportNavigationFault() { + MOZ_ASSERT(NS_IsMainThread()); + mNavigationFaultCount++; + } + + ServiceWorkerInfo(nsIPrincipal* aPrincipal, const nsACString& aScope, + uint64_t aRegistrationId, uint64_t aRegistrationVersion, + const nsACString& aScriptSpec, const nsAString& aCacheName, + nsLoadFlags aLoadFlags); + + ServiceWorkerState State() const { return mDescriptor.State(); } + + const OriginAttributes& GetOriginAttributes() const { + return mOriginAttributes; + } + + const nsString& CacheName() const { return mCacheName; } + + nsLoadFlags GetImportsLoadFlags() const { return mImportsLoadFlags; } + + uint64_t ID() const { return mDescriptor.Id(); } + + const ServiceWorkerDescriptor& Descriptor() const { return mDescriptor; } + + nsresult TestingInjectCancellation() { return mTestingInjectCancellation; } + + void UpdateState(ServiceWorkerState aState); + + // Only used to set initial state when loading from disk! + void SetActivateStateUncheckedWithoutEvent(ServiceWorkerState aState) { + MOZ_ASSERT(NS_IsMainThread()); + mDescriptor.SetState(aState); + } + + void SetHandlesFetch(bool aHandlesFetch) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(mHandlesFetch == Unknown); + mHandlesFetch = aHandlesFetch ? Enabled : Disabled; + mDescriptor.SetHandlesFetch(aHandlesFetch); + } + + void SetRegistrationVersion(uint64_t aVersion); + + bool HandlesFetch() const { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(mHandlesFetch != Unknown); + return mHandlesFetch != Disabled; + } + + void UpdateInstalledTime(); + + void UpdateActivatedTime(); + + void UpdateRedundantTime(); + + int64_t GetInstalledTime() const { return mInstalledTime; } + + void SetInstalledTime(const int64_t aTime) { + if (aTime == 0) { + return; + } + + mInstalledTime = aTime; + } + + int64_t GetActivatedTime() const { return mActivatedTime; } + + void SetActivatedTime(const int64_t aTime) { + if (aTime == 0) { + return; + } + + mActivatedTime = aTime; + } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerinfo_h diff --git a/dom/serviceworkers/ServiceWorkerInterceptController.cpp b/dom/serviceworkers/ServiceWorkerInterceptController.cpp new file mode 100644 index 0000000000..87fdf82af7 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerInterceptController.cpp @@ -0,0 +1,173 @@ +/* -*- 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 "ServiceWorkerInterceptController.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/net/HttpBaseChannel.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsICookieJarSettings.h" +#include "ServiceWorkerManager.h" +#include "nsIPrincipal.h" +#include "nsQueryObject.h" + +namespace mozilla::dom { + +namespace { +bool IsWithinObjectOrEmbed(const nsCOMPtr<nsILoadInfo>& loadInfo) { + RefPtr<BrowsingContext> browsingContext; + loadInfo->GetTargetBrowsingContext(getter_AddRefs(browsingContext)); + + for (BrowsingContext* cur = browsingContext.get(); cur; + cur = cur->GetParent()) { + if (cur->IsEmbedderTypeObjectOrEmbed()) { + return true; + } + } + + return false; +} +} // namespace + +NS_IMPL_ISUPPORTS(ServiceWorkerInterceptController, + nsINetworkInterceptController) + +NS_IMETHODIMP +ServiceWorkerInterceptController::ShouldPrepareForIntercept( + nsIURI* aURI, nsIChannel* aChannel, bool* aShouldIntercept) { + *aShouldIntercept = false; + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + // Block interception if the request's destination is within an object or + // embed element. + if (IsWithinObjectOrEmbed(loadInfo)) { + return NS_OK; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + // For subresource requests we base our decision solely on the client's + // controller value. Any settings that would have blocked service worker + // access should have been set before the initial navigation created the + // window. + if (!nsContentUtils::IsNonSubresourceRequest(aChannel)) { + const Maybe<ServiceWorkerDescriptor>& controller = + loadInfo->GetController(); + + // If the controller doesn't handle fetch events, return false + if (!controller.isSome()) { + return NS_OK; + } + + *aShouldIntercept = controller.ref().HandlesFetch(); + + // The service worker has no fetch event handler, try to schedule a + // soft-update through ServiceWorkerRegistrationInfo. + // Get ServiceWorkerRegistrationInfo by the ServiceWorkerInfo's principal + // and scope + if (!*aShouldIntercept && swm) { + nsCOMPtr<nsIPrincipal> principal = + controller.ref().GetPrincipal().unwrap(); + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(principal, controller.ref().Scope()); + // Could not get ServiceWorkerRegistration here if unregister is + // executed before getting here. + if (NS_WARN_IF(!registration)) { + return NS_OK; + } + registration->MaybeScheduleTimeCheckAndUpdate(); + } + + RefPtr<net::HttpBaseChannel> httpChannel = do_QueryObject(aChannel); + + if (httpChannel && + httpChannel->GetRequestHead()->HasHeader(net::nsHttp::Range)) { + RequestMode requestMode = + InternalRequest::MapChannelToRequestMode(aChannel); + bool mayLoad = nsContentUtils::CheckMayLoad( + loadInfo->GetLoadingPrincipal(), aChannel, + /*allowIfInheritsPrincipal*/ false); + if (requestMode == RequestMode::No_cors && !mayLoad) { + *aShouldIntercept = false; + } + } + + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = StoragePrincipalHelper::GetPrincipal( + aChannel, + StaticPrefs::privacy_partition_serviceWorkers() + ? StoragePrincipalHelper::eForeignPartitionedPrincipal + : StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + // First check with the ServiceWorkerManager for a matching service worker. + if (!swm || !swm->IsAvailable(principal, aURI, aChannel)) { + return NS_OK; + } + + // Check if we're in a secure context, unless service worker testing is + // enabled. + if (!nsContentUtils::ComputeIsSecureContext(aChannel) && + !StaticPrefs::dom_serviceWorkers_testing_enabled()) { + return NS_OK; + } + + // Then check to see if we are allowed to control the window. + // It is important to check for the availability of the service worker first + // to avoid showing warnings about the use of third-party cookies in the UI + // unnecessarily when no service worker is being accessed. + auto storageAccess = StorageAllowedForChannel(aChannel); + if (storageAccess != StorageAccess::eAllow) { + if (!StaticPrefs::privacy_partition_serviceWorkers()) { + return NS_OK; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + + if (!StoragePartitioningEnabled(storageAccess, cookieJarSettings)) { + return NS_OK; + } + } + + *aShouldIntercept = true; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInterceptController::ChannelIntercepted( + nsIInterceptedChannel* aChannel) { + // Note, do not cancel the interception here. The caller will try to + // ResetInterception() on error. + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return NS_ERROR_FAILURE; + } + + ErrorResult error; + swm->DispatchFetchEvent(aChannel, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerInterceptController.h b/dom/serviceworkers/ServiceWorkerInterceptController.h new file mode 100644 index 0000000000..32113291ef --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerInterceptController.h @@ -0,0 +1,25 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerinterceptcontroller_h +#define mozilla_dom_serviceworkerinterceptcontroller_h + +#include "nsINetworkInterceptController.h" + +namespace mozilla::dom { + +class ServiceWorkerInterceptController final + : public nsINetworkInterceptController { + ~ServiceWorkerInterceptController() = default; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSINETWORKINTERCEPTCONTROLLER +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerinterceptcontroller_h diff --git a/dom/serviceworkers/ServiceWorkerJob.cpp b/dom/serviceworkers/ServiceWorkerJob.cpp new file mode 100644 index 0000000000..980d48b66c --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerJob.cpp @@ -0,0 +1,220 @@ +/* -*- 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 "ServiceWorkerJob.h" + +#include "mozilla/dom/WorkerCommon.h" +#include "nsIPrincipal.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "ServiceWorkerManager.h" + +namespace mozilla::dom { + +ServiceWorkerJob::Type ServiceWorkerJob::GetType() const { return mType; } + +ServiceWorkerJob::State ServiceWorkerJob::GetState() const { return mState; } + +bool ServiceWorkerJob::Canceled() const { return mCanceled; } + +bool ServiceWorkerJob::ResultCallbacksInvoked() const { + return mResultCallbacksInvoked; +} + +bool ServiceWorkerJob::IsEquivalentTo(ServiceWorkerJob* aJob) const { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aJob); + return mType == aJob->mType && mScope.Equals(aJob->mScope) && + mScriptSpec.Equals(aJob->mScriptSpec) && + mPrincipal->Equals(aJob->mPrincipal); +} + +void ServiceWorkerJob::AppendResultCallback(Callback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(mState != State::Finished); + MOZ_DIAGNOSTIC_ASSERT(aCallback); + MOZ_DIAGNOSTIC_ASSERT(mFinalCallback != aCallback); + MOZ_ASSERT(!mResultCallbackList.Contains(aCallback)); + MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked); + mResultCallbackList.AppendElement(aCallback); +} + +void ServiceWorkerJob::StealResultCallbacksFrom(ServiceWorkerJob* aJob) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aJob); + MOZ_ASSERT(aJob->mState == State::Initial); + + // Take the callbacks from the other job immediately to avoid the + // any possibility of them existing on both jobs at once. + nsTArray<RefPtr<Callback>> callbackList = + std::move(aJob->mResultCallbackList); + + for (RefPtr<Callback>& callback : callbackList) { + // Use AppendResultCallback() so that assertion checking is performed on + // each callback. + AppendResultCallback(callback); + } +} + +void ServiceWorkerJob::Start(Callback* aFinalCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(!mCanceled); + + MOZ_DIAGNOSTIC_ASSERT(aFinalCallback); + MOZ_DIAGNOSTIC_ASSERT(!mFinalCallback); + MOZ_ASSERT(!mResultCallbackList.Contains(aFinalCallback)); + mFinalCallback = aFinalCallback; + + MOZ_DIAGNOSTIC_ASSERT(mState == State::Initial); + mState = State::Started; + + nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod( + "ServiceWorkerJob::AsyncExecute", this, &ServiceWorkerJob::AsyncExecute); + + // We may have to wait for the PBackground actor to be initialized + // before proceeding. We should always be able to get a ServiceWorkerManager, + // however, since Start() should not be called during shutdown. + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + + // Otherwise start asynchronously. We should never run a job synchronously. + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable.forget()))); +} + +void ServiceWorkerJob::Cancel() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mCanceled); + mCanceled = true; + + if (GetState() != State::Started) { + MOZ_ASSERT(GetState() == State::Initial); + + ErrorResult error(NS_ERROR_DOM_ABORT_ERR); + InvokeResultCallbacks(error); + + // The callbacks might not consume the error, which is fine. + error.SuppressException(); + } +} + +ServiceWorkerJob::ServiceWorkerJob(Type aType, nsIPrincipal* aPrincipal, + const nsACString& aScope, + nsCString aScriptSpec) + : mType(aType), + mPrincipal(aPrincipal), + mScope(aScope), + mScriptSpec(std::move(aScriptSpec)), + mState(State::Initial), + mCanceled(false), + mResultCallbacksInvoked(false) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPrincipal); + MOZ_ASSERT(!mScope.IsEmpty()); + + // Empty script URL if and only if this is an unregister job. + MOZ_ASSERT((mType == Type::Unregister) == mScriptSpec.IsEmpty()); +} + +ServiceWorkerJob::~ServiceWorkerJob() { + MOZ_ASSERT(NS_IsMainThread()); + // Jobs must finish or never be started. Destroying an actively running + // job is an error. + MOZ_ASSERT(mState != State::Started); + MOZ_ASSERT_IF(mState == State::Finished, mResultCallbacksInvoked); +} + +void ServiceWorkerJob::InvokeResultCallbacks(ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(mState != State::Finished); + MOZ_DIAGNOSTIC_ASSERT_IF(mState == State::Initial, Canceled()); + + MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked); + mResultCallbacksInvoked = true; + + nsTArray<RefPtr<Callback>> callbackList = std::move(mResultCallbackList); + + for (RefPtr<Callback>& callback : callbackList) { + // The callback might consume an exception on the ErrorResult, so we need + // to clone in order to maintain the error for the next callback. + ErrorResult rv; + aRv.CloneTo(rv); + + if (GetState() == State::Started) { + callback->JobFinished(this, rv); + } else { + callback->JobDiscarded(rv); + } + + // The callback might not consume the error. + rv.SuppressException(); + } +} + +void ServiceWorkerJob::InvokeResultCallbacks(nsresult aRv) { + ErrorResult converted(aRv); + InvokeResultCallbacks(converted); +} + +void ServiceWorkerJob::Finish(ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + // Avoid double-completion because it can result on operating on cleaned + // up data. This should not happen, though, so also assert to try to + // narrow down the causes. + MOZ_DIAGNOSTIC_ASSERT(mState == State::Started); + if (mState != State::Started) { + return; + } + + // Ensure that we only surface SecurityErr, TypeErr or InvalidStateErr to + // script. + if (aRv.Failed() && !aRv.ErrorCodeIs(NS_ERROR_DOM_SECURITY_ERR) && + !aRv.ErrorCodeIs(NS_ERROR_INTERNAL_ERRORRESULT_TYPEERROR) && + !aRv.ErrorCodeIs(NS_ERROR_DOM_INVALID_STATE_ERR)) { + // Remove the old error code so we can replace it with a TypeError. + aRv.SuppressException(); + + // Throw the type error with a generic error message. We use a stack + // reference to bypass the normal static analysis for "return right after + // throwing", since it's not the right check here: this ErrorResult came in + // pre-thrown. + ErrorResult& rv = aRv; + rv.ThrowTypeError<MSG_SW_INSTALL_ERROR>(mScriptSpec, mScope); + } + + // The final callback may drop the last ref to this object. + RefPtr<ServiceWorkerJob> kungFuDeathGrip = this; + + if (!mResultCallbacksInvoked) { + InvokeResultCallbacks(aRv); + } + + mState = State::Finished; + + MOZ_DIAGNOSTIC_ASSERT(mFinalCallback); + if (mFinalCallback) { + mFinalCallback->JobFinished(this, aRv); + mFinalCallback = nullptr; + } + + // The callback might not consume the error. + aRv.SuppressException(); + + // Async release this object to ensure that our caller methods complete + // as well. + NS_ReleaseOnMainThread("ServiceWorkerJobProxyRunnable", + kungFuDeathGrip.forget(), true /* always proxy */); +} + +void ServiceWorkerJob::Finish(nsresult aRv) { + ErrorResult converted(aRv); + Finish(converted); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerJob.h b/dom/serviceworkers/ServiceWorkerJob.h new file mode 100644 index 0000000000..70eed04636 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerJob.h @@ -0,0 +1,126 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerjob_h +#define mozilla_dom_serviceworkerjob_h + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsIPrincipal; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class ServiceWorkerJob { + public: + // Implement this interface to receive notification when a job completes or + // is discarded. + class Callback { + public: + // Called once when the job completes. If the job is started, then this + // will be called. If a job is never executed due to browser shutdown, + // then this method will never be called. This method is always called + // on the main thread asynchronously after Start() completes. + virtual void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) = 0; + + // If the job has not started and will never start, then this will be + // called; either JobFinished or JobDiscarded will be called, but not both. + virtual void JobDiscarded(ErrorResult& aStatus) = 0; + + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + }; + + enum class Type { Register, Update, Unregister }; + + enum class State { Initial, Started, Finished }; + + Type GetType() const; + + State GetState() const; + + // Determine if the job has been canceled. This does not change the + // current State, but indicates that the job should progress to Finished + // as soon as possible. + bool Canceled() const; + + // Determine if the result callbacks have already been called. This is + // equivalent to the spec checked to see if the job promise has settled. + bool ResultCallbacksInvoked() const; + + bool IsEquivalentTo(ServiceWorkerJob* aJob) const; + + // Add a callback that will be invoked when the job's result is available. + // Some job types will invoke this before the job is actually finished. + // If an early callback does not occur, then it will be called automatically + // when Finish() is called. These callbacks will be invoked while the job + // state is Started. + void AppendResultCallback(Callback* aCallback); + + // This takes ownership of any result callbacks associated with the given job + // and then appends them to this job's callback list. + void StealResultCallbacksFrom(ServiceWorkerJob* aJob); + + // Start the job. All work will be performed asynchronously on + // the main thread. The Finish() method must be called exactly + // once after this point. A final callback must be provided. It + // will be invoked after all other callbacks have been processed. + void Start(Callback* aFinalCallback); + + // Set an internal flag indicating that a started job should finish as + // soon as possible. + void Cancel(); + + protected: + ServiceWorkerJob(Type aType, nsIPrincipal* aPrincipal, + const nsACString& aScope, nsCString aScriptSpec); + + virtual ~ServiceWorkerJob(); + + // Invoke the result callbacks immediately. The job must be in the + // Started state or be canceled and in the Initial state. The callbacks are + // cleared after being invoked, so subsequent method calls have no effect. + void InvokeResultCallbacks(ErrorResult& aRv); + + // Convenience method that converts to ErrorResult and calls real method. + void InvokeResultCallbacks(nsresult aRv); + + // Indicate that the job has completed. The must be called exactly + // once after Start() has initiated job execution. It may not be + // called until Start() has returned. + void Finish(ErrorResult& aRv); + + // Convenience method that converts to ErrorResult and calls real method. + void Finish(nsresult aRv); + + // Specific job types should define AsyncExecute to begin their work. + // All errors and successes must result in Finish() being called. + virtual void AsyncExecute() = 0; + + const Type mType; + nsCOMPtr<nsIPrincipal> mPrincipal; + const nsCString mScope; + const nsCString mScriptSpec; + + private: + RefPtr<Callback> mFinalCallback; + nsTArray<RefPtr<Callback>> mResultCallbackList; + State mState; + bool mCanceled; + bool mResultCallbacksInvoked; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJob) +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_serviceworkerjob_h diff --git a/dom/serviceworkers/ServiceWorkerJobQueue.cpp b/dom/serviceworkers/ServiceWorkerJobQueue.cpp new file mode 100644 index 0000000000..497265249a --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerJobQueue.cpp @@ -0,0 +1,120 @@ +/* -*- 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 "ServiceWorkerJobQueue.h" + +#include "nsThreadUtils.h" +#include "ServiceWorkerJob.h" +#include "mozilla/dom/WorkerCommon.h" + +namespace mozilla::dom { + +class ServiceWorkerJobQueue::Callback final + : public ServiceWorkerJob::Callback { + RefPtr<ServiceWorkerJobQueue> mQueue; + + ~Callback() = default; + + public: + explicit Callback(ServiceWorkerJobQueue* aQueue) : mQueue(aQueue) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mQueue); + } + + virtual void JobFinished(ServiceWorkerJob* aJob, + ErrorResult& aStatus) override { + MOZ_ASSERT(NS_IsMainThread()); + mQueue->JobFinished(aJob); + } + + virtual void JobDiscarded(ErrorResult&) override { + // no-op; nothing to do. + } + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJobQueue::Callback, override) +}; + +ServiceWorkerJobQueue::~ServiceWorkerJobQueue() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mJobList.IsEmpty()); +} + +void ServiceWorkerJobQueue::JobFinished(ServiceWorkerJob* aJob) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aJob); + + // XXX There are some corner cases where jobs can double-complete. Until + // we track all these down we do a non-fatal assert in debug builds and + // a runtime check to verify the queue is in the correct state. + NS_ASSERTION(!mJobList.IsEmpty(), + "Job queue should contain the job that just completed."); + NS_ASSERTION(mJobList.SafeElementAt(0, nullptr) == aJob, + "Job queue should contain the job that just completed."); + if (NS_WARN_IF(mJobList.SafeElementAt(0, nullptr) != aJob)) { + return; + } + + mJobList.RemoveElementAt(0); + + if (mJobList.IsEmpty()) { + return; + } + + RunJob(); +} + +void ServiceWorkerJobQueue::RunJob() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mJobList.IsEmpty()); + MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Initial); + + RefPtr<Callback> callback = new Callback(this); + mJobList[0]->Start(callback); +} + +ServiceWorkerJobQueue::ServiceWorkerJobQueue() { + MOZ_ASSERT(NS_IsMainThread()); +} + +void ServiceWorkerJobQueue::ScheduleJob(ServiceWorkerJob* aJob) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aJob); + MOZ_ASSERT(!mJobList.Contains(aJob)); + + if (mJobList.IsEmpty()) { + mJobList.AppendElement(aJob); + RunJob(); + return; + } + + MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Started); + + RefPtr<ServiceWorkerJob>& tailJob = mJobList[mJobList.Length() - 1]; + if (!tailJob->ResultCallbacksInvoked() && aJob->IsEquivalentTo(tailJob)) { + tailJob->StealResultCallbacksFrom(aJob); + return; + } + + mJobList.AppendElement(aJob); +} + +void ServiceWorkerJobQueue::CancelAll() { + MOZ_ASSERT(NS_IsMainThread()); + + for (RefPtr<ServiceWorkerJob>& job : mJobList) { + job->Cancel(); + } + + // Remove jobs that are queued but not started since they should never + // run after being canceled. This means throwing away all jobs except + // for the job at the front of the list. + if (!mJobList.IsEmpty()) { + MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Started); + mJobList.TruncateLength(1); + } +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerJobQueue.h b/dom/serviceworkers/ServiceWorkerJobQueue.h new file mode 100644 index 0000000000..9e51aefe4f --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerJobQueue.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerjobqueue_h +#define mozilla_dom_serviceworkerjobqueue_h + +#include "mozilla/RefPtr.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class ServiceWorkerJob; + +class ServiceWorkerJobQueue final { + class Callback; + + nsTArray<RefPtr<ServiceWorkerJob>> mJobList; + + ~ServiceWorkerJobQueue(); + + void JobFinished(ServiceWorkerJob* aJob); + + void RunJob(); + + public: + ServiceWorkerJobQueue(); + + void ScheduleJob(ServiceWorkerJob* aJob); + + void CancelAll(); + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJobQueue) +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerjobqueue_h diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp new file mode 100644 index 0000000000..6b1bb43398 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerManager.cpp @@ -0,0 +1,3364 @@ +/* -*- 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 "ServiceWorkerManager.h" + +#include <algorithm> + +#include "nsCOMPtr.h" +#include "nsICookieJarSettings.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsINamed.h" +#include "nsINetworkInterceptController.h" +#include "nsIMutableArray.h" +#include "nsIPrincipal.h" +#include "nsITimer.h" +#include "nsIUploadChannel2.h" +#include "nsServiceManagerUtils.h" +#include "nsDebug.h" +#include "nsIPermissionManager.h" +#include "nsXULAppAPI.h" + +#include "jsapi.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/LoadContext.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ClientHandle.h" +#include "mozilla/dom/ClientManager.h" +#include "mozilla/dom/ClientSource.h" +#include "mozilla/dom/ConsoleUtils.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/NotificationEvent.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/SharedWorker.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/extensions/WebExtensionPolicy.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/Unused.h" +#include "mozilla/EnumSet.h" + +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIDUtils.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsTArray.h" + +#include "ServiceWorker.h" +#include "ServiceWorkerContainer.h" +#include "ServiceWorkerInfo.h" +#include "ServiceWorkerJobQueue.h" +#include "ServiceWorkerManagerChild.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerRegisterJob.h" +#include "ServiceWorkerRegistrar.h" +#include "ServiceWorkerRegistration.h" +#include "ServiceWorkerScriptCache.h" +#include "ServiceWorkerShutdownBlocker.h" +#include "ServiceWorkerEvents.h" +#include "ServiceWorkerUnregisterJob.h" +#include "ServiceWorkerUpdateJob.h" +#include "ServiceWorkerUtils.h" +#include "ServiceWorkerQuotaUtils.h" + +#ifdef PostMessage +# undef PostMessage +#endif + +mozilla::LazyLogModule sWorkerTelemetryLog("WorkerTelemetry"); + +#ifdef LOG +# undef LOG +#endif +#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args); + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla::dom { + +// Counts the number of registered ServiceWorkers, and the number that +// handle Fetch, for reporting in Telemetry +uint32_t gServiceWorkersRegistered = 0; +uint32_t gServiceWorkersRegisteredFetch = 0; + +static_assert( + nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW == + static_cast<uint32_t>(RequestRedirect::Follow), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert( + nsIHttpChannelInternal::REDIRECT_MODE_ERROR == + static_cast<uint32_t>(RequestRedirect::Error), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert( + nsIHttpChannelInternal::REDIRECT_MODE_MANUAL == + static_cast<uint32_t>(RequestRedirect::Manual), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert( + 3 == RequestRedirectValues::Count, + "RequestRedirect enumeration value should make Necko Redirect mode value."); + +static_assert( + nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT == + static_cast<uint32_t>(RequestCache::Default), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert( + nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE == + static_cast<uint32_t>(RequestCache::No_store), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert( + nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD == + static_cast<uint32_t>(RequestCache::Reload), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert( + nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE == + static_cast<uint32_t>(RequestCache::No_cache), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert( + nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE == + static_cast<uint32_t>(RequestCache::Force_cache), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert( + nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED == + static_cast<uint32_t>(RequestCache::Only_if_cached), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert( + 6 == RequestCacheValues::Count, + "RequestCache enumeration value should match Necko Cache mode value."); + +static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::Imports) == + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" + " should match ServiceWorkerUpdateViaCache enumeration."); +static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::All) == + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, + "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" + " should match ServiceWorkerUpdateViaCache enumeration."); +static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None) == + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE, + "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" + " should match ServiceWorkerUpdateViaCache enumeration."); + +static StaticRefPtr<ServiceWorkerManager> gInstance; + +namespace { + +nsresult PopulateRegistrationData( + nsIPrincipal* aPrincipal, + const ServiceWorkerRegistrationInfo* aRegistration, + ServiceWorkerRegistrationData& aData) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aRegistration); + + if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal())) { + return NS_ERROR_FAILURE; + } + + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aData.scope() = aRegistration->Scope(); + + // TODO: When bug 1426401 is implemented we will need to handle more + // than just the active worker here. + RefPtr<ServiceWorkerInfo> active = aRegistration->GetActive(); + MOZ_ASSERT(active); + if (NS_WARN_IF(!active)) { + return NS_ERROR_FAILURE; + } + + aData.currentWorkerURL() = active->ScriptSpec(); + aData.cacheName() = active->CacheName(); + aData.currentWorkerHandlesFetch() = active->HandlesFetch(); + + aData.currentWorkerInstalledTime() = active->GetInstalledTime(); + aData.currentWorkerActivatedTime() = active->GetActivatedTime(); + + aData.updateViaCache() = + static_cast<uint32_t>(aRegistration->GetUpdateViaCache()); + + aData.lastUpdateTime() = aRegistration->GetLastUpdateTime(); + + aData.navigationPreloadState() = aRegistration->GetNavigationPreloadState(); + + MOZ_ASSERT(ServiceWorkerRegistrationDataIsValid(aData)); + + return NS_OK; +} + +class TeardownRunnable final : public Runnable { + public: + explicit TeardownRunnable(ServiceWorkerManagerChild* aActor) + : Runnable("dom::ServiceWorkerManager::TeardownRunnable"), + mActor(aActor) { + MOZ_ASSERT(mActor); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(mActor); + PServiceWorkerManagerChild::Send__delete__(mActor); + return NS_OK; + } + + private: + ~TeardownRunnable() = default; + + RefPtr<ServiceWorkerManagerChild> mActor; +}; + +constexpr char kFinishShutdownTopic[] = "profile-before-change-qm"; + +already_AddRefed<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() { + AssertIsOnMainThread(); + + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + MOZ_ASSERT(svc); + + nsCOMPtr<nsIAsyncShutdownClient> barrier; + DebugOnly<nsresult> rv = + svc->GetProfileChangeTeardown(getter_AddRefs(barrier)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return barrier.forget(); +} + +Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal( + nsIURI* aScopeURI, const OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(aScopeURI); + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aScopeURI, aOriginAttributes); + if (NS_WARN_IF(!principal)) { + return Err(NS_ERROR_FAILURE); + } + + return principal; +} + +Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal( + const nsACString& aScope, const OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(nsContentUtils::IsAbsoluteURL(aScope)); + + nsCOMPtr<nsIURI> scopeURI; + MOZ_TRY(NS_NewURI(getter_AddRefs(scopeURI), aScope)); + + return ScopeToPrincipal(scopeURI, aOriginAttributes); +} + +} // namespace + +struct ServiceWorkerManager::RegistrationDataPerPrincipal final { + // Implements a container of keys for the "scope to registration map": + // https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map + // + // where each key is an absolute URL. + // + // The properties of this map that the spec uses are + // 1) insertion, + // 2) removal, + // 3) iteration of scopes in FIFO order (excluding removed scopes), + // 4) and finding, for a given path, the maximal length scope which is a + // prefix of the path. + // + // Additionally, because this is a container of keys for a map, there + // shouldn't be duplicate scopes. + // + // The current implementation uses a dynamic array as the underlying + // container, which is not optimal for unbounded container sizes (all + // supported operations are in linear time) but may be superior for small + // container sizes. + // + // If this is proven to be too slow, the underlying storage should be replaced + // with a linked list of scopes in combination with an ordered map that maps + // scopes to linked list elements/iterators. This would reduce all of the + // above operations besides iteration (necessarily linear) to logarithmic + // time. + class ScopeContainer final : private nsTArray<nsCString> { + using Base = nsTArray<nsCString>; + + public: + using Base::Contains; + using Base::IsEmpty; + using Base::Length; + + // No using-declaration to avoid importing the non-const overload. + decltype(auto) operator[](Base::index_type aIndex) const { + return Base::operator[](aIndex); + } + + void InsertScope(const nsACString& aScope) { + MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::IsAbsoluteURL(aScope)); + + if (Contains(aScope)) { + return; + } + + AppendElement(aScope); + } + + void RemoveScope(const nsACString& aScope) { + MOZ_ALWAYS_TRUE(RemoveElement(aScope)); + } + + // Implements most of "Match Service Worker Registration": + // https://w3c.github.io/ServiceWorker/#scope-match-algorithm + Maybe<nsCString> MatchScope(const nsACString& aClientUrl) const { + Maybe<nsCString> match; + + for (const nsCString& scope : *this) { + if (StringBeginsWith(aClientUrl, scope)) { + if (!match || scope.Length() > match->Length()) { + match = Some(scope); + } + } + } + + // Step 7.2: + // "Assert: matchingScope’s origin and clientURL’s origin are same + // origin." + MOZ_DIAGNOSTIC_ASSERT_IF(match, IsSameOrigin(*match, aClientUrl)); + + return match; + } + + private: + bool IsSameOrigin(const nsACString& aMatchingScope, + const nsACString& aClientUrl) const { + auto parseResult = ScopeToPrincipal(aMatchingScope, OriginAttributes()); + + if (NS_WARN_IF(parseResult.isErr())) { + return false; + } + + auto scopePrincipal = parseResult.unwrap(); + + parseResult = ScopeToPrincipal(aClientUrl, OriginAttributes()); + + if (NS_WARN_IF(parseResult.isErr())) { + return false; + } + + auto clientPrincipal = parseResult.unwrap(); + + bool equals = false; + + if (NS_WARN_IF( + NS_FAILED(scopePrincipal->Equals(clientPrincipal, &equals)))) { + return false; + } + + return equals; + } + }; + + ScopeContainer mScopeContainer; + + // Scope to registration. + // The scope should be a fully qualified valid URL. + nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo> mInfos; + + // Maps scopes to job queues. + nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerJobQueue> mJobQueues; + + // Map scopes to scheduled update timers. + nsInterfaceHashtable<nsCStringHashKey, nsITimer> mUpdateTimers; + + // The number of times we have done a quota usage check for this origin for + // mitigation purposes. See the docs on nsIServiceWorkerRegistrationInfo, + // where this value is exposed. + int32_t mQuotaUsageCheckCount = 0; +}; + +////////////////////////// +// ServiceWorkerManager // +////////////////////////// + +NS_IMPL_ADDREF(ServiceWorkerManager) +NS_IMPL_RELEASE(ServiceWorkerManager) + +NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager) + NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager) +NS_INTERFACE_MAP_END + +ServiceWorkerManager::ServiceWorkerManager() + : mActor(nullptr), mShuttingDown(false) {} + +ServiceWorkerManager::~ServiceWorkerManager() { + // The map will assert if it is not empty when destroyed. + mRegistrationInfos.Clear(); + + // This can happen if the browser is started up in ProfileManager mode, in + // which case XPCOM will startup and shutdown, but there won't be any + // profile-* topic notifications. The shutdown blocker expects to be in a + // NotAcceptingPromises state when it's destroyed, and this transition + // normally happens in the "profile-change-teardown" notification callback + // (which won't be called in ProfileManager mode). + if (!mShuttingDown && mShutdownBlocker) { + mShutdownBlocker->StopAcceptingPromises(); + } +} + +void ServiceWorkerManager::BlockShutdownOn(GenericNonExclusivePromise* aPromise, + uint32_t aShutdownStateId) { + AssertIsOnMainThread(); + + MOZ_ASSERT(mShutdownBlocker); + MOZ_ASSERT(aPromise); + + mShutdownBlocker->WaitOnPromise(aPromise, aShutdownStateId); +} + +void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) { + // ServiceWorkers now only support parent intercept. In parent intercept + // mode, only the parent process ServiceWorkerManager has any state or does + // anything. + // + // It is our goal to completely eliminate support for content process + // ServiceWorkerManager instances and make getting a SWM instance trigger a + // fatal assertion. But until we've reached that point, we make + // initialization a no-op so that content process ServiceWorkerManager + // instances will simply have no state and no registrations. + if (!XRE_IsParentProcess()) { + return; + } + + nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier = GetAsyncShutdownBarrier(); + + if (shutdownBarrier) { + mShutdownBlocker = ServiceWorkerShutdownBlocker::CreateAndRegisterOn( + *shutdownBarrier, *this); + MOZ_ASSERT(mShutdownBlocker); + } + + MOZ_DIAGNOSTIC_ASSERT(aRegistrar); + + nsTArray<ServiceWorkerRegistrationData> data; + aRegistrar->GetRegistrations(data); + LoadRegistrations(data); + + PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + MaybeStartShutdown(); + return; + } + + PServiceWorkerManagerChild* actor = + actorChild->SendPServiceWorkerManagerConstructor(); + if (!actor) { + MaybeStartShutdown(); + return; + } + + mActor = static_cast<ServiceWorkerManagerChild*>(actor); + + mTelemetryLastChange = TimeStamp::Now(); +} + +void ServiceWorkerManager::RecordTelemetry(uint32_t aNumber, uint32_t aFetch) { + // Submit N value pairs to Telemetry for the time we were at those values + auto now = TimeStamp::Now(); + // round down, with a minimum of 1 repeat. In theory this gives + // inaccuracy if there are frequent changes, but that's uncommon. + uint32_t repeats = (uint32_t)((now - mTelemetryLastChange).ToMilliseconds()) / + mTelemetryPeriodMs; + mTelemetryLastChange = now; + if (repeats == 0) { + repeats = 1; + } + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + "ServiceWorkerTelemetryRunnable", [aNumber, aFetch, repeats]() { + LOG(("ServiceWorkers running: %u samples of %u/%u", repeats, aNumber, + aFetch)); + // Don't allocate infinitely huge arrays if someone visits a SW site + // after a few months running. 1 month is about 500K repeats @ 5s + // sampling + uint32_t num_repeats = std::min(repeats, 1000000U); // 4MB max + nsTArray<uint32_t> values; + + uint32_t* array = values.AppendElements(num_repeats); + for (uint32_t i = 0; i < num_repeats; i++) { + array[i] = aNumber; + } + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "All"_ns, + values); + + for (uint32_t i = 0; i < num_repeats; i++) { + array[i] = aFetch; + } + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "Fetch"_ns, + values); + }); + NS_DispatchBackgroundTask(runnable.forget(), nsIEventTarget::DISPATCH_NORMAL); +} + +RefPtr<GenericErrorResultPromise> ServiceWorkerManager::StartControllingClient( + const ClientInfo& aClientInfo, + ServiceWorkerRegistrationInfo* aRegistrationInfo, + bool aControlClientHandle) { + MOZ_DIAGNOSTIC_ASSERT(aRegistrationInfo->GetActive()); + + // XXX We can't use a generic lambda (accepting auto&& entry) like elsewhere + // with WithEntryHandle, since we get linker errors then using clang+lld. This + // might be a toolchain issue? + return mControlledClients.WithEntryHandle( + aClientInfo.Id(), + [&](decltype(mControlledClients)::EntryHandle&& entry) + -> RefPtr<GenericErrorResultPromise> { + const RefPtr<ServiceWorkerManager> self = this; + + const ServiceWorkerDescriptor& active = + aRegistrationInfo->GetActive()->Descriptor(); + + if (entry) { + const RefPtr<ServiceWorkerRegistrationInfo> old = + std::move(entry.Data()->mRegistrationInfo); + + const RefPtr<GenericErrorResultPromise> promise = + aControlClientHandle + ? entry.Data()->mClientHandle->Control(active) + : GenericErrorResultPromise::CreateAndResolve(false, + __func__); + + entry.Data()->mRegistrationInfo = aRegistrationInfo; + + if (old != aRegistrationInfo) { + StopControllingRegistration(old); + aRegistrationInfo->StartControllingClient(); + } + + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, + 1); + + // Always check to see if we failed to actually control the client. In + // that case remove the client from our list of controlled clients. + return promise->Then( + GetMainThreadSerialEventTarget(), __func__, + [](bool) { + // do nothing on success + return GenericErrorResultPromise::CreateAndResolve(true, + __func__); + }, + [self, aClientInfo](const CopyableErrorResult& aRv) { + // failed to control, forget about this client + self->StopControllingClient(aClientInfo); + return GenericErrorResultPromise::CreateAndReject(aRv, + __func__); + }); + } + + RefPtr<ClientHandle> clientHandle = ClientManager::CreateHandle( + aClientInfo, GetMainThreadSerialEventTarget()); + + const RefPtr<GenericErrorResultPromise> promise = + aControlClientHandle + ? clientHandle->Control(active) + : GenericErrorResultPromise::CreateAndResolve(false, __func__); + + aRegistrationInfo->StartControllingClient(); + + entry.Insert( + MakeUnique<ControlledClientData>(clientHandle, aRegistrationInfo)); + + clientHandle->OnDetach()->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, aClientInfo] { self->StopControllingClient(aClientInfo); }); + + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, + 1); + + // Always check to see if we failed to actually control the client. In + // that case removed the client from our list of controlled clients. + return promise->Then( + GetMainThreadSerialEventTarget(), __func__, + [](bool) { + // do nothing on success + return GenericErrorResultPromise::CreateAndResolve(true, + __func__); + }, + [self, aClientInfo](const CopyableErrorResult& aRv) { + // failed to control, forget about this client + self->StopControllingClient(aClientInfo); + return GenericErrorResultPromise::CreateAndReject(aRv, __func__); + }); + }); +} + +void ServiceWorkerManager::StopControllingClient( + const ClientInfo& aClientInfo) { + auto entry = mControlledClients.Lookup(aClientInfo.Id()); + if (!entry) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> reg = + std::move(entry.Data()->mRegistrationInfo); + + entry.Remove(); + + StopControllingRegistration(reg); +} + +void ServiceWorkerManager::MaybeStartShutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mShuttingDown) { + return; + } + + mShuttingDown = true; + + for (const auto& dataPtr : mRegistrationInfos.Values()) { + for (const auto& timerEntry : dataPtr->mUpdateTimers.Values()) { + timerEntry->Cancel(); + } + dataPtr->mUpdateTimers.Clear(); + + for (const auto& queueEntry : dataPtr->mJobQueues.Values()) { + queueEntry->CancelAll(); + } + dataPtr->mJobQueues.Clear(); + + for (const auto& registrationEntry : dataPtr->mInfos.Values()) { + registrationEntry->ShutdownWorkers(); + } + + // ServiceWorkerCleanup may try to unregister registrations, so don't clear + // mInfos. + } + + for (const auto& entry : mControlledClients.Values()) { + entry->mRegistrationInfo->ShutdownWorkers(); + } + + for (auto iter = mOrphanedRegistrations.iter(); !iter.done(); iter.next()) { + iter.get()->ShutdownWorkers(); + } + + if (mShutdownBlocker) { + mShutdownBlocker->StopAcceptingPromises(); + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, kFinishShutdownTopic, false); + return; + } + + MaybeFinishShutdown(); +} + +void ServiceWorkerManager::MaybeFinishShutdown() { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, kFinishShutdownTopic); + } + + if (!mActor) { + return; + } + + mActor->ManagerShuttingDown(); + + RefPtr<TeardownRunnable> runnable = new TeardownRunnable(mActor); + nsresult rv = NS_DispatchToMainThread(runnable); + Unused << NS_WARN_IF(NS_FAILED(rv)); + mActor = nullptr; + + // This also submits final telemetry + ServiceWorkerPrivate::RunningShutdown(); +} + +class ServiceWorkerResolveWindowPromiseOnRegisterCallback final + : public ServiceWorkerJob::Callback { + public: + NS_INLINE_DECL_REFCOUNTING( + ServiceWorkerResolveWindowPromiseOnRegisterCallback, override) + + virtual void JobFinished(ServiceWorkerJob* aJob, + ErrorResult& aStatus) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aJob); + + if (aStatus.Failed()) { + mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register); + RefPtr<ServiceWorkerRegisterJob> registerJob = + static_cast<ServiceWorkerRegisterJob*>(aJob); + RefPtr<ServiceWorkerRegistrationInfo> reg = registerJob->GetRegistration(); + + mPromiseHolder.Resolve(reg->Descriptor(), __func__); + } + + virtual void JobDiscarded(ErrorResult& aStatus) override { + MOZ_ASSERT(NS_IsMainThread()); + + mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__); + } + + RefPtr<ServiceWorkerRegistrationPromise> Promise() { + MOZ_ASSERT(NS_IsMainThread()); + return mPromiseHolder.Ensure(__func__); + } + + private: + ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() = default; + + MozPromiseHolder<ServiceWorkerRegistrationPromise> mPromiseHolder; +}; + +NS_IMETHODIMP +ServiceWorkerManager::RegisterForTest(nsIPrincipal* aPrincipal, + const nsAString& aScopeURL, + const nsAString& aScriptURL, + JSContext* aCx, + mozilla::dom::Promise** aPromise) { + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult erv; + RefPtr<Promise> outer = Promise::Create(global, erv); + if (NS_WARN_IF(erv.Failed())) { + return erv.StealNSResult(); + } + + if (!StaticPrefs::dom_serviceWorkers_testing_enabled()) { + outer->MaybeRejectWithAbortError( + "registerForTest only allowed when dom.serviceWorkers.testing.enabled " + "is true"); + outer.forget(aPromise); + return NS_OK; + } + + if (aPrincipal == nullptr) { + outer->MaybeRejectWithAbortError("Missing principal"); + outer.forget(aPromise); + return NS_OK; + } + + if (aScriptURL.IsEmpty()) { + outer->MaybeRejectWithAbortError("Missing script url"); + outer.forget(aPromise); + return NS_OK; + } + + if (aScopeURL.IsEmpty()) { + outer->MaybeRejectWithAbortError("Missing scope url"); + outer.forget(aPromise); + return NS_OK; + } + + // The ClientType isn't really used here, but ClientType::Window + // is the least bad choice since this is happening on the main thread. + Maybe<ClientInfo> clientInfo = + dom::ClientManager::CreateInfo(ClientType::Window, aPrincipal); + + if (!clientInfo.isSome()) { + outer->MaybeRejectWithUnknownError("Error creating clientInfo"); + outer.forget(aPromise); + return NS_OK; + } + + auto scope = NS_ConvertUTF16toUTF8(aScopeURL); + auto scriptURL = NS_ConvertUTF16toUTF8(aScriptURL); + + auto regPromise = Register(clientInfo.ref(), scope, scriptURL, + dom::ServiceWorkerUpdateViaCache::Imports); + const RefPtr<ServiceWorkerManager> self(this); + const nsCOMPtr<nsIPrincipal> principal(aPrincipal); + regPromise->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, outer, principal, + scope](const ServiceWorkerRegistrationDescriptor& regDesc) { + RefPtr<ServiceWorkerRegistrationInfo> registration = + self->GetRegistration(principal, NS_ConvertUTF16toUTF8(scope)); + if (registration) { + outer->MaybeResolve(registration); + } else { + outer->MaybeRejectWithUnknownError( + "Failed to retrieve ServiceWorkerRegistrationInfo"); + } + }, + [outer](const mozilla::CopyableErrorResult& err) { + CopyableErrorResult result(err); + outer->MaybeReject(std::move(result)); + }); + + outer.forget(aPromise); + + return NS_OK; +} + +RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::Register( + const ClientInfo& aClientInfo, const nsACString& aScopeURL, + const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) { + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScopeURL); + if (NS_FAILED(rv)) { + // Odd, since it was serialiazed from an nsIURI. + CopyableErrorResult err; + err.ThrowInvalidStateError("Scope URL cannot be parsed"); + return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__); + } + + nsCOMPtr<nsIURI> scriptURI; + rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL); + if (NS_FAILED(rv)) { + // Odd, since it was serialiazed from an nsIURI. + CopyableErrorResult err; + err.ThrowInvalidStateError("Script URL cannot be parsed"); + return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__); + } + + IgnoredErrorResult err; + ServiceWorkerScopeAndScriptAreValid(aClientInfo, scopeURI, scriptURI, err); + if (err.Failed()) { + return ServiceWorkerRegistrationPromise::CreateAndReject( + CopyableErrorResult(std::move(err)), __func__); + } + + // If the previous validation step passed then we must have a principal. + auto principalOrErr = aClientInfo.GetPrincipal(); + + if (NS_WARN_IF(principalOrErr.isErr())) { + return ServiceWorkerRegistrationPromise::CreateAndReject( + CopyableErrorResult(principalOrErr.unwrapErr()), __func__); + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(principal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ServiceWorkerRegistrationPromise::CreateAndReject( + CopyableErrorResult(rv), __func__); + } + + RefPtr<ServiceWorkerJobQueue> queue = + GetOrCreateJobQueue(scopeKey, aScopeURL); + + RefPtr<ServiceWorkerResolveWindowPromiseOnRegisterCallback> cb = + new ServiceWorkerResolveWindowPromiseOnRegisterCallback(); + + RefPtr<ServiceWorkerRegisterJob> job = new ServiceWorkerRegisterJob( + principal, aScopeURL, aScriptURL, + static_cast<ServiceWorkerUpdateViaCache>(aUpdateViaCache)); + + job->AppendResultCallback(cb); + queue->ScheduleJob(job); + + MOZ_ASSERT(NS_IsMainThread()); + + return cb->Promise(); +} + +/* + * Implements the async aspects of the getRegistrations algorithm. + */ +class GetRegistrationsRunnable final : public Runnable { + const ClientInfo mClientInfo; + RefPtr<ServiceWorkerRegistrationListPromise::Private> mPromise; + + public: + explicit GetRegistrationsRunnable(const ClientInfo& aClientInfo) + : Runnable("dom::ServiceWorkerManager::GetRegistrationsRunnable"), + mClientInfo(aClientInfo), + mPromise(new ServiceWorkerRegistrationListPromise::Private(__func__)) {} + + RefPtr<ServiceWorkerRegistrationListPromise> Promise() const { + return mPromise; + } + + NS_IMETHOD + Run() override { + auto scopeExit = MakeScopeExit( + [&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return NS_OK; + } + + auto principalOrErr = mClientInfo.GetPrincipal(); + if (NS_WARN_IF(principalOrErr.isErr())) { + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + nsTArray<ServiceWorkerRegistrationDescriptor> array; + + if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsContentPrincipal())) { + return NS_OK; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(principal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + scopeExit.release(); + mPromise->Resolve(array, __func__); + return NS_OK; + } + + for (uint32_t i = 0; i < data->mScopeContainer.Length(); ++i) { + RefPtr<ServiceWorkerRegistrationInfo> info = + data->mInfos.GetWeak(data->mScopeContainer[i]); + + NS_ConvertUTF8toUTF16 scope(data->mScopeContainer[i]); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + // Unfortunately we don't seem to have an obvious window id here; in + // particular ClientInfo does not have one, and neither do service worker + // registrations, as far as I can tell. + rv = principal->CheckMayLoadWithReporting( + scopeURI, false /* allowIfInheritsPrincipal */, + 0 /* innerWindowID */); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + array.AppendElement(info->Descriptor()); + } + + scopeExit.release(); + mPromise->Resolve(array, __func__); + + return NS_OK; + } +}; + +RefPtr<ServiceWorkerRegistrationListPromise> +ServiceWorkerManager::GetRegistrations(const ClientInfo& aClientInfo) const { + RefPtr<GetRegistrationsRunnable> runnable = + new GetRegistrationsRunnable(aClientInfo); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable)); + return runnable->Promise(); +} + +/* + * Implements the async aspects of the getRegistration algorithm. + */ +class GetRegistrationRunnable final : public Runnable { + const ClientInfo mClientInfo; + RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise; + nsCString mURL; + + public: + GetRegistrationRunnable(const ClientInfo& aClientInfo, const nsACString& aURL) + : Runnable("dom::ServiceWorkerManager::GetRegistrationRunnable"), + mClientInfo(aClientInfo), + mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)), + mURL(aURL) {} + + RefPtr<ServiceWorkerRegistrationPromise> Promise() const { return mPromise; } + + NS_IMETHOD + Run() override { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); + return NS_OK; + } + + auto principalOrErr = mClientInfo.GetPrincipal(); + if (NS_WARN_IF(principalOrErr.isErr())) { + mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromise->Reject(rv, __func__); + return NS_OK; + } + + // Unfortunately we don't seem to have an obvious window id here; in + // particular ClientInfo does not have one, and neither do service worker + // registrations, as far as I can tell. + rv = principal->CheckMayLoadWithReporting( + uri, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */); + if (NS_FAILED(rv)) { + mPromise->Reject(NS_ERROR_DOM_SECURITY_ERR, __func__); + return NS_OK; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetServiceWorkerRegistrationInfo(principal, uri); + + if (!registration) { + // Reject with NS_OK means "not found". + mPromise->Reject(NS_OK, __func__); + return NS_OK; + } + + mPromise->Resolve(registration->Descriptor(), __func__); + + return NS_OK; + } +}; + +RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::GetRegistration( + const ClientInfo& aClientInfo, const nsACString& aURL) const { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<GetRegistrationRunnable> runnable = + new GetRegistrationRunnable(aClientInfo, aURL); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable)); + + return runnable->Promise(); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, + const nsACString& aScope, + const nsTArray<uint8_t>& aDataBytes, + uint8_t optional_argc) { + if (optional_argc == 1) { + // This does one copy here (while constructing the Maybe) and another when + // we end up copying into the SendPushEventRunnable. We could fix that to + // only do one copy by making things between here and there take + // Maybe<nsTArray<uint8_t>>&&, but then we'd need to copy before we know + // whether we really need to in PushMessageDispatcher::NotifyWorkers. Since + // in practice this only affects JS callers that pass data, and we don't + // have any right now, let's not worry about it. + return SendPushEvent(aOriginAttributes, aScope, u""_ns, + Some(aDataBytes.Clone())); + } + MOZ_ASSERT(optional_argc == 0); + return SendPushEvent(aOriginAttributes, aScope, u""_ns, Nothing()); +} + +nsresult ServiceWorkerManager::SendPushEvent( + const nsACString& aOriginAttributes, const nsACString& aScope, + const nsAString& aMessageId, const Maybe<nsTArray<uint8_t>>& aData) { + OriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr<nsIPrincipal> principal; + MOZ_TRY_VAR(principal, ScopeToPrincipal(aScope, attrs)); + + // The registration handling a push notification must have an exact scope + // match. This will try to find an exact match, unlike how fetch may find the + // registration with the longest scope that's a prefix of the fetched URL. + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(principal, aScope); + if (NS_WARN_IF(!registration)) { + return NS_ERROR_FAILURE; + } + + MOZ_DIAGNOSTIC_ASSERT(registration->Scope().Equals(aScope)); + + ServiceWorkerInfo* serviceWorker = registration->GetActive(); + if (NS_WARN_IF(!serviceWorker)) { + return NS_ERROR_FAILURE; + } + + return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData, + registration); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendPushSubscriptionChangeEvent( + const nsACString& aOriginAttributes, const nsACString& aScope) { + OriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); + if (!info) { + return NS_ERROR_FAILURE; + } + return info->WorkerPrivate()->SendPushSubscriptionChangeEvent(); +} + +nsresult ServiceWorkerManager::SendNotificationEvent( + const nsAString& aEventName, const nsACString& aOriginSuffix, + const nsACString& aScope, const nsAString& aID, const nsAString& aTitle, + const nsAString& aDir, const nsAString& aLang, const nsAString& aBody, + const nsAString& aTag, const nsAString& aIcon, const nsAString& aData, + const nsAString& aBehavior) { + OriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginSuffix)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); + if (!info) { + return NS_ERROR_FAILURE; + } + + ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate(); + return workerPrivate->SendNotificationEvent( + aEventName, aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData, + aBehavior, NS_ConvertUTF8toUTF16(aScope)); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendNotificationClickEvent( + const nsACString& aOriginSuffix, const nsACString& aScope, + const nsAString& aID, const nsAString& aTitle, const nsAString& aDir, + const nsAString& aLang, const nsAString& aBody, const nsAString& aTag, + const nsAString& aIcon, const nsAString& aData, + const nsAString& aBehavior) { + return SendNotificationEvent(nsLiteralString(NOTIFICATION_CLICK_EVENT_NAME), + aOriginSuffix, aScope, aID, aTitle, aDir, aLang, + aBody, aTag, aIcon, aData, aBehavior); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendNotificationCloseEvent( + const nsACString& aOriginSuffix, const nsACString& aScope, + const nsAString& aID, const nsAString& aTitle, const nsAString& aDir, + const nsAString& aLang, const nsAString& aBody, const nsAString& aTag, + const nsAString& aIcon, const nsAString& aData, + const nsAString& aBehavior) { + return SendNotificationEvent(nsLiteralString(NOTIFICATION_CLOSE_EVENT_NAME), + aOriginSuffix, aScope, aID, aTitle, aDir, aLang, + aBody, aTag, aIcon, aData, aBehavior); +} + +RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::WhenReady( + const ClientInfo& aClientInfo) { + AssertIsOnMainThread(); + + for (auto& prd : mPendingReadyList) { + if (prd->mClientHandle->Info().Id() == aClientInfo.Id() && + prd->mClientHandle->Info().PrincipalInfo() == + aClientInfo.PrincipalInfo()) { + return prd->mPromise; + } + } + + RefPtr<ServiceWorkerRegistrationInfo> reg = + GetServiceWorkerRegistrationInfo(aClientInfo); + if (reg && reg->GetActive()) { + return ServiceWorkerRegistrationPromise::CreateAndResolve(reg->Descriptor(), + __func__); + } + + nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget(); + + RefPtr<ClientHandle> handle = + ClientManager::CreateHandle(aClientInfo, target); + mPendingReadyList.AppendElement(MakeUnique<PendingReadyData>(handle)); + + RefPtr<ServiceWorkerManager> self(this); + handle->OnDetach()->Then(target, __func__, + [self = std::move(self), aClientInfo] { + self->RemovePendingReadyPromise(aClientInfo); + }); + + return mPendingReadyList.LastElement()->mPromise; +} + +void ServiceWorkerManager::CheckPendingReadyPromises() { + nsTArray<UniquePtr<PendingReadyData>> pendingReadyList = + std::move(mPendingReadyList); + for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) { + UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i])); + + RefPtr<ServiceWorkerRegistrationInfo> reg = + GetServiceWorkerRegistrationInfo(prd->mClientHandle->Info()); + + if (reg && reg->GetActive()) { + prd->mPromise->Resolve(reg->Descriptor(), __func__); + } else { + mPendingReadyList.AppendElement(std::move(prd)); + } + } +} + +void ServiceWorkerManager::RemovePendingReadyPromise( + const ClientInfo& aClientInfo) { + nsTArray<UniquePtr<PendingReadyData>> pendingReadyList = + std::move(mPendingReadyList); + for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) { + UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i])); + + if (prd->mClientHandle->Info().Id() == aClientInfo.Id() && + prd->mClientHandle->Info().PrincipalInfo() == + aClientInfo.PrincipalInfo()) { + prd->mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + } else { + mPendingReadyList.AppendElement(std::move(prd)); + } + } +} + +void ServiceWorkerManager::NoteInheritedController( + const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aController) { + MOZ_ASSERT(NS_IsMainThread()); + + auto principalOrErr = PrincipalInfoToPrincipal(aController.PrincipalInfo()); + + if (NS_WARN_IF(principalOrErr.isErr())) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + nsCOMPtr<nsIURI> scope; + nsresult rv = NS_NewURI(getter_AddRefs(scope), aController.Scope()); + NS_ENSURE_SUCCESS_VOID(rv); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, scope); + NS_ENSURE_TRUE_VOID(registration); + NS_ENSURE_TRUE_VOID(registration->GetActive()); + + StartControllingClient(aClientInfo, registration, + false /* aControlClientHandle */); +} + +ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForScope( + const OriginAttributes& aOriginAttributes, const nsACString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); + if (NS_FAILED(rv)) { + return nullptr; + } + + auto result = ScopeToPrincipal(scopeURI, aOriginAttributes); + if (NS_WARN_IF(result.isErr())) { + return nullptr; + } + + auto principal = result.unwrap(); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, scopeURI); + if (!registration) { + return nullptr; + } + + return registration->GetActive(); +} + +namespace { + +class UnregisterJobCallback final : public ServiceWorkerJob::Callback { + nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback; + + ~UnregisterJobCallback() { MOZ_ASSERT(!mCallback); } + + public: + explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCallback); + } + + void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aJob); + MOZ_ASSERT(mCallback); + + auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; }); + + if (aStatus.Failed()) { + mCallback->UnregisterFailed(); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister); + RefPtr<ServiceWorkerUnregisterJob> unregisterJob = + static_cast<ServiceWorkerUnregisterJob*>(aJob); + mCallback->UnregisterSucceeded(unregisterJob->GetResult()); + } + + void JobDiscarded(ErrorResult&) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCallback); + + mCallback->UnregisterFailed(); + mCallback = nullptr; + } + + NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback, override) +}; + +} // anonymous namespace + +NS_IMETHODIMP +ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal, + nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aPrincipal) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + +// This is not accessible by content, and callers should always ensure scope is +// a correct URI, so this is wrapped in DEBUG +#ifdef DEBUG + nsCOMPtr<nsIURI> scopeURI; + rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } +#endif + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_ConvertUTF16toUTF8 scope(aScope); + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope); + + RefPtr<ServiceWorkerUnregisterJob> job = new ServiceWorkerUnregisterJob( + aPrincipal, scope, true /* send to parent */); + + if (aCallback) { + RefPtr<UnregisterJobCallback> cb = new UnregisterJobCallback(aCallback); + job->AppendResultCallback(cb); + } + + queue->ScheduleJob(job); + return NS_OK; +} + +void ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aWorker); + + RefPtr<ServiceWorkerRegistrationInfo> reg = + GetRegistration(aWorker->Principal(), aWorker->Scope()); + if (!reg) { + return; + } + + if (reg->GetActive() != aWorker) { + return; + } + + reg->TryToActivateAsync(); +} + +already_AddRefed<ServiceWorkerJobQueue> +ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey, + const nsACString& aScope) { + MOZ_ASSERT(!aKey.IsEmpty()); + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + // XXX we could use WithEntryHandle here to avoid a hashtable lookup, except + // that leads to a false positive assertion, see bug 1370674 comment 7. + if (!mRegistrationInfos.Get(aKey, &data)) { + data = mRegistrationInfos + .InsertOrUpdate(aKey, MakeUnique<RegistrationDataPerPrincipal>()) + .get(); + } + + RefPtr queue = data->mJobQueues.GetOrInsertNew(aScope); + return queue.forget(); +} + +/* static */ +already_AddRefed<ServiceWorkerManager> ServiceWorkerManager::GetInstance() { + if (!gInstance) { + RefPtr<ServiceWorkerRegistrar> swr; + + // XXX: Substitute this with an assertion. See comment in Init. + if (XRE_IsParentProcess()) { + // Don't (re-)create the ServiceWorkerManager if we are already shutting + // down. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return nullptr; + } + // Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar + // is initialized. + swr = ServiceWorkerRegistrar::Get(); + if (!swr) { + return nullptr; + } + } + + MOZ_ASSERT(NS_IsMainThread()); + + gInstance = new ServiceWorkerManager(); + gInstance->Init(swr); + ClearOnShutdown(&gInstance); + } + RefPtr<ServiceWorkerManager> copy = gInstance.get(); + return copy.forget(); +} + +void ServiceWorkerManager::ReportToAllClients( + const nsCString& aScope, const nsString& aMessage, + const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags) { + ConsoleUtils::ReportForServiceWorkerScope( + NS_ConvertUTF8toUTF16(aScope), aMessage, aFilename, aLineNumber, + aColumnNumber, ConsoleUtils::eError); +} + +/* static */ +void ServiceWorkerManager::LocalizeAndReportToAllClients( + const nsCString& aScope, const char* aStringKey, + const nsTArray<nsString>& aParamArray, uint32_t aFlags, + const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, + uint32_t aColumnNumber) { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return; + } + + nsresult rv; + nsAutoString message; + rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, + aStringKey, aParamArray, message); + if (NS_SUCCEEDED(rv)) { + swm->ReportToAllClients(aScope, message, aFilename, aLine, aLineNumber, + aColumnNumber, aFlags); + } else { + NS_WARNING("Failed to format and therefore report localized error."); + } +} + +void ServiceWorkerManager::HandleError( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsCString& aScope, + const nsString& aWorkerURL, const nsString& aMessage, + const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (NS_WARN_IF(!mRegistrationInfos.Get(scopeKey, &data))) { + return; + } + + // Always report any uncaught exceptions or errors to the console of + // each client. + ReportToAllClients(aScope, aMessage, aFilename, aLine, aLineNumber, + aColumnNumber, aFlags); +} + +void ServiceWorkerManager::LoadRegistration( + const ServiceWorkerRegistrationData& aRegistration) { + MOZ_ASSERT(NS_IsMainThread()); + + auto principalOrErr = PrincipalInfoToPrincipal(aRegistration.principal()); + if (NS_WARN_IF(principalOrErr.isErr())) { + return; + } + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + // Purge extensions registrations if they are disabled by prefs. + if (!StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) { + nsCOMPtr<nsIURI> uri = principal->GetURI(); + + // We do check the URI scheme here because when this is going to run + // the extension may not have been loaded yet and the WebExtensionPolicy + // may not exist yet. + if (uri->SchemeIs("moz-extension")) { + const auto& cacheName = aRegistration.cacheName(); + serviceWorkerScriptCache::PurgeCache(principal, cacheName); + return; + } + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(principal, aRegistration.scope()); + if (!registration) { + registration = + CreateNewRegistration(aRegistration.scope(), principal, + static_cast<ServiceWorkerUpdateViaCache>( + aRegistration.updateViaCache()), + aRegistration.navigationPreloadState()); + } else { + // If active worker script matches our expectations for a "current worker", + // then we are done. Since scripts with the same URL might have different + // contents such as updated scripts or scripts with different LoadFlags, we + // use the CacheName to judge whether the two scripts are identical, where + // the CacheName is an UUID generated when a new script is found. + if (registration->GetActive() && + registration->GetActive()->CacheName() == aRegistration.cacheName()) { + // No needs for updates. + return; + } + } + + registration->SetLastUpdateTime(aRegistration.lastUpdateTime()); + + nsLoadFlags importsLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + if (aRegistration.updateViaCache() != + static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None)) { + importsLoadFlags |= nsIRequest::VALIDATE_ALWAYS; + } + + const nsCString& currentWorkerURL = aRegistration.currentWorkerURL(); + if (!currentWorkerURL.IsEmpty()) { + registration->SetActive(new ServiceWorkerInfo( + registration->Principal(), registration->Scope(), registration->Id(), + registration->Version(), currentWorkerURL, aRegistration.cacheName(), + importsLoadFlags)); + registration->GetActive()->SetHandlesFetch( + aRegistration.currentWorkerHandlesFetch()); + registration->GetActive()->SetInstalledTime( + aRegistration.currentWorkerInstalledTime()); + registration->GetActive()->SetActivatedTime( + aRegistration.currentWorkerActivatedTime()); + } +} + +void ServiceWorkerManager::LoadRegistrations( + const nsTArray<ServiceWorkerRegistrationData>& aRegistrations) { + MOZ_ASSERT(NS_IsMainThread()); + uint32_t fetch = 0; + for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) { + LoadRegistration(aRegistrations[i]); + if (aRegistrations[i].currentWorkerHandlesFetch()) { + fetch++; + } + } + gServiceWorkersRegistered = aRegistrations.Length(); + gServiceWorkersRegisteredFetch = fetch; + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS, + u"All"_ns, gServiceWorkersRegistered); + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS, + u"Fetch"_ns, gServiceWorkersRegisteredFetch); + LOG(("LoadRegistrations: %u, fetch %u\n", gServiceWorkersRegistered, + gServiceWorkersRegisteredFetch)); +} + +void ServiceWorkerManager::StoreRegistration( + nsIPrincipal* aPrincipal, ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aRegistration); + + if (mShuttingDown) { + return; + } + + // Do not store a registration for addons that are not installed, not enabled + // or installed temporarily. + // + // If the dom.serviceWorkers.testing.persistTemporaryInstalledAddons is set + // to true, the registration for a temporary installed addon will still be + // persisted (only meant to be used to make it easier to test some particular + // scenario with a temporary installed addon which doesn't need to be signed + // to be installed on release channel builds). + if (aPrincipal->SchemeIs("moz-extension")) { + RefPtr<extensions::WebExtensionPolicy> addonPolicy = + BasePrincipal::Cast(aPrincipal)->AddonPolicy(); + if (!addonPolicy || !addonPolicy->Active() || + (addonPolicy->TemporarilyInstalled() && + !StaticPrefs:: + dom_serviceWorkers_testing_persistTemporarilyInstalledAddons())) { + return; + } + } + + ServiceWorkerRegistrationData data; + nsresult rv = PopulateRegistrationData(aPrincipal, aRegistration, data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + PrincipalInfo principalInfo; + if (NS_WARN_IF( + NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) { + return; + } + + mActor->SendRegister(data); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo( + const ClientInfo& aClientInfo) const { + auto principalOrErr = aClientInfo.GetPrincipal(); + if (NS_WARN_IF(principalOrErr.isErr())) { + return nullptr; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aClientInfo.URL()); + NS_ENSURE_SUCCESS(rv, nullptr); + + return GetServiceWorkerRegistrationInfo(principal, uri); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, + nsIURI* aURI) const { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_FAILED(rv)) { + return nullptr; + } + + return GetServiceWorkerRegistrationInfo(scopeKey, aURI); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo( + const nsACString& aScopeKey, nsIURI* aURI) const { + MOZ_ASSERT(aURI); + + nsAutoCString spec; + nsresult rv = aURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsAutoCString scope; + RegistrationDataPerPrincipal* data; + if (!FindScopeForPath(aScopeKey, spec, &data, scope)) { + return nullptr; + } + + MOZ_ASSERT(data); + + RefPtr<ServiceWorkerRegistrationInfo> registration; + data->mInfos.Get(scope, getter_AddRefs(registration)); + // ordered scopes and registrations better be in sync. + MOZ_ASSERT(registration); + +#ifdef DEBUG + nsAutoCString origin; + rv = registration->Principal()->GetOrigin(origin); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(origin.Equals(aScopeKey)); +#endif + + return registration.forget(); +} + +/* static */ +nsresult ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal, + nsACString& aKey) { + MOZ_ASSERT(aPrincipal); + + if (!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal()) { + return NS_ERROR_FAILURE; + } + + nsresult rv = aPrincipal->GetOrigin(aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +/* static */ +nsresult ServiceWorkerManager::PrincipalInfoToScopeKey( + const PrincipalInfo& aPrincipalInfo, nsACString& aKey) { + if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo) { + return NS_ERROR_FAILURE; + } + + auto content = aPrincipalInfo.get_ContentPrincipalInfo(); + + nsAutoCString suffix; + content.attrs().CreateSuffix(suffix); + + aKey = content.originNoSuffix(); + aKey.Append(suffix); + + return NS_OK; +} + +/* static */ +void ServiceWorkerManager::AddScopeAndRegistration( + const nsACString& aScope, ServiceWorkerRegistrationInfo* aInfo) { + MOZ_ASSERT(aInfo); + MOZ_ASSERT(aInfo->Principal()); + MOZ_ASSERT(!aInfo->IsUnregistered()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(aInfo->Principal(), scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + MOZ_ASSERT(!scopeKey.IsEmpty()); + + auto* const data = swm->mRegistrationInfos.GetOrInsertNew(scopeKey); + data->mScopeContainer.InsertScope(aScope); + data->mInfos.InsertOrUpdate(aScope, RefPtr{aInfo}); + swm->NotifyListenersOnRegister(aInfo); +} + +/* static */ +bool ServiceWorkerManager::FindScopeForPath( + const nsACString& aScopeKey, const nsACString& aPath, + RegistrationDataPerPrincipal** aData, nsACString& aMatch) { + MOZ_ASSERT(aData); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + if (!swm || !swm->mRegistrationInfos.Get(aScopeKey, aData)) { + return false; + } + + Maybe<nsCString> scope = (*aData)->mScopeContainer.MatchScope(aPath); + + if (scope) { + // scope.isSome() will still truen true after this; we are just moving the + // string inside the Maybe, so the Maybe will contain an empty string. + aMatch = std::move(*scope); + } + + return scope.isSome(); +} + +/* static */ +bool ServiceWorkerManager::HasScope(nsIPrincipal* aPrincipal, + const nsACString& aScope) { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return false; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + return false; + } + + return data->mScopeContainer.Contains(aScope); +} + +/* static */ +void ServiceWorkerManager::RemoveScopeAndRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(aRegistration->Principal(), scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + if (auto entry = data->mUpdateTimers.Lookup(aRegistration->Scope())) { + entry.Data()->Cancel(); + entry.Remove(); + } + + // Verify there are no controlled clients for the purged registration. + for (auto iter = swm->mControlledClients.Iter(); !iter.Done(); iter.Next()) { + auto& reg = iter.UserData()->mRegistrationInfo; + if (reg->Scope().Equals(aRegistration->Scope()) && + reg->Principal()->Equals(aRegistration->Principal()) && + reg->IsCorrupt()) { + iter.Remove(); + } + } + + RefPtr<ServiceWorkerRegistrationInfo> info; + data->mInfos.Remove(aRegistration->Scope(), getter_AddRefs(info)); + aRegistration->SetUnregistered(); + data->mScopeContainer.RemoveScope(aRegistration->Scope()); + swm->NotifyListenersOnUnregister(info); + + swm->MaybeRemoveRegistrationInfo(scopeKey); +} + +void ServiceWorkerManager::MaybeRemoveRegistrationInfo( + const nsACString& aScopeKey) { + if (auto entry = mRegistrationInfos.Lookup(aScopeKey)) { + if (entry.Data()->mScopeContainer.IsEmpty() && + entry.Data()->mJobQueues.Count() == 0) { + entry.Remove(); + + // Need to reset the mQuotaUsageCheckCount, if + // RegistrationDataPerPrincipal:: mScopeContainer is empty. This + // RegistrationDataPerPrincipal might be reused, such that quota usage + // mitigation can be triggered for the new added registration. + } else if (entry.Data()->mScopeContainer.IsEmpty() && + entry.Data()->mQuotaUsageCheckCount) { + entry.Data()->mQuotaUsageCheckCount = 0; + } + } +} + +bool ServiceWorkerManager::StartControlling( + const ClientInfo& aClientInfo, + const ServiceWorkerDescriptor& aServiceWorker) { + MOZ_ASSERT(NS_IsMainThread()); + + auto principalOrErr = + PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo()); + + if (NS_WARN_IF(principalOrErr.isErr())) { + return false; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + nsCOMPtr<nsIURI> scope; + nsresult rv = NS_NewURI(getter_AddRefs(scope), aServiceWorker.Scope()); + NS_ENSURE_SUCCESS(rv, false); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, scope); + NS_ENSURE_TRUE(registration, false); + NS_ENSURE_TRUE(registration->GetActive(), false); + + StartControllingClient(aClientInfo, registration); + + return true; +} + +void ServiceWorkerManager::MaybeCheckNavigationUpdate( + const ClientInfo& aClientInfo) { + MOZ_ASSERT(NS_IsMainThread()); + // We perform these success path navigation update steps when the + // document tells us its more or less done loading. This avoids + // slowing down page load and also lets pages consistently get + // updatefound events when they fire. + // + // 9.8.20 If respondWithEntered is false, then: + // 9.8.22 Else: (respondWith was entered and succeeded) + // If request is a non-subresource request, then: Invoke Soft Update + // algorithm. + ControlledClientData* data = mControlledClients.Get(aClientInfo.Id()); + if (data && data->mRegistrationInfo) { + data->mRegistrationInfo->MaybeScheduleUpdate(); + } +} + +void ServiceWorkerManager::StopControllingRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + aRegistration->StopControllingClient(); + if (aRegistration->IsControllingClients()) { + return; + } + + if (aRegistration->IsUnregistered()) { + if (aRegistration->IsIdle()) { + aRegistration->Clear(); + } else { + aRegistration->ClearWhenIdle(); + } + return; + } + + // We use to aggressively terminate the worker at this point, but it + // caused problems. There are more uses for a service worker than actively + // controlled documents. We need to let the worker naturally terminate + // in case its handling push events, message events, etc. + aRegistration->TryToActivateAsync(); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetScopeForUrl(nsIPrincipal* aPrincipal, + const nsAString& aUrl, nsAString& aScope) { + MOZ_ASSERT(aPrincipal); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> r = + GetServiceWorkerRegistrationInfo(aPrincipal, uri); + if (!r) { + return NS_ERROR_FAILURE; + } + + CopyUTF8toUTF16(r->Scope(), aScope); + return NS_OK; +} + +namespace { + +class ContinueDispatchFetchEventRunnable : public Runnable { + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + nsCOMPtr<nsIInterceptedChannel> mChannel; + nsCOMPtr<nsILoadGroup> mLoadGroup; + + public: + ContinueDispatchFetchEventRunnable( + ServiceWorkerPrivate* aServiceWorkerPrivate, + nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup) + : Runnable( + "dom::ServiceWorkerManager::ContinueDispatchFetchEventRunnable"), + mServiceWorkerPrivate(aServiceWorkerPrivate), + mChannel(aChannel), + mLoadGroup(aLoadGroup) { + MOZ_ASSERT(aServiceWorkerPrivate); + MOZ_ASSERT(aChannel); + } + + void HandleError() { + MOZ_ASSERT(NS_IsMainThread()); + NS_WARNING("Unexpected error while dispatching fetch event!"); + nsresult rv = mChannel->ResetInterception(false); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to resume intercepted network request"); + mChannel->CancelInterception(rv); + } + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIChannel> channel; + nsresult rv = mChannel->GetChannel(getter_AddRefs(channel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleError(); + return NS_OK; + } + + // The channel might have encountered an unexpected error while ensuring + // the upload stream is cloneable. Check here and reset the interception + // if that happens. + nsresult status; + rv = channel->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) { + HandleError(); + return NS_OK; + } + + nsString clientId; + nsString resultingClientId; + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo(); + if (clientInfo.isSome()) { + clientId = NSID_TrimBracketsUTF16(clientInfo->Id()); + } + + // Having an initial or reserved client are mutually exclusive events: + // either an initial client is used upon navigating an about:blank + // iframe, or a new, reserved environment/client is created (e.g. + // upon a top-level navigation). See step 4 of + // https://html.spec.whatwg.org/#process-a-navigate-fetch as well as + // https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444 + Maybe<ClientInfo> resulting = loadInfo->GetInitialClientInfo(); + + if (resulting.isNothing()) { + resulting = loadInfo->GetReservedClientInfo(); + } else { + MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing()); + } + + if (resulting.isSome()) { + resultingClientId = NSID_TrimBracketsUTF16(resulting->Id()); + } + + rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId, + resultingClientId); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleError(); + } + + return NS_OK; + } +}; + +} // anonymous namespace + +void ServiceWorkerManager::DispatchFetchEvent(nsIInterceptedChannel* aChannel, + ErrorResult& aRv) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsIChannel> internalChannel; + aRv = aChannel->GetChannel(getter_AddRefs(internalChannel)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsILoadGroup> loadGroup; + aRv = internalChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsILoadInfo> loadInfo = internalChannel->LoadInfo(); + RefPtr<ServiceWorkerInfo> serviceWorker; + + if (!nsContentUtils::IsNonSubresourceRequest(internalChannel)) { + const Maybe<ServiceWorkerDescriptor>& controller = + loadInfo->GetController(); + if (NS_WARN_IF(controller.isNothing())) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + nsresult rv = GetClientRegistration(loadInfo->GetClientInfo().ref(), + getter_AddRefs(registration)); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + + serviceWorker = registration->GetActive(); + if (NS_WARN_IF(!serviceWorker) || + NS_WARN_IF(serviceWorker->Descriptor().Id() != controller.ref().Id())) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } else { + nsCOMPtr<nsIURI> uri; + aRv = aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // non-subresource request means the URI contains the principal + OriginAttributes attrs = loadInfo->GetOriginAttributes(); + if (StaticPrefs::privacy_partition_serviceWorkers()) { + StoragePrincipalHelper::GetOriginAttributes( + internalChannel, attrs, + StoragePrincipalHelper::eForeignPartitionedPrincipal); + } + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, attrs); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, uri); + if (NS_WARN_IF(!registration)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // While we only enter this method if IsAvailable() previously saw + // an active worker, it is possible for that worker to be removed + // before we get to this point. Therefore we must handle a nullptr + // active worker here. + serviceWorker = registration->GetActive(); + if (NS_WARN_IF(!serviceWorker)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // If there is a reserved client it should be marked as controlled before + // the FetchEvent is dispatched. + Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo(); + + // Also override the initial about:blank controller since the real + // network load may be intercepted by a different service worker. If + // the intial about:blank has a controller here its simply been + // inherited from its parent. + if (clientInfo.isNothing()) { + clientInfo = loadInfo->GetInitialClientInfo(); + + // TODO: We need to handle the case where the initial about:blank is + // controlled, but the final document load is not. Right now + // the spec does not really say what to do. There currently + // is no way for the controller to be cleared from a client in + // the spec or our implementation. We may want to force a + // new inner window to be created instead of reusing the + // initial about:blank global. See bug 1419620 and the spec + // issue here: https://github.com/w3c/ServiceWorker/issues/1232 + } + + if (clientInfo.isSome()) { + // ClientChannelHelper is not called for STS upgrades that get + // intercepted by a service worker when interception occurs in + // the content process. Therefore the reserved client is not + // properly cleared in that case leading to a situation where + // a ClientSource with an http:// principal is controlled by + // a ServiceWorker with an https:// principal. + // + // This does not occur when interception is handled by the + // simpler InterceptedHttpChannel approach in the parent. + // + // As a temporary work around check for this principal mismatch + // here and perform the ClientChannelHelper's replacement of + // reserved client automatically. + if (!XRE_IsParentProcess()) { + auto clientPrincipalOrErr = clientInfo.ref().GetPrincipal(); + + nsCOMPtr<nsIPrincipal> clientPrincipal; + if (clientPrincipalOrErr.isOk()) { + clientPrincipal = clientPrincipalOrErr.unwrap(); + } + + if (!clientPrincipal || !clientPrincipal->Equals(principal)) { + UniquePtr<ClientSource> reservedClient = + loadInfo->TakeReservedClientSource(); + + nsCOMPtr<nsISerialEventTarget> target = + reservedClient ? reservedClient->EventTarget() + : GetMainThreadSerialEventTarget(); + + reservedClient.reset(); + reservedClient = ClientManager::CreateSource(ClientType::Window, + target, principal); + + loadInfo->GiveReservedClientSource(std::move(reservedClient)); + + clientInfo = loadInfo->GetReservedClientInfo(); + } + } + + // First, attempt to mark the reserved client controlled directly. This + // will update the controlled status in the ClientManagerService in the + // parent. It will also eventually propagate back to the ClientSource. + StartControllingClient(clientInfo.ref(), registration); + } + + uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL; + nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(internalChannel); + MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode)); + + // Synthetic redirects for non-subresource requests with a "follow" + // redirect mode may switch controllers. This is basically worker + // scripts right now. In this case we need to explicitly clear the + // controller to avoid assertions on the SetController() below. + if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) { + loadInfo->ClearController(); + } + + // But we also note the reserved state on the LoadInfo. This allows the + // ClientSource to be updated immediately after the nsIChannel starts. + // This is necessary to have the correct controller in place for immediate + // follow-on requests. + loadInfo->SetController(serviceWorker->Descriptor()); + } + + MOZ_DIAGNOSTIC_ASSERT(serviceWorker); + + RefPtr<ContinueDispatchFetchEventRunnable> continueRunnable = + new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(), + aChannel, loadGroup); + + // When this service worker was registered, we also sent down the permissions + // for the runnable. They should have arrived by now, but we still need to + // wait for them if they have not. + RefPtr<PermissionManager> permMgr = PermissionManager::GetInstance(); + if (permMgr) { + permMgr->WhenPermissionsAvailable(serviceWorker->Principal(), + continueRunnable); + } else { + continueRunnable->HandleError(); + } +} + +bool ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI, + nsIChannel* aChannel) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + MOZ_ASSERT(aChannel); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(aPrincipal, aURI); + + if (!registration || !registration->GetActive()) { + return false; + } + + // Checking if the matched service worker handles fetch events or not. + // If it does, directly return true and handle the client controlling logic + // in DispatchFetchEvent(). otherwise, do followings then return false. + // 1. Set the matched service worker as the controller of LoadInfo and + // correspoinding ClinetInfo + // 2. Maybe schedule a soft update + if (!registration->GetActive()->HandlesFetch()) { + // Checkin if the channel is not allowed for the service worker. + auto storageAccess = StorageAllowedForChannel(aChannel); + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + if (storageAccess != StorageAccess::eAllow) { + if (!StaticPrefs::privacy_partition_serviceWorkers()) { + return false; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + + if (!StoragePartitioningEnabled(storageAccess, cookieJarSettings)) { + return false; + } + } + + // ServiceWorkerInterceptController::ShouldPrepareForIntercept() handles the + // subresource cases. Must be non-subresource case here. + MOZ_ASSERT(nsContentUtils::IsNonSubresourceRequest(aChannel)); + + Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo(); + if (clientInfo.isNothing()) { + clientInfo = loadInfo->GetInitialClientInfo(); + } + + if (clientInfo.isSome()) { + StartControllingClient(clientInfo.ref(), registration); + } + + uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL; + nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(aChannel); + MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode)); + + // Synthetic redirects for non-subresource requests with a "follow" + // redirect mode may switch controllers. This is basically worker + // scripts right now. In this case we need to explicitly clear the + // controller to avoid assertions on the SetController() below. + if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) { + loadInfo->ClearController(); + } + + loadInfo->SetController(registration->GetActive()->Descriptor()); + + // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm 17.1 + // try schedule a soft-update for non-subresource case. + registration->MaybeScheduleUpdate(); + return false; + } + // Found a matching service worker which handles fetch events, return true. + return true; +} + +nsresult ServiceWorkerManager::GetClientRegistration( + const ClientInfo& aClientInfo, + ServiceWorkerRegistrationInfo** aRegistrationInfo) { + ControlledClientData* data = mControlledClients.Get(aClientInfo.Id()); + if (!data || !data->mRegistrationInfo) { + return NS_ERROR_NOT_AVAILABLE; + } + + // If the document is controlled, the current worker MUST be non-null. + if (!data->mRegistrationInfo->GetActive()) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<ServiceWorkerRegistrationInfo> ref = data->mRegistrationInfo; + ref.forget(aRegistrationInfo); + return NS_OK; +} + +int32_t ServiceWorkerManager::GetPrincipalQuotaUsageCheckCount( + nsIPrincipal* aPrincipal) { + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return -1; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return -1; + } + + return data->mQuotaUsageCheckCount; +} + +void ServiceWorkerManager::CheckPrincipalQuotaUsage(nsIPrincipal* aPrincipal, + const nsACString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + // Had already schedule a quota usage check. + if (data->mQuotaUsageCheckCount != 0) { + return; + } + + ++data->mQuotaUsageCheckCount; + + // Get the corresponding ServiceWorkerRegistrationInfo here. Unregisteration + // might be triggered later, should get it here before it be removed from + // data.mInfos, such that NotifyListenersOnQuotaCheckFinish() can notify the + // corresponding ServiceWorkerRegistrationInfo after asynchronous quota + // checking finish. + RefPtr<ServiceWorkerRegistrationInfo> info; + data->mInfos.Get(aScope, getter_AddRefs(info)); + MOZ_ASSERT(info); + + RefPtr<ServiceWorkerManager> self = this; + + ClearQuotaUsageIfNeeded(aPrincipal, [self, info](bool aResult) { + MOZ_ASSERT(NS_IsMainThread()); + self->NotifyListenersOnQuotaUsageCheckFinish(info); + }); +} + +void ServiceWorkerManager::SoftUpdate(const OriginAttributes& aOriginAttributes, + const nsACString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mShuttingDown) { + return; + } + + SoftUpdateInternal(aOriginAttributes, aScope, nullptr); +} + +namespace { + +class UpdateJobCallback final : public ServiceWorkerJob::Callback { + RefPtr<ServiceWorkerUpdateFinishCallback> mCallback; + + ~UpdateJobCallback() { MOZ_ASSERT(!mCallback); } + + public: + explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCallback); + } + + void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aJob); + MOZ_ASSERT(mCallback); + + auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; }); + + if (aStatus.Failed()) { + mCallback->UpdateFailed(aStatus); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update); + RefPtr<ServiceWorkerUpdateJob> updateJob = + static_cast<ServiceWorkerUpdateJob*>(aJob); + RefPtr<ServiceWorkerRegistrationInfo> reg = updateJob->GetRegistration(); + mCallback->UpdateSucceeded(reg); + } + + void JobDiscarded(ErrorResult& aStatus) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCallback); + + mCallback->UpdateFailed(aStatus); + mCallback = nullptr; + } + + NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback, override) +}; + +} // anonymous namespace + +void ServiceWorkerManager::SoftUpdateInternal( + const OriginAttributes& aOriginAttributes, const nsACString& aScope, + ServiceWorkerUpdateFinishCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mShuttingDown) { + return; + } + + auto result = ScopeToPrincipal(aScope, aOriginAttributes); + if (NS_WARN_IF(result.isErr())) { + return; + } + + auto principal = result.unwrap(); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(principal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(scopeKey, aScope); + if (NS_WARN_IF(!registration)) { + return; + } + + // "If registration's installing worker is not null, abort these steps." + if (registration->GetInstalling()) { + return; + } + + // "Let newestWorker be the result of running Get Newest Worker algorithm + // passing registration as its argument. + // If newestWorker is null, abort these steps." + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (!newest) { + return; + } + + // "If the registration queue for registration is empty, invoke Update + // algorithm, or its equivalent, with client, registration as its argument." + // TODO(catalinb): We don't implement the force bypass cache flag. + // See: https://github.com/slightlyoff/ServiceWorker/issues/759 + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope); + + RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob( + principal, registration->Scope(), newest->ScriptSpec(), + registration->GetUpdateViaCache()); + + if (aCallback) { + RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback); + job->AppendResultCallback(cb); + } + + queue->ScheduleJob(job); +} + +void ServiceWorkerManager::Update( + nsIPrincipal* aPrincipal, const nsACString& aScope, + nsCString aNewestWorkerScriptUrl, + ServiceWorkerUpdateFinishCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty()); + + UpdateInternal(aPrincipal, aScope, std::move(aNewestWorkerScriptUrl), + aCallback); +} + +void ServiceWorkerManager::UpdateInternal( + nsIPrincipal* aPrincipal, const nsACString& aScope, + nsCString&& aNewestWorkerScriptUrl, + ServiceWorkerUpdateFinishCallback* aCallback) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty()); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(scopeKey, aScope); + if (NS_WARN_IF(!registration)) { + ErrorResult error; + error.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(aScope, "uninstalled"); + aCallback->UpdateFailed(error); + + // In case the callback does not consume the exception + error.SuppressException(); + return; + } + + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope); + + // "Let job be the result of running Create Job with update, registration’s + // scope url, newestWorker’s script url, promise, and the context object’s + // relevant settings object." + RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob( + aPrincipal, registration->Scope(), std::move(aNewestWorkerScriptUrl), + registration->GetUpdateViaCache()); + + RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback); + job->AppendResultCallback(cb); + + // "Invoke Schedule Job with job." + queue->ScheduleJob(job); +} + +RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient( + const ClientInfo& aClientInfo, + ServiceWorkerRegistrationInfo* aWorkerRegistration) { + MOZ_DIAGNOSTIC_ASSERT(aWorkerRegistration); + + if (!aWorkerRegistration->GetActive()) { + CopyableErrorResult rv; + rv.ThrowInvalidStateError("Worker is not active"); + return GenericErrorResultPromise::CreateAndReject(rv, __func__); + } + + // Same origin check + auto principalOrErr = aClientInfo.GetPrincipal(); + + if (NS_WARN_IF(principalOrErr.isErr())) { + CopyableErrorResult rv; + rv.ThrowSecurityError("Could not extract client's principal"); + return GenericErrorResultPromise::CreateAndReject(rv, __func__); + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + if (!aWorkerRegistration->Principal()->Equals(principal)) { + CopyableErrorResult rv; + rv.ThrowSecurityError("Worker is for a different origin"); + return GenericErrorResultPromise::CreateAndReject(rv, __func__); + } + + // The registration that should be controlling the client + RefPtr<ServiceWorkerRegistrationInfo> matchingRegistration = + GetServiceWorkerRegistrationInfo(aClientInfo); + + // The registration currently controlling the client + RefPtr<ServiceWorkerRegistrationInfo> controllingRegistration; + GetClientRegistration(aClientInfo, getter_AddRefs(controllingRegistration)); + + if (aWorkerRegistration != matchingRegistration || + aWorkerRegistration == controllingRegistration) { + return GenericErrorResultPromise::CreateAndResolve(true, __func__); + } + + return StartControllingClient(aClientInfo, aWorkerRegistration); +} + +RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient( + const ClientInfo& aClientInfo, + const ServiceWorkerDescriptor& aServiceWorker) { + auto principalOrErr = aServiceWorker.GetPrincipal(); + if (NS_WARN_IF(principalOrErr.isErr())) { + return GenericErrorResultPromise::CreateAndResolve(false, __func__); + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(principal, aServiceWorker.Scope()); + + // While ServiceWorkerManager is distributed across child processes its + // possible for us to sometimes get a claim for a new worker that has + // not propagated to this process yet. For now, simply note that we + // are done. The fix for this is to move the SWM to the parent process + // so there are no consistency errors. + if (NS_WARN_IF(!registration) || NS_WARN_IF(!registration->GetActive())) { + return GenericErrorResultPromise::CreateAndResolve(false, __func__); + } + + return MaybeClaimClient(aClientInfo, registration); +} + +void ServiceWorkerManager::UpdateClientControllers( + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerInfo> activeWorker = aRegistration->GetActive(); + MOZ_DIAGNOSTIC_ASSERT(activeWorker); + + AutoTArray<RefPtr<ClientHandle>, 16> handleList; + for (const auto& client : mControlledClients.Values()) { + if (client->mRegistrationInfo != aRegistration) { + continue; + } + + handleList.AppendElement(client->mClientHandle); + } + + // Fire event after iterating mControlledClients is done to prevent + // modification by reentering from the event handlers during iteration. + for (auto& handle : handleList) { + RefPtr<GenericErrorResultPromise> p = + handle->Control(activeWorker->Descriptor()); + + RefPtr<ServiceWorkerManager> self = this; + + // If we fail to control the client, then automatically remove it + // from our list of controlled clients. + p->Then( + GetMainThreadSerialEventTarget(), __func__, + [](bool) { + // do nothing on success + }, + [self, clientInfo = handle->Info()](const CopyableErrorResult& aRv) { + // failed to control, forget about this client + self->StopControllingClient(clientInfo); + }); + } +} + +void ServiceWorkerManager::EvictFromBFCache( + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(NS_IsMainThread()); + for (const auto& client : mControlledClients.Values()) { + if (client->mRegistrationInfo == aRegistration) { + client->mClientHandle->EvictFromBFCache(); + } + } +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetRegistration(nsIPrincipal* aPrincipal, + const nsACString& aScope) const { + MOZ_ASSERT(aPrincipal); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return GetRegistration(scopeKey, aScope); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetRegistration(const PrincipalInfo& aPrincipalInfo, + const nsACString& aScope) const { + nsAutoCString scopeKey; + nsresult rv = PrincipalInfoToScopeKey(aPrincipalInfo, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return GetRegistration(scopeKey, aScope); +} + +NS_IMETHODIMP +ServiceWorkerManager::ReloadRegistrationsForTest() { + if (NS_WARN_IF(!StaticPrefs::dom_serviceWorkers_testing_enabled())) { + return NS_ERROR_FAILURE; + } + + // Let's keep it simple and fail if there are any controlled client, + // the test case can take care of making sure there is none when this + // method will be called. + if (NS_WARN_IF(!mControlledClients.IsEmpty())) { + return NS_ERROR_FAILURE; + } + + for (const auto& info : mRegistrationInfos.Values()) { + for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) { + MOZ_ASSERT(reg); + reg->ForceShutdown(); + } + } + + mRegistrationInfos.Clear(); + + nsTArray<ServiceWorkerRegistrationData> data; + RefPtr<ServiceWorkerRegistrar> swr = ServiceWorkerRegistrar::Get(); + if (NS_WARN_IF(!swr->ReloadDataForTest())) { + return NS_ERROR_FAILURE; + } + swr->GetRegistrations(data); + LoadRegistrations(data); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::RegisterForAddonPrincipal(nsIPrincipal* aPrincipal, + JSContext* aCx, + dom::Promise** aPromise) { + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult erv; + RefPtr<Promise> outer = Promise::Create(global, erv); + if (NS_WARN_IF(erv.Failed())) { + return erv.StealNSResult(); + } + + auto enabled = + StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup(); + if (!enabled) { + outer->MaybeRejectWithNotAllowedError( + "Disabled. extensions.backgroundServiceWorker.enabled is false"); + outer.forget(aPromise); + return NS_OK; + } + + MOZ_ASSERT(aPrincipal); + auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy(); + if (!addonPolicy) { + outer->MaybeRejectWithNotAllowedError("Not an extension principal"); + outer.forget(aPromise); + return NS_OK; + } + + nsCString scope; + auto result = addonPolicy->GetURL(u""_ns); + if (result.isOk()) { + scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap())); + } else { + outer->MaybeRejectWithUnknownError("Unable to resolve addon scope URL"); + outer.forget(aPromise); + return NS_OK; + } + + nsString scriptURL; + addonPolicy->GetBackgroundWorker(scriptURL); + + if (scriptURL.IsEmpty()) { + outer->MaybeRejectWithNotFoundError("Missing background worker script url"); + outer.forget(aPromise); + return NS_OK; + } + + Maybe<ClientInfo> clientInfo = + dom::ClientManager::CreateInfo(ClientType::All, aPrincipal); + + if (!clientInfo.isSome()) { + outer->MaybeRejectWithUnknownError("Error creating clientInfo"); + outer.forget(aPromise); + return NS_OK; + } + + auto regPromise = + Register(clientInfo.ref(), scope, NS_ConvertUTF16toUTF8(scriptURL), + dom::ServiceWorkerUpdateViaCache::Imports); + const RefPtr<ServiceWorkerManager> self(this); + const nsCOMPtr<nsIPrincipal> principal(aPrincipal); + regPromise->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, outer, principal, + scope](const ServiceWorkerRegistrationDescriptor& regDesc) { + RefPtr<ServiceWorkerRegistrationInfo> registration = + self->GetRegistration(principal, scope); + if (registration) { + outer->MaybeResolve(registration); + } else { + outer->MaybeRejectWithUnknownError( + "Failed to retrieve ServiceWorkerRegistrationInfo"); + } + }, + [outer](const mozilla::CopyableErrorResult& err) { + CopyableErrorResult result(err); + outer->MaybeReject(std::move(result)); + }); + + outer.forget(aPromise); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::GetRegistrationForAddonPrincipal( + nsIPrincipal* aPrincipal, nsIServiceWorkerRegistrationInfo** aInfo) { + MOZ_ASSERT(aPrincipal); + + MOZ_ASSERT(aPrincipal); + auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy(); + if (!addonPolicy) { + return NS_ERROR_FAILURE; + } + + nsCString scope; + auto result = addonPolicy->GetURL(u""_ns); + if (result.isOk()) { + scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap())); + } else { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> info = + GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI); + if (!info) { + aInfo = nullptr; + return NS_OK; + } + info.forget(aInfo); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::WakeForExtensionAPIEvent( + const nsAString& aExtensionBaseURL, const nsAString& aAPINamespace, + const nsAString& aAPIEventName, JSContext* aCx, dom::Promise** aPromise) { + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + ErrorResult erv; + RefPtr<Promise> outer = Promise::Create(global, erv); + if (NS_WARN_IF(erv.Failed())) { + return erv.StealNSResult(); + } + + auto enabled = + StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup(); + if (!enabled) { + outer->MaybeRejectWithNotAllowedError( + "Disabled. extensions.backgroundServiceWorker.enabled is false"); + outer.forget(aPromise); + return NS_OK; + } + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aExtensionBaseURL); + if (NS_FAILED(rv)) { + outer->MaybeReject(rv); + outer.forget(aPromise); + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal; + MOZ_TRY_VAR(principal, ScopeToPrincipal(scopeURI, {})); + + auto* addonPolicy = BasePrincipal::Cast(principal)->AddonPolicy(); + if (NS_WARN_IF(!addonPolicy)) { + outer->MaybeRejectWithNotAllowedError( + "Not an extension principal or extension disabled"); + outer.forget(aPromise); + return NS_OK; + } + + OriginAttributes attrs; + ServiceWorkerInfo* info = GetActiveWorkerInfoForScope( + attrs, NS_ConvertUTF16toUTF8(aExtensionBaseURL)); + if (NS_WARN_IF(!info)) { + outer->MaybeRejectWithInvalidStateError( + "No active worker for the extension background service worker"); + outer.forget(aPromise); + return NS_OK; + } + + ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate(); + auto result = + workerPrivate->WakeForExtensionAPIEvent(aAPINamespace, aAPIEventName); + if (result.isErr()) { + outer->MaybeReject(result.propagateErr()); + outer.forget(aPromise); + return NS_OK; + } + + RefPtr<ServiceWorkerPrivate::PromiseExtensionWorkerHasListener> innerPromise = + result.unwrap(); + + innerPromise->Then( + GetMainThreadSerialEventTarget(), __func__, + [outer](bool aSubscribedEvent) { outer->MaybeResolve(aSubscribedEvent); }, + [outer](nsresult aErrorResult) { outer->MaybeReject(aErrorResult); }); + + outer.forget(aPromise); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::GetRegistrationByPrincipal( + nsIPrincipal* aPrincipal, const nsAString& aScope, + nsIServiceWorkerRegistrationInfo** aInfo) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aInfo); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> info = + GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI); + if (!info) { + return NS_ERROR_FAILURE; + } + info.forget(aInfo); + + return NS_OK; +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey, + const nsACString& aScope) const { + RefPtr<ServiceWorkerRegistrationInfo> reg; + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(aScopeKey, &data)) { + return reg.forget(); + } + + data->mInfos.Get(aScope, getter_AddRefs(reg)); + return reg.forget(); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::CreateNewRegistration( + const nsCString& aScope, nsIPrincipal* aPrincipal, + ServiceWorkerUpdateViaCache aUpdateViaCache, + IPCNavigationPreloadState aNavigationPreloadState) { +#ifdef DEBUG + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + RefPtr<ServiceWorkerRegistrationInfo> tmp = + GetRegistration(aPrincipal, aScope); + MOZ_ASSERT(!tmp); +#endif + + RefPtr<ServiceWorkerRegistrationInfo> registration = + new ServiceWorkerRegistrationInfo(aScope, aPrincipal, aUpdateViaCache, + std::move(aNavigationPreloadState)); + + // From now on ownership of registration is with + // mServiceWorkerRegistrationInfos. + AddScopeAndRegistration(aScope, registration); + return registration.forget(); +} + +void ServiceWorkerManager::MaybeRemoveRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(aRegistration); + RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); + if (!newest && HasScope(aRegistration->Principal(), aRegistration->Scope())) { + RemoveRegistration(aRegistration); + } +} + +void ServiceWorkerManager::RemoveRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + // Note, we do not need to call mActor->SendUnregister() here. There are a + // few ways we can get here: 1) Through a normal unregister which calls + // SendUnregister() in the + // unregister job Start() method. + // 2) Through origin storage being purged. These result in ForceUnregister() + // starting unregister jobs which in turn call SendUnregister(). + // 3) Through the failure to install a new service worker. Since we don't + // store the registration until install succeeds, we do not need to call + // SendUnregister here. + MOZ_ASSERT(HasScope(aRegistration->Principal(), aRegistration->Scope())); + + RemoveScopeAndRegistration(aRegistration); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID)); + if (!array) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (const auto& info : mRegistrationInfos.Values()) { + for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) { + MOZ_ASSERT(reg); + + array->AppendElement(reg); + } + } + + array.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveRegistrationsByOriginAttributes( + const nsAString& aPattern) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(!aPattern.IsEmpty()); + + OriginAttributesPattern pattern; + MOZ_ALWAYS_TRUE(pattern.Init(aPattern)); + + for (const auto& data : mRegistrationInfos.Values()) { + // We can use iteration because ForceUnregister (and Unregister) are + // async. Otherwise doing some R/W operations on an hashtable during + // iteration will crash. + for (ServiceWorkerRegistrationInfo* reg : data->mInfos.Values()) { + MOZ_ASSERT(reg); + MOZ_ASSERT(reg->Principal()); + + bool matches = pattern.Matches(reg->Principal()->OriginAttributesRef()); + if (!matches) { + continue; + } + + ForceUnregister(data.get(), reg); + } + } + + return NS_OK; +} + +void ServiceWorkerManager::ForceUnregister( + RegistrationDataPerPrincipal* aRegistrationData, + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(aRegistrationData); + MOZ_ASSERT(aRegistration); + + RefPtr<ServiceWorkerJobQueue> queue; + aRegistrationData->mJobQueues.Get(aRegistration->Scope(), + getter_AddRefs(queue)); + if (queue) { + queue->CancelAll(); + } + + if (auto entry = + aRegistrationData->mUpdateTimers.Lookup(aRegistration->Scope())) { + entry.Data()->Cancel(); + entry.Remove(); + } + + // Since Unregister is async, it is ok to call it in an enumeration. + Unregister(aRegistration->Principal(), nullptr, + NS_ConvertUTF8toUTF16(aRegistration->Scope())); +} + +NS_IMETHODIMP +ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aListener || mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveListener( + nsIServiceWorkerManagerListener* aListener) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aListener || !mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, kFinishShutdownTopic) == 0) { + MaybeFinishShutdown(); + return NS_OK; + } + + MOZ_CRASH("Received message we aren't supposed to be registered for!"); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::PropagateUnregister( + nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + // Return earlier with an explicit failure if this xpcom method is called + // when the ServiceWorkerManager is not initialized yet or it is already + // shutting down. + if (NS_WARN_IF(!mActor)) { + return NS_ERROR_FAILURE; + } + + PrincipalInfo principalInfo; + if (NS_WARN_IF( + NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) { + return NS_ERROR_FAILURE; + } + + mActor->SendPropagateUnregister(principalInfo, aScope); + + nsresult rv = Unregister(aPrincipal, aCallback, aScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void ServiceWorkerManager::NotifyListenersOnRegister( + nsIServiceWorkerRegistrationInfo* aInfo) { + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners( + mListeners.Clone()); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnRegister(aInfo); + } +} + +void ServiceWorkerManager::NotifyListenersOnUnregister( + nsIServiceWorkerRegistrationInfo* aInfo) { + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners( + mListeners.Clone()); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnUnregister(aInfo); + } +} + +void ServiceWorkerManager::NotifyListenersOnQuotaUsageCheckFinish( + nsIServiceWorkerRegistrationInfo* aRegistration) { + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners( + mListeners.Clone()); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnQuotaUsageCheckFinish(aRegistration); + } +} + +class UpdateTimerCallback final : public nsITimerCallback, public nsINamed { + nsCOMPtr<nsIPrincipal> mPrincipal; + const nsCString mScope; + + ~UpdateTimerCallback() = default; + + public: + UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope) + : mPrincipal(aPrincipal), mScope(aScope) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPrincipal); + MOZ_ASSERT(!mScope.IsEmpty()); + } + + NS_IMETHOD + Notify(nsITimer* aTimer) override { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // shutting down, do nothing + return NS_OK; + } + + swm->UpdateTimerFired(mPrincipal, mScope); + return NS_OK; + } + + NS_IMETHOD + GetName(nsACString& aName) override { + aName.AssignLiteral("UpdateTimerCallback"); + return NS_OK; + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback, nsINamed) + +void ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal, + const nsACString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (mShuttingDown) { + return; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + data->mUpdateTimers.WithEntryHandle( + aScope, [&aPrincipal, &aScope](auto&& entry) { + if (entry) { + // In case there is already a timer scheduled, just use the original + // schedule time. We don't want to push it out to a later time since + // that could allow updates to be starved forever if events are + // continuously fired. + return; + } + + nsCOMPtr<nsITimerCallback> callback = + new UpdateTimerCallback(aPrincipal, aScope); + + const uint32_t UPDATE_DELAY_MS = 1000; + + nsCOMPtr<nsITimer> timer; + + const nsresult rv = + NS_NewTimerWithCallback(getter_AddRefs(timer), callback, + UPDATE_DELAY_MS, nsITimer::TYPE_ONE_SHOT); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + entry.Insert(std::move(timer)); + }); +} + +void ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal, + const nsACString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (mShuttingDown) { + return; + } + + // First cleanup the timer. + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + if (auto entry = data->mUpdateTimers.Lookup(aScope)) { + entry.Data()->Cancel(); + entry.Remove(); + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + data->mInfos.Get(aScope, getter_AddRefs(registration)); + if (!registration) { + return; + } + + if (!registration->CheckAndClearIfUpdateNeeded()) { + return; + } + + OriginAttributes attrs = aPrincipal->OriginAttributesRef(); + + SoftUpdate(attrs, aScope); +} + +void ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal, + const nsACString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (!mActor) { + return; + } + + PrincipalInfo principalInfo; + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + Unused << mActor->SendUnregister(principalInfo, + NS_ConvertUTF8toUTF16(aScope)); +} + +void ServiceWorkerManager::AddOrphanedRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRegistration); + MOZ_ASSERT(aRegistration->IsUnregistered()); + MOZ_ASSERT(!aRegistration->IsControllingClients()); + MOZ_ASSERT(!aRegistration->IsIdle()); + MOZ_ASSERT(!mOrphanedRegistrations.has(aRegistration)); + + MOZ_ALWAYS_TRUE(mOrphanedRegistrations.putNew(aRegistration)); +} + +void ServiceWorkerManager::RemoveOrphanedRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRegistration); + MOZ_ASSERT(aRegistration->IsUnregistered()); + MOZ_ASSERT(!aRegistration->IsControllingClients()); + MOZ_ASSERT(aRegistration->IsIdle()); + MOZ_ASSERT(mOrphanedRegistrations.has(aRegistration)); + + mOrphanedRegistrations.remove(aRegistration); +} + +uint32_t ServiceWorkerManager::MaybeInitServiceWorkerShutdownProgress() const { + if (!mShutdownBlocker) { + return ServiceWorkerShutdownBlocker::kInvalidShutdownStateId; + } + + return mShutdownBlocker->CreateShutdownState(); +} + +void ServiceWorkerManager::ReportServiceWorkerShutdownProgress( + uint32_t aShutdownStateId, + ServiceWorkerShutdownState::Progress aProgress) const { + MOZ_ASSERT(mShutdownBlocker); + mShutdownBlocker->ReportShutdownProgress(aShutdownStateId, aProgress); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerManager.h b/dom/serviceworkers/ServiceWorkerManager.h new file mode 100644 index 0000000000..9a3b9ee39f --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerManager.h @@ -0,0 +1,439 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_workers_serviceworkermanager_h +#define mozilla_dom_workers_serviceworkermanager_h + +#include <cstdint> +#include "ErrorList.h" +#include "ServiceWorkerShutdownState.h" +#include "js/ErrorReport.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/HashTable.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/ClientHandle.h" +#include "mozilla/dom/ClientOpPromise.h" +#include "mozilla/dom/ServiceWorkerRegistrationBinding.h" +#include "mozilla/dom/ServiceWorkerRegistrationInfo.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/mozalloc.h" +#include "nsClassHashtable.h" +#include "nsContentUtils.h" +#include "nsHashKeys.h" +#include "nsIObserver.h" +#include "nsIServiceWorkerManager.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +class nsIConsoleReportCollector; + +namespace mozilla { + +class OriginAttributes; + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +extern uint32_t gServiceWorkersRegistered; +extern uint32_t gServiceWorkersRegisteredFetch; + +class ContentParent; +class ServiceWorkerInfo; +class ServiceWorkerJobQueue; +class ServiceWorkerManagerChild; +class ServiceWorkerPrivate; +class ServiceWorkerRegistrar; +class ServiceWorkerShutdownBlocker; + +class ServiceWorkerUpdateFinishCallback { + protected: + virtual ~ServiceWorkerUpdateFinishCallback() = default; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateFinishCallback) + + virtual void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) = 0; + + virtual void UpdateFailed(ErrorResult& aStatus) = 0; +}; + +#define NS_SERVICEWORKERMANAGER_IMPL_IID \ + { /* f4f8755a-69ca-46e8-a65d-775745535990 */ \ + 0xf4f8755a, 0x69ca, 0x46e8, { \ + 0xa6, 0x5d, 0x77, 0x57, 0x45, 0x53, 0x59, 0x90 \ + } \ + } + +/* + * The ServiceWorkerManager is a per-process global that deals with the + * installation, querying and event dispatch of ServiceWorkers for all the + * origins in the process. + * + * NOTE: the following documentation is a WIP: + * + * The ServiceWorkerManager (SWM) is a main-thread, parent-process singleton + * that encapsulates the browser-global state of service workers. This state + * includes, but is not limited to, all service worker registrations and all + * controlled service worker clients. The SWM also provides methods to read and + * mutate this state and to dispatch operations (e.g. DOM events such as a + * FetchEvent) to service workers. + * + * Example usage: + * + * MOZ_ASSERT(NS_IsMainThread(), "SWM is main-thread only"); + * + * RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + * + * // Nullness must be checked by code that possibly executes during browser + * // shutdown, which is when the SWM is destroyed. + * if (swm) { + * // Do something with the SWM. + * } + */ +class ServiceWorkerManager final : public nsIServiceWorkerManager, + public nsIObserver { + friend class GetRegistrationsRunnable; + friend class GetRegistrationRunnable; + friend class ServiceWorkerJob; + friend class ServiceWorkerRegistrationInfo; + friend class ServiceWorkerShutdownBlocker; + friend class ServiceWorkerUnregisterJob; + friend class ServiceWorkerUpdateJob; + friend class UpdateTimerCallback; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVICEWORKERMANAGER + NS_DECL_NSIOBSERVER + + // Return true if the given principal and URI matches a registered service + // worker which handles fetch event. + // If there is a matched service worker but doesn't handle fetch events, this + // method will try to set the matched service worker as the controller of the + // passed in channel. Then also schedule a soft-update job for the service + // worker. + bool IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI, + nsIChannel* aChannel); + + void DispatchFetchEvent(nsIInterceptedChannel* aChannel, ErrorResult& aRv); + + void Update(nsIPrincipal* aPrincipal, const nsACString& aScope, + nsCString aNewestWorkerScriptUrl, + ServiceWorkerUpdateFinishCallback* aCallback); + + void UpdateInternal(nsIPrincipal* aPrincipal, const nsACString& aScope, + nsCString&& aNewestWorkerScriptUrl, + ServiceWorkerUpdateFinishCallback* aCallback); + + void SoftUpdate(const OriginAttributes& aOriginAttributes, + const nsACString& aScope); + + void SoftUpdateInternal(const OriginAttributes& aOriginAttributes, + const nsACString& aScope, + ServiceWorkerUpdateFinishCallback* aCallback); + + RefPtr<ServiceWorkerRegistrationPromise> Register( + const ClientInfo& aClientInfo, const nsACString& aScopeURL, + const nsACString& aScriptURL, + ServiceWorkerUpdateViaCache aUpdateViaCache); + + RefPtr<ServiceWorkerRegistrationPromise> GetRegistration( + const ClientInfo& aClientInfo, const nsACString& aURL) const; + + RefPtr<ServiceWorkerRegistrationListPromise> GetRegistrations( + const ClientInfo& aClientInfo) const; + + already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration( + nsIPrincipal* aPrincipal, const nsACString& aScope) const; + + already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration( + const mozilla::ipc::PrincipalInfo& aPrincipal, + const nsACString& aScope) const; + + already_AddRefed<ServiceWorkerRegistrationInfo> CreateNewRegistration( + const nsCString& aScope, nsIPrincipal* aPrincipal, + ServiceWorkerUpdateViaCache aUpdateViaCache, + IPCNavigationPreloadState aNavigationPreloadState = + IPCNavigationPreloadState(false, "true"_ns)); + + void RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + void StoreRegistration(nsIPrincipal* aPrincipal, + ServiceWorkerRegistrationInfo* aRegistration); + + /** + * Report an error for the given scope to any window we think might be + * interested, failing over to the Browser Console if we couldn't find any. + * + * Error messages should be localized, so you probably want to call + * LocalizeAndReportToAllClients instead, which in turn calls us after + * localizing the error. + */ + void ReportToAllClients(const nsCString& aScope, const nsString& aMessage, + const nsString& aFilename, const nsString& aLine, + uint32_t aLineNumber, uint32_t aColumnNumber, + uint32_t aFlags); + + /** + * Report a localized error for the given scope to any window we think might + * be interested. + * + * Note that this method takes an nsTArray<nsString> for the parameters, not + * bare chart16_t*[]. You can use a std::initializer_list constructor inline + * so that argument might look like: nsTArray<nsString> { some_nsString, + * PromiseFlatString(some_nsSubString_aka_nsAString), + * NS_ConvertUTF8toUTF16(some_nsCString_or_nsCSubString), + * u"some literal"_ns }. If you have anything else, like a + * number, you can use an nsAutoString with AppendInt/friends. + * + * @param [aFlags] + * The nsIScriptError flag, one of errorFlag (0x0), warningFlag (0x1), + * infoFlag (0x8). We default to error if omitted because usually we're + * logging exceptional and/or obvious breakage. + */ + static void LocalizeAndReportToAllClients( + const nsCString& aScope, const char* aStringKey, + const nsTArray<nsString>& aParamArray, uint32_t aFlags = 0x0, + const nsString& aFilename = u""_ns, const nsString& aLine = u""_ns, + uint32_t aLineNumber = 0, uint32_t aColumnNumber = 0); + + // Always consumes the error by reporting to consoles of all controlled + // documents. + void HandleError(JSContext* aCx, nsIPrincipal* aPrincipal, + const nsCString& aScope, const nsString& aWorkerURL, + const nsString& aMessage, const nsString& aFilename, + const nsString& aLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType); + + [[nodiscard]] RefPtr<GenericErrorResultPromise> MaybeClaimClient( + const ClientInfo& aClientInfo, + ServiceWorkerRegistrationInfo* aWorkerRegistration); + + [[nodiscard]] RefPtr<GenericErrorResultPromise> MaybeClaimClient( + const ClientInfo& aClientInfo, + const ServiceWorkerDescriptor& aServiceWorker); + + static already_AddRefed<ServiceWorkerManager> GetInstance(); + + void LoadRegistration(const ServiceWorkerRegistrationData& aRegistration); + + void LoadRegistrations( + const nsTArray<ServiceWorkerRegistrationData>& aRegistrations); + + void MaybeCheckNavigationUpdate(const ClientInfo& aClientInfo); + + nsresult SendPushEvent(const nsACString& aOriginAttributes, + const nsACString& aScope, const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData); + + void WorkerIsIdle(ServiceWorkerInfo* aWorker); + + RefPtr<ServiceWorkerRegistrationPromise> WhenReady( + const ClientInfo& aClientInfo); + + void CheckPendingReadyPromises(); + + void RemovePendingReadyPromise(const ClientInfo& aClientInfo); + + void NoteInheritedController(const ClientInfo& aClientInfo, + const ServiceWorkerDescriptor& aController); + + void BlockShutdownOn(GenericNonExclusivePromise* aPromise, + uint32_t aShutdownStateId); + + nsresult GetClientRegistration( + const ClientInfo& aClientInfo, + ServiceWorkerRegistrationInfo** aRegistrationInfo); + + int32_t GetPrincipalQuotaUsageCheckCount(nsIPrincipal* aPrincipal); + + void CheckPrincipalQuotaUsage(nsIPrincipal* aPrincipal, + const nsACString& aScope); + + // Returns the shutdown state ID (may be an invalid ID if an + // nsIAsyncShutdownBlocker is not used). + uint32_t MaybeInitServiceWorkerShutdownProgress() const; + + void ReportServiceWorkerShutdownProgress( + uint32_t aShutdownStateId, + ServiceWorkerShutdownState::Progress aProgress) const; + + // Record periodic telemetry on number of running ServiceWorkers. When + // the number of running ServiceWorkers changes (or on shutdown), + // ServiceWorkerPrivateImpl will call RecordTelemetry with the number of + // running serviceworkers and those supporting Fetch. We use + // mTelemetryLastChange to determine how many datapoints to inject into + // Telemetry, and dispatch a background runnable to call + // RecordTelemetryGap() and Accumulate them. + void RecordTelemetry(uint32_t aNumber, uint32_t aFetch); + + void EvictFromBFCache(ServiceWorkerRegistrationInfo* aRegistration); + + private: + struct RegistrationDataPerPrincipal; + + static bool FindScopeForPath(const nsACString& aScopeKey, + const nsACString& aPath, + RegistrationDataPerPrincipal** aData, + nsACString& aMatch); + + ServiceWorkerManager(); + ~ServiceWorkerManager(); + + void Init(ServiceWorkerRegistrar* aRegistrar); + + RefPtr<GenericErrorResultPromise> StartControllingClient( + const ClientInfo& aClientInfo, + ServiceWorkerRegistrationInfo* aRegistrationInfo, + bool aControlClientHandle = true); + + void StopControllingClient(const ClientInfo& aClientInfo); + + void MaybeStartShutdown(); + + void MaybeFinishShutdown(); + + already_AddRefed<ServiceWorkerJobQueue> GetOrCreateJobQueue( + const nsACString& aOriginSuffix, const nsACString& aScope); + + void MaybeRemoveRegistrationInfo(const nsACString& aScopeKey); + + already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration( + const nsACString& aScopeKey, const nsACString& aScope) const; + + void AbortCurrentUpdate(ServiceWorkerRegistrationInfo* aRegistration); + + nsresult Update(ServiceWorkerRegistrationInfo* aRegistration); + + ServiceWorkerInfo* GetActiveWorkerInfoForScope( + const OriginAttributes& aOriginAttributes, const nsACString& aScope); + + void StopControllingRegistration( + ServiceWorkerRegistrationInfo* aRegistration); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetServiceWorkerRegistrationInfo(const ClientInfo& aClientInfo) const; + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, + nsIURI* aURI) const; + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey, + nsIURI* aURI) const; + + // This method generates a key using isInElementBrowser from the principal. We + // don't use the origin because it can change during the loading. + static nsresult PrincipalToScopeKey(nsIPrincipal* aPrincipal, + nsACString& aKey); + + static nsresult PrincipalInfoToScopeKey( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, nsACString& aKey); + + static void AddScopeAndRegistration( + const nsACString& aScope, ServiceWorkerRegistrationInfo* aRegistation); + + static bool HasScope(nsIPrincipal* aPrincipal, const nsACString& aScope); + + static void RemoveScopeAndRegistration( + ServiceWorkerRegistrationInfo* aRegistration); + + void QueueFireEventOnServiceWorkerRegistrations( + ServiceWorkerRegistrationInfo* aRegistration, const nsAString& aName); + + void UpdateClientControllers(ServiceWorkerRegistrationInfo* aRegistration); + + void MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + RefPtr<ServiceWorkerManagerChild> mActor; + + bool mShuttingDown; + + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> mListeners; + + void NotifyListenersOnRegister( + nsIServiceWorkerRegistrationInfo* aRegistration); + + void NotifyListenersOnUnregister( + nsIServiceWorkerRegistrationInfo* aRegistration); + + void NotifyListenersOnQuotaUsageCheckFinish( + nsIServiceWorkerRegistrationInfo* aRegistration); + + void ScheduleUpdateTimer(nsIPrincipal* aPrincipal, const nsACString& aScope); + + void UpdateTimerFired(nsIPrincipal* aPrincipal, const nsACString& aScope); + + void MaybeSendUnregister(nsIPrincipal* aPrincipal, const nsACString& aScope); + + nsresult SendNotificationEvent(const nsAString& aEventName, + const nsACString& aOriginSuffix, + const nsACString& aScope, const nsAString& aID, + const nsAString& aTitle, const nsAString& aDir, + const nsAString& aLang, const nsAString& aBody, + const nsAString& aTag, const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior); + + // Used by remove() and removeAll() when clearing history. + // MUST ONLY BE CALLED FROM UnregisterIfMatchesHost! + void ForceUnregister(RegistrationDataPerPrincipal* aRegistrationData, + ServiceWorkerRegistrationInfo* aRegistration); + + // An "orphaned" registration is one that is unregistered and not controlling + // clients. The ServiceWorkerManager must know about all orphaned + // registrations to forcefully shutdown all Service Workers during browser + // shutdown. + void AddOrphanedRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + void RemoveOrphanedRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + HashSet<RefPtr<ServiceWorkerRegistrationInfo>, + PointerHasher<ServiceWorkerRegistrationInfo*>> + mOrphanedRegistrations; + + RefPtr<ServiceWorkerShutdownBlocker> mShutdownBlocker; + + nsClassHashtable<nsCStringHashKey, RegistrationDataPerPrincipal> + mRegistrationInfos; + + struct ControlledClientData { + RefPtr<ClientHandle> mClientHandle; + RefPtr<ServiceWorkerRegistrationInfo> mRegistrationInfo; + + ControlledClientData(ClientHandle* aClientHandle, + ServiceWorkerRegistrationInfo* aRegistrationInfo) + : mClientHandle(aClientHandle), mRegistrationInfo(aRegistrationInfo) {} + }; + + nsClassHashtable<nsIDHashKey, ControlledClientData> mControlledClients; + + struct PendingReadyData { + RefPtr<ClientHandle> mClientHandle; + RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise; + + explicit PendingReadyData(ClientHandle* aClientHandle) + : mClientHandle(aClientHandle), + mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)) {} + }; + + nsTArray<UniquePtr<PendingReadyData>> mPendingReadyList; + + const uint32_t mTelemetryPeriodMs = 5 * 1000; + TimeStamp mTelemetryLastChange; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkermanager_h diff --git a/dom/serviceworkers/ServiceWorkerManagerChild.h b/dom/serviceworkers/ServiceWorkerManagerChild.h new file mode 100644 index 0000000000..54a374b14b --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerManagerChild.h @@ -0,0 +1,42 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerManagerChild_h +#define mozilla_dom_ServiceWorkerManagerChild_h + +#include "mozilla/dom/PServiceWorkerManagerChild.h" +#include "mozilla/ipc/BackgroundUtils.h" + +namespace mozilla { + +class OriginAttributes; + +namespace ipc { +class BackgroundChildImpl; +} // namespace ipc + +namespace dom { + +class ServiceWorkerManagerChild final : public PServiceWorkerManagerChild { + friend class mozilla::ipc::BackgroundChildImpl; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerManagerChild) + + void ManagerShuttingDown() { mShuttingDown = true; } + + private: + ServiceWorkerManagerChild() : mShuttingDown(false) {} + + ~ServiceWorkerManagerChild() = default; + + bool mShuttingDown; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ServiceWorkerManagerChild_h diff --git a/dom/serviceworkers/ServiceWorkerManagerParent.cpp b/dom/serviceworkers/ServiceWorkerManagerParent.cpp new file mode 100644 index 0000000000..5ed0f4faa8 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerManagerParent.cpp @@ -0,0 +1,106 @@ +/* -*- 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 "ServiceWorkerManagerParent.h" +#include "ServiceWorkerUtils.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ServiceWorkerRegistrar.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/Unused.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +ServiceWorkerManagerParent::ServiceWorkerManagerParent() { + AssertIsOnBackgroundThread(); +} + +ServiceWorkerManagerParent::~ServiceWorkerManagerParent() { + AssertIsOnBackgroundThread(); +} + +mozilla::ipc::IPCResult ServiceWorkerManagerParent::RecvRegister( + const ServiceWorkerRegistrationData& aData) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!BackgroundParent::IsOtherProcessActor(Manager())); + + // Basic validation. + if (aData.scope().IsEmpty() || + aData.principal().type() == PrincipalInfo::TNullPrincipalInfo || + aData.principal().type() == PrincipalInfo::TSystemPrincipalInfo) { + return IPC_FAIL_NO_REASON(this); + } + + // If false then we have shutdown during the process of trying to update the + // registrar. We can give up on this modification. + if (const RefPtr<dom::ServiceWorkerRegistrar> service = + dom::ServiceWorkerRegistrar::Get()) { + service->RegisterServiceWorker(aData); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ServiceWorkerManagerParent::RecvUnregister( + const PrincipalInfo& aPrincipalInfo, const nsString& aScope) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!BackgroundParent::IsOtherProcessActor(Manager())); + + // Basic validation. + if (aScope.IsEmpty() || + aPrincipalInfo.type() == PrincipalInfo::TNullPrincipalInfo || + aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + return IPC_FAIL_NO_REASON(this); + } + + // If false then we have shutdown during the process of trying to update the + // registrar. We can give up on this modification. + if (const RefPtr<dom::ServiceWorkerRegistrar> service = + dom::ServiceWorkerRegistrar::Get()) { + service->UnregisterServiceWorker(aPrincipalInfo, + NS_ConvertUTF16toUTF8(aScope)); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ServiceWorkerManagerParent::RecvPropagateUnregister( + const PrincipalInfo& aPrincipalInfo, const nsString& aScope) { + AssertIsOnBackgroundThread(); + + RefPtr<dom::ServiceWorkerRegistrar> service = + dom::ServiceWorkerRegistrar::Get(); + MOZ_ASSERT(service); + + // It's possible that we don't have any ServiceWorkerManager managing this + // scope but we still need to unregister it from the ServiceWorkerRegistrar. + service->UnregisterServiceWorker(aPrincipalInfo, + NS_ConvertUTF16toUTF8(aScope)); + + // There is no longer any point to propagating because the only sender is the + // one and only ServiceWorkerManager, but it is necessary for us to have run + // the unregister call above because until Bug 1183245 is fixed, + // nsIServiceWorkerManager.propagateUnregister() is a de facto API for + // clearing ServiceWorker registrations by Sanitizer.jsm via + // ServiceWorkerCleanUp.jsm, as well as devtools "unregister" affordance and + // the no-longer-relevant about:serviceworkers UI. + + return IPC_OK(); +} + +void ServiceWorkerManagerParent::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/ServiceWorkerManagerParent.h b/dom/serviceworkers/ServiceWorkerManagerParent.h new file mode 100644 index 0000000000..741f2250b3 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerManagerParent.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerManagerParent_h +#define mozilla_dom_ServiceWorkerManagerParent_h + +#include "mozilla/dom/PServiceWorkerManagerParent.h" + +namespace mozilla { + +namespace ipc { +class BackgroundParentImpl; +} // namespace ipc + +namespace dom { + +class ServiceWorkerManagerService; + +class ServiceWorkerManagerParent final : public PServiceWorkerManagerParent { + friend class mozilla::ipc::BackgroundParentImpl; + friend class PServiceWorkerManagerParent; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerManagerParent) + + private: + ServiceWorkerManagerParent(); + ~ServiceWorkerManagerParent(); + + mozilla::ipc::IPCResult RecvRegister( + const ServiceWorkerRegistrationData& aData); + + mozilla::ipc::IPCResult RecvUnregister(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope); + + mozilla::ipc::IPCResult RecvPropagateUnregister( + const PrincipalInfo& aPrincipalInfo, const nsString& aScope); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ServiceWorkerManagerParent_h diff --git a/dom/serviceworkers/ServiceWorkerOp.cpp b/dom/serviceworkers/ServiceWorkerOp.cpp new file mode 100644 index 0000000000..8cd9218852 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerOp.cpp @@ -0,0 +1,1913 @@ +/* -*- 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 "nsProxyRelease.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->GetOwningEventTarget()->IsOnCurrentThread()); + + auto launcherData = aOwner->mLauncherData.Access(); + + if (NS_WARN_IF(!launcherData->mIPCActive)) { + 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::PendingTerminated>()) || + NS_WARN_IF(aState.is<RemoteWorkerChild::Terminated>())) { + 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__); + }); + } + + RefPtr<RemoteWorkerChild> owner = aOwner; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), owner = std::move(owner)]() mutable { + MaybeReportServiceWorkerShutdownProgress(self->mArgs); + + auto lock = owner->mState.Lock(); + auto& state = lock.ref(); + + if (NS_WARN_IF(!state.is<Running>() && !self->IsTerminationOp())) { + self->RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (self->IsTerminationOp()) { + owner->CloseWorkerOnMainThread(state); + } else { + MOZ_ASSERT(state.is<Running>()); + + RefPtr<WorkerRunnable> workerRunnable = + self->GetRunnable(state.as<Running>().mWorkerPrivate); + + if (NS_WARN_IF(!workerRunnable->Dispatch())) { + self->RejectAll(NS_ERROR_FAILURE); + } + } + + nsCOMPtr<nsIEventTarget> target = owner->GetOwningEventTarget(); + NS_ProxyRelease(__func__, target, owner.forget()); + }); + + mStarted = true; + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return true; +} + +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__); + + if (mActor) { + NS_ProxyRelease("FetchEventOp::mActor", RemoteWorkerService::Thread(), + mActor.forget()); + } +} + +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(); + 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; + } + + { + ErrorResult error; + bool bodyUsed = response->GetBodyUsed(error); + error.WouldReportJSException(); + if (NS_WARN_IF(error.Failed())) { + autoCancel.SetCancelErrorResult(aCx, error); + return; + } + if (NS_WARN_IF(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(MakeTuple( + 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<FetchEventPreloadResponseEndPromise> preloadResponseEndPromise = + mActor->GetPreloadResponseEndPromise(); + MOZ_ASSERT(preloadResponseEndPromise); + preloadResponseEndPromise + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, performanceStorage, + globalObjectAsSupports](ResponseEndArgs&& aArgs) { + if (aArgs.timing().isSome() && performanceStorage) { + performanceStorage->AddEntry( + aArgs.timing().ref().entryName(), + aArgs.timing().ref().initiatorType(), + MakeUnique<PerformanceTimingData>( + aArgs.timing().ref().timingData())); + } + 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 diff --git a/dom/serviceworkers/ServiceWorkerOp.h b/dom/serviceworkers/ServiceWorkerOp.h new file mode 100644 index 0000000000..b916b6dea4 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerOp.h @@ -0,0 +1,195 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerop_h__ +#define mozilla_dom_serviceworkerop_h__ + +#include <functional> + +#include "mozilla/dom/ServiceWorkerOpPromise.h" +#include "nsISupportsImpl.h" + +#include "ServiceWorkerEvents.h" +#include "ServiceWorkerOpPromise.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" +#include "mozilla/dom/WorkerRunnable.h" + +namespace mozilla::dom { + +class FetchEventOpProxyChild; + +class ServiceWorkerOp : public RemoteWorkerChild::Op { + public: + // `aCallback` will be called when the operation completes or is canceled. + static already_AddRefed<ServiceWorkerOp> Create( + ServiceWorkerOpArgs&& aArgs, + std::function<void(const ServiceWorkerOpResult&)>&& aCallback); + + ServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, + std::function<void(const ServiceWorkerOpResult&)>&& aCallback); + + ServiceWorkerOp(const ServiceWorkerOp&) = delete; + + ServiceWorkerOp& operator=(const ServiceWorkerOp&) = delete; + + ServiceWorkerOp(ServiceWorkerOp&&) = default; + + ServiceWorkerOp& operator=(ServiceWorkerOp&&) = default; + + // Returns `true` if the operation has started and `false` otherwise. + bool MaybeStart(RemoteWorkerChild* aOwner, + RemoteWorkerChild::State& aState) final; + + void Cancel() final; + + protected: + ~ServiceWorkerOp(); + + bool Started() const; + + bool IsTerminationOp() const; + + // Override to provide a runnable that's not a `ServiceWorkerOpRunnable.` + virtual RefPtr<WorkerRunnable> GetRunnable(WorkerPrivate* aWorkerPrivate); + + // Overridden by ServiceWorkerOp subclasses, it should return true when + // the ServiceWorkerOp was executed successfully (and false if it did fail). + // Content throwing an exception during event dispatch is still considered + // success. + virtual bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) = 0; + + // Override to reject any additional MozPromises that subclasses may contain. + virtual void RejectAll(nsresult aStatus); + + ServiceWorkerOpArgs mArgs; + + // Subclasses must settle this promise when appropriate. + MozPromiseHolder<ServiceWorkerOpPromise> mPromiseHolder; + + private: + class ServiceWorkerOpRunnable; + + bool mStarted = false; +}; + +class ExtendableEventOp : public ServiceWorkerOp, + public ExtendableEventCallback { + using ServiceWorkerOp::ServiceWorkerOp; + + protected: + ~ExtendableEventOp() = default; + + void FinishedWithResult(ExtendableEventResult aResult) override; +}; + +class FetchEventOp final : public ExtendableEventOp, + public PromiseNativeHandler { + using ExtendableEventOp::ExtendableEventOp; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + /** + * This must be called once and only once before the first call to + * `MaybeStart()`; `aActor` will be used for `AsyncLog()` and + * `ReportCanceled().` + */ + void SetActor(RefPtr<FetchEventOpProxyChild> aActor); + + void RevokeActor(FetchEventOpProxyChild* aActor); + + // This must be called at most once before the first call to `MaybeStart().` + RefPtr<FetchEventRespondWithPromise> GetRespondWithPromise(); + + // This must be called when `FetchEvent::RespondWith()` is called. + void RespondWithCalledAt(const nsCString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, + uint32_t aRespondWithColumnNumber); + + void ReportCanceled(const nsCString& aPreventDefaultScriptSpec, + uint32_t aPreventDefaultLineNumber, + uint32_t aPreventDefaultColumnNumber); + + private: + class AutoCancel; + + ~FetchEventOp(); + + bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + + void RejectAll(nsresult aStatus) override; + + void FinishedWithResult(ExtendableEventResult aResult) override; + + /** + * `{Resolved,Reject}Callback()` are use to handle the + * `FetchEvent::RespondWith()` promise. + */ + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + void MaybeFinished(); + + // Requires mRespondWithClosure to be non-empty. + void AsyncLog(const nsCString& aMessageName, nsTArray<nsString> aParams); + + void AsyncLog(const nsCString& aScriptSpec, uint32_t aLineNumber, + uint32_t aColumnNumber, const nsCString& aMessageName, + nsTArray<nsString> aParams); + + void GetRequestURL(nsAString& aOutRequestURL); + + // A failure code means that the dispatch failed. + nsresult DispatchFetchEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate); + + // Worker Launcher thread only. Used for `AsyncLog().` + RefPtr<FetchEventOpProxyChild> mActor; + + /** + * Created on the Worker Launcher thread and settled on the worker thread. + * If this isn't settled before `mPromiseHolder` (which it should be), + * `FetchEventOpChild` will cancel the intercepted network request. + */ + MozPromiseHolder<FetchEventRespondWithPromise> mRespondWithPromiseHolder; + + // Worker thread only. + Maybe<ExtendableEventResult> mResult; + bool mPostDispatchChecksDone = false; + + // Worker thread only; set when `FetchEvent::RespondWith()` is called. + Maybe<FetchEventRespondWithClosure> mRespondWithClosure; + + // Must be set to `nullptr` on the worker thread because `Promise`'s + // destructor must be called on the worker thread. + RefPtr<Promise> mHandled; + + // Must be set to `nullptr` on the worker thread because `Promise`'s + // destructor must be called on the worker thread. + RefPtr<Promise> mPreloadResponse; + + // Holds the callback that resolves mPreloadResponse. + MozPromiseRequestHolder<FetchEventPreloadResponseAvailablePromise> + mPreloadResponseAvailablePromiseRequestHolder; + MozPromiseRequestHolder<FetchEventPreloadResponseEndPromise> + mPreloadResponseEndPromiseRequestHolder; + + TimeStamp mFetchHandlerStart; + TimeStamp mFetchHandlerFinish; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerop_h__ diff --git a/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh b/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh new file mode 100644 index 0000000000..55c0525a39 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh @@ -0,0 +1,189 @@ +/* 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 ClientIPCTypes; +include DOMTypes; +include FetchTypes; + +include "mozilla/dom/ServiceWorkerIPCUtils.h"; + +using ServiceWorkerState from "mozilla/dom/ServiceWorkerBinding.h"; +using mozilla::TimeStamp from "mozilla/TimeStamp.h"; + +namespace mozilla { +namespace dom { + +/** + * ServiceWorkerOpArgs + */ +struct ServiceWorkerCheckScriptEvaluationOpArgs {}; + +struct ServiceWorkerUpdateStateOpArgs { + ServiceWorkerState state; +}; + +struct ServiceWorkerTerminateWorkerOpArgs { + uint32_t shutdownStateId; +}; + +struct ServiceWorkerLifeCycleEventOpArgs { + nsString eventName; +}; + +// Possibly need to differentiate an empty array from the absence of an array. +union OptionalPushData { + void_t; + uint8_t[]; +}; + +struct ServiceWorkerPushEventOpArgs { + nsString messageId; + OptionalPushData data; +}; + +struct ServiceWorkerPushSubscriptionChangeEventOpArgs {}; + +struct ServiceWorkerNotificationEventOpArgs { + nsString eventName; + nsString id; + nsString title; + nsString dir; + nsString lang; + nsString body; + nsString tag; + nsString icon; + nsString data; + nsString behavior; + nsString scope; + uint32_t disableOpenClickDelay; +}; + +struct ServiceWorkerExtensionAPIEventOpArgs { + // WebExtensions API namespace and event names, for a list of the API namespaces + // and related API event names refer to the API JSONSchema files in-tree: + // + // https://searchfox.org/mozilla-central/search?q=&path=extensions%2Fschemas%2F*.json + nsString apiNamespace; + nsString apiEventName; +}; + +struct ServiceWorkerMessageEventOpArgs { + ClientInfoAndState clientInfoAndState; + ClonedOrErrorMessageData clonedData; +}; + +struct ServiceWorkerFetchEventOpArgsCommon { + nsCString workerScriptSpec; + IPCInternalRequest internalRequest; + nsString clientId; + nsString resultingClientId; + bool isNonSubresourceRequest; + // Is navigation preload enabled for this fetch? If true, if some + // preloadResponse was not already provided in this structure, then it's + // expected that a PreloadResponse message will eventually be sent. + bool preloadNavigation; + // Failure injection helper; non-NS_OK values indicate that the event, instead + // of dispatching should instead return a `CancelInterceptionArgs` response + // with this nsresult. This value originates from + // `nsIServiceWorkerInfo::testingInjectCancellation`. + nsresult testingInjectCancellation; +}; + +struct ParentToParentServiceWorkerFetchEventOpArgs { + ServiceWorkerFetchEventOpArgsCommon common; + ParentToParentInternalResponse? preloadResponse; + ResponseEndArgs? preloadResponseEndArgs; +}; + +struct ParentToChildServiceWorkerFetchEventOpArgs { + ServiceWorkerFetchEventOpArgsCommon common; + ParentToChildInternalResponse? preloadResponse; + ResponseEndArgs? preloadResponseEndArgs; +}; + +union ServiceWorkerOpArgs { + ServiceWorkerCheckScriptEvaluationOpArgs; + ServiceWorkerUpdateStateOpArgs; + ServiceWorkerTerminateWorkerOpArgs; + ServiceWorkerLifeCycleEventOpArgs; + ServiceWorkerPushEventOpArgs; + ServiceWorkerPushSubscriptionChangeEventOpArgs; + ServiceWorkerNotificationEventOpArgs; + ServiceWorkerMessageEventOpArgs; + ServiceWorkerExtensionAPIEventOpArgs; + ParentToChildServiceWorkerFetchEventOpArgs; +}; + +/** + * IPCFetchEventRespondWithResult + */ +struct FetchEventRespondWithClosure { + nsCString respondWithScriptSpec; + uint32_t respondWithLineNumber; + uint32_t respondWithColumnNumber; +}; + +struct FetchEventTimeStamps { + TimeStamp fetchHandlerStart; + TimeStamp fetchHandlerFinish; +}; + +struct ChildToParentSynthesizeResponseArgs { + ChildToParentInternalResponse internalResponse; + FetchEventRespondWithClosure closure; + FetchEventTimeStamps timeStamps; +}; + +struct ParentToParentSynthesizeResponseArgs { + ParentToParentInternalResponse internalResponse; + FetchEventRespondWithClosure closure; + FetchEventTimeStamps timeStamps; +}; + +struct ResetInterceptionArgs { + FetchEventTimeStamps timeStamps; +}; + +struct CancelInterceptionArgs { + nsresult status; + FetchEventTimeStamps timeStamps; +}; + +union ChildToParentFetchEventRespondWithResult { + ChildToParentSynthesizeResponseArgs; + ResetInterceptionArgs; + CancelInterceptionArgs; +}; + +union ParentToParentFetchEventRespondWithResult { + ParentToParentSynthesizeResponseArgs; + ResetInterceptionArgs; + CancelInterceptionArgs; +}; + +/** + * ServiceWorkerOpResult + */ +struct ServiceWorkerCheckScriptEvaluationOpResult { + bool workerScriptExecutedSuccessfully; + bool fetchHandlerWasAdded; +}; + +struct ServiceWorkerFetchEventOpResult { + nsresult rv; +}; + +struct ServiceWorkerExtensionAPIEventOpResult { + bool extensionAPIEventListenerWasAdded; +}; + +union ServiceWorkerOpResult { + nsresult; + ServiceWorkerCheckScriptEvaluationOpResult; + ServiceWorkerFetchEventOpResult; + ServiceWorkerExtensionAPIEventOpResult; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/ServiceWorkerOpPromise.h b/dom/serviceworkers/ServiceWorkerOpPromise.h new file mode 100644 index 0000000000..f46b645961 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerOpPromise.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkeroppromise_h__ +#define mozilla_dom_serviceworkeroppromise_h__ + +#include <utility> + +#include "mozilla/MozPromise.h" +#include "mozilla/Tuple.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" + +namespace mozilla::dom { + +class InternalResponse; + +using SynthesizeResponseArgs = + Tuple<SafeRefPtr<InternalResponse>, FetchEventRespondWithClosure, + FetchEventTimeStamps>; + +using FetchEventRespondWithResult = + Variant<SynthesizeResponseArgs, ResetInterceptionArgs, + CancelInterceptionArgs>; + +using FetchEventRespondWithPromise = + MozPromise<FetchEventRespondWithResult, CancelInterceptionArgs, true>; + +// The reject type int is arbitrary, since this promise will never get rejected. +// Unfortunately void is not supported as a reject type. +using FetchEventPreloadResponseAvailablePromise = + MozPromise<SafeRefPtr<InternalResponse>, int, true>; + +using FetchEventPreloadResponseEndPromise = + MozPromise<ResponseEndArgs, int, true>; + +using ServiceWorkerOpPromise = + MozPromise<ServiceWorkerOpResult, nsresult, true>; + +using ServiceWorkerFetchEventOpPromise = + MozPromise<ServiceWorkerFetchEventOpResult, nsresult, true>; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkeroppromise_h__ diff --git a/dom/serviceworkers/ServiceWorkerParent.cpp b/dom/serviceworkers/ServiceWorkerParent.cpp new file mode 100644 index 0000000000..4e31344e92 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerParent.cpp @@ -0,0 +1,62 @@ +/* -*- 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 "ServiceWorkerParent.h" + +#include "ServiceWorkerCloneData.h" +#include "ServiceWorkerProxy.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/ClientState.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" + +namespace mozilla::dom { + +using mozilla::dom::ipc::StructuredCloneData; +using mozilla::ipc::IPCResult; + +void ServiceWorkerParent::ActorDestroy(ActorDestroyReason aReason) { + if (mProxy) { + mProxy->RevokeActor(this); + mProxy = nullptr; + } +} + +IPCResult ServiceWorkerParent::RecvTeardown() { + MaybeSendDelete(); + return IPC_OK(); +} + +IPCResult ServiceWorkerParent::RecvPostMessage( + const ClonedOrErrorMessageData& aClonedData, + const ClientInfoAndState& aSource) { + RefPtr<ServiceWorkerCloneData> data = new ServiceWorkerCloneData(); + data->CopyFromClonedMessageData(aClonedData); + + mProxy->PostMessage(std::move(data), ClientInfo(aSource.info()), + ClientState::FromIPC(aSource.state())); + + return IPC_OK(); +} + +ServiceWorkerParent::ServiceWorkerParent() : mDeleteSent(false) {} + +ServiceWorkerParent::~ServiceWorkerParent() { MOZ_DIAGNOSTIC_ASSERT(!mProxy); } + +void ServiceWorkerParent::Init(const IPCServiceWorkerDescriptor& aDescriptor) { + MOZ_DIAGNOSTIC_ASSERT(!mProxy); + mProxy = new ServiceWorkerProxy(ServiceWorkerDescriptor(aDescriptor)); + mProxy->Init(this); +} + +void ServiceWorkerParent::MaybeSendDelete() { + if (mDeleteSent) { + return; + } + mDeleteSent = true; + Unused << Send__delete__(this); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerParent.h b/dom/serviceworkers/ServiceWorkerParent.h new file mode 100644 index 0000000000..7224c632c7 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerParent.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerparent_h__ +#define mozilla_dom_serviceworkerparent_h__ + +#include "mozilla/dom/PServiceWorkerParent.h" + +namespace mozilla::dom { + +class IPCServiceWorkerDescriptor; +class ServiceWorkerProxy; + +class ServiceWorkerParent final : public PServiceWorkerParent { + RefPtr<ServiceWorkerProxy> mProxy; + bool mDeleteSent; + + ~ServiceWorkerParent(); + + // PServiceWorkerParent + void ActorDestroy(ActorDestroyReason aReason) override; + + mozilla::ipc::IPCResult RecvTeardown() override; + + mozilla::ipc::IPCResult RecvPostMessage( + const ClonedOrErrorMessageData& aClonedData, + const ClientInfoAndState& aSource) override; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerParent, override); + + ServiceWorkerParent(); + + void Init(const IPCServiceWorkerDescriptor& aDescriptor); + + void MaybeSendDelete(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerparent_h__ diff --git a/dom/serviceworkers/ServiceWorkerPrivate.cpp b/dom/serviceworkers/ServiceWorkerPrivate.cpp new file mode 100644 index 0000000000..d8fbe91d76 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp @@ -0,0 +1,1678 @@ +/* -*- 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 "ServiceWorkerPrivate.h" + +#include <utility> + +#include "MainThreadUtils.h" +#include "ServiceWorkerCloneData.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerRegistrationInfo.h" +#include "ServiceWorkerUtils.h" +#include "js/ErrorReport.h" +#include "mozIThirdPartyUtil.h" +#include "mozilla/Assertions.h" +#include "mozilla/CycleCollectedJSContext.h" // for MicroTaskRunnable +#include "mozilla/ErrorResult.h" +#include "mozilla/JSObjectHolder.h" +#include "mozilla/Maybe.h" +#include "mozilla/Preferences.h" +#include "mozilla/RemoteLazyInputStreamStorage.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ClientIPCTypes.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/dom/FetchEventOpChild.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/RemoteType.h" +#include "mozilla/dom/RemoteWorkerControllerChild.h" +#include "mozilla/dom/RemoteWorkerManager.h" // RemoteWorkerManager::GetRemoteType +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/extensions/WebExtensionPolicy.h" // WebExtensionPolicy +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/CookieJarSettings.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsICacheInfoChannel.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsINetworkInterceptController.h" +#include "nsINamed.h" +#include "nsIObserverService.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsISupportsImpl.h" +#include "nsIURI.h" +#include "nsIUploadChannel2.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsThreadUtils.h" + +#include "mozilla/dom/Client.h" +#include "mozilla/dom/FetchUtil.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/dom/NotificationEvent.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/PushEventBinding.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/WorkerDebugger.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "nsIReferrerInfo.h" + +extern mozilla::LazyLogModule sWorkerTelemetryLog; + +#ifdef LOG +# undef LOG +#endif +#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args); + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla::dom { + +uint32_t ServiceWorkerPrivate::sRunningServiceWorkers = 0; +uint32_t ServiceWorkerPrivate::sRunningServiceWorkersFetch = 0; +uint32_t ServiceWorkerPrivate::sRunningServiceWorkersMax = 0; +uint32_t ServiceWorkerPrivate::sRunningServiceWorkersFetchMax = 0; + +// Tracks the "dom.serviceWorkers.disable_open_click_delay" preference. Modified +// on main thread, read on worker threads. +// It is updated every time a "notificationclick" event is dispatched. While +// this is done without synchronization, at the worst, the thread will just get +// an older value within which a popup is allowed to be displayed, which will +// still be a valid value since it was set prior to dispatching the runnable. +Atomic<uint32_t> gDOMDisableOpenClickDelay(0); + +/** + * KeepAliveToken + */ +KeepAliveToken::KeepAliveToken(ServiceWorkerPrivate* aPrivate) + : mPrivate(aPrivate) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrivate); + mPrivate->AddToken(); +} + +KeepAliveToken::~KeepAliveToken() { + MOZ_ASSERT(NS_IsMainThread()); + mPrivate->ReleaseToken(); +} + +NS_IMPL_ISUPPORTS0(KeepAliveToken) + +/** + * RAIIActorPtrHolder + */ +ServiceWorkerPrivate::RAIIActorPtrHolder::RAIIActorPtrHolder( + already_AddRefed<RemoteWorkerControllerChild> aActor) + : mActor(aActor) { + AssertIsOnMainThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mActor->Manager()); +} + +ServiceWorkerPrivate::RAIIActorPtrHolder::~RAIIActorPtrHolder() { + AssertIsOnMainThread(); + + mDestructorPromiseHolder.ResolveIfExists(true, __func__); + + mActor->MaybeSendDelete(); +} + +RemoteWorkerControllerChild* +ServiceWorkerPrivate::RAIIActorPtrHolder::operator->() const { + AssertIsOnMainThread(); + + return get(); +} + +RemoteWorkerControllerChild* ServiceWorkerPrivate::RAIIActorPtrHolder::get() + const { + AssertIsOnMainThread(); + + return mActor.get(); +} + +RefPtr<GenericPromise> +ServiceWorkerPrivate::RAIIActorPtrHolder::OnDestructor() { + AssertIsOnMainThread(); + + return mDestructorPromiseHolder.Ensure(__func__); +} + +/** + * PendingFunctionEvent + */ +ServiceWorkerPrivate::PendingFunctionalEvent::PendingFunctionalEvent( + ServiceWorkerPrivate* aOwner, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration) + : mOwner(aOwner), mRegistration(std::move(aRegistration)) { + AssertIsOnMainThread(); + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mOwner->mInfo); + MOZ_ASSERT(mOwner->mInfo->State() == ServiceWorkerState::Activating); + MOZ_ASSERT(mRegistration); +} + +ServiceWorkerPrivate::PendingFunctionalEvent::~PendingFunctionalEvent() { + AssertIsOnMainThread(); +} + +ServiceWorkerPrivate::PendingPushEvent::PendingPushEvent( + ServiceWorkerPrivate* aOwner, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ServiceWorkerPushEventOpArgs&& aArgs) + : PendingFunctionalEvent(aOwner, std::move(aRegistration)), + mArgs(std::move(aArgs)) { + AssertIsOnMainThread(); +} + +nsresult ServiceWorkerPrivate::PendingPushEvent::Send() { + AssertIsOnMainThread(); + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mOwner->mInfo); + + return mOwner->SendPushEventInternal(std::move(mRegistration), + std::move(mArgs)); +} + +ServiceWorkerPrivate::PendingFetchEvent::PendingFetchEvent( + ServiceWorkerPrivate* aOwner, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel>&& aChannel, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises) + : PendingFunctionalEvent(aOwner, std::move(aRegistration)), + mArgs(std::move(aArgs)), + mChannel(std::move(aChannel)), + mPreloadResponseReadyPromises(std::move(aPreloadResponseReadyPromises)) { + AssertIsOnMainThread(); + MOZ_ASSERT(mChannel); +} + +nsresult ServiceWorkerPrivate::PendingFetchEvent::Send() { + AssertIsOnMainThread(); + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mOwner->mInfo); + + return mOwner->SendFetchEventInternal( + std::move(mRegistration), std::move(mArgs), std::move(mChannel), + std::move(mPreloadResponseReadyPromises)); +} + +ServiceWorkerPrivate::PendingFetchEvent::~PendingFetchEvent() { + AssertIsOnMainThread(); + + if (NS_WARN_IF(mChannel)) { + mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED); + } +} + +namespace { + +class HeaderFiller final : public nsIHttpHeaderVisitor { + public: + NS_DECL_ISUPPORTS + + explicit HeaderFiller(HeadersGuardEnum aGuard) + : mInternalHeaders(new InternalHeaders(aGuard)) { + MOZ_ASSERT(mInternalHeaders); + } + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + ErrorResult result; + mInternalHeaders->Append(aHeader, aValue, result); + + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + return NS_OK; + } + + RefPtr<InternalHeaders> Extract() { + return RefPtr<InternalHeaders>(std::move(mInternalHeaders)); + } + + private: + ~HeaderFiller() = default; + + RefPtr<InternalHeaders> mInternalHeaders; +}; + +NS_IMPL_ISUPPORTS(HeaderFiller, nsIHttpHeaderVisitor) + +Result<IPCInternalRequest, nsresult> GetIPCInternalRequest( + nsIInterceptedChannel* aChannel) { + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> uri; + MOZ_TRY(aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri))); + + nsCOMPtr<nsIURI> uriNoFragment; + MOZ_TRY(NS_GetURIWithoutRef(uri, getter_AddRefs(uriNoFragment))); + + nsCOMPtr<nsIChannel> underlyingChannel; + MOZ_TRY(aChannel->GetChannel(getter_AddRefs(underlyingChannel))); + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(underlyingChannel); + MOZ_ASSERT(httpChannel, "How come we don't have an HTTP channel?"); + + nsCOMPtr<nsIHttpChannelInternal> internalChannel = + do_QueryInterface(httpChannel); + NS_ENSURE_TRUE(internalChannel, Err(NS_ERROR_NOT_AVAILABLE)); + + nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel = + do_QueryInterface(underlyingChannel); + + nsAutoCString spec; + MOZ_TRY(uriNoFragment->GetSpec(spec)); + + nsAutoCString fragment; + MOZ_TRY(uri->GetRef(fragment)); + + nsAutoCString method; + MOZ_TRY(httpChannel->GetRequestMethod(method)); + + // This is safe due to static_asserts in ServiceWorkerManager.cpp + uint32_t cacheModeInt; + MOZ_ALWAYS_SUCCEEDS(internalChannel->GetFetchCacheMode(&cacheModeInt)); + RequestCache cacheMode = static_cast<RequestCache>(cacheModeInt); + + RequestMode requestMode = + InternalRequest::MapChannelToRequestMode(underlyingChannel); + + // This is safe due to static_asserts in ServiceWorkerManager.cpp + uint32_t redirectMode; + MOZ_ALWAYS_SUCCEEDS(internalChannel->GetRedirectMode(&redirectMode)); + RequestRedirect requestRedirect = static_cast<RequestRedirect>(redirectMode); + + RequestCredentials requestCredentials = + InternalRequest::MapChannelToRequestCredentials(underlyingChannel); + + nsAutoString referrer; + ReferrerPolicy referrerPolicy = ReferrerPolicy::_empty; + + nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo(); + if (referrerInfo) { + referrerPolicy = referrerInfo->ReferrerPolicy(); + Unused << referrerInfo->GetComputedReferrerSpec(referrer); + } + + uint32_t loadFlags; + MOZ_TRY(underlyingChannel->GetLoadFlags(&loadFlags)); + + nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->LoadInfo(); + nsContentPolicyType contentPolicyType = loadInfo->InternalContentPolicyType(); + + nsAutoString integrity; + MOZ_TRY(internalChannel->GetIntegrityMetadata(integrity)); + + RefPtr<HeaderFiller> headerFiller = + MakeRefPtr<HeaderFiller>(HeadersGuardEnum::Request); + MOZ_TRY(httpChannel->VisitNonDefaultRequestHeaders(headerFiller)); + + RefPtr<InternalHeaders> internalHeaders = headerFiller->Extract(); + + ErrorResult result; + internalHeaders->SetGuard(HeadersGuardEnum::Immutable, result); + if (NS_WARN_IF(result.Failed())) { + return Err(result.StealNSResult()); + } + + nsTArray<HeadersEntry> ipcHeaders; + HeadersGuardEnum ipcHeadersGuard; + internalHeaders->ToIPC(ipcHeaders, ipcHeadersGuard); + + nsAutoCString alternativeDataType; + if (cacheInfoChannel && + !cacheInfoChannel->PreferredAlternativeDataTypes().IsEmpty()) { + // TODO: the internal request probably needs all the preferred types. + alternativeDataType.Assign( + cacheInfoChannel->PreferredAlternativeDataTypes()[0].type()); + } + + Maybe<PrincipalInfo> principalInfo; + Maybe<PrincipalInfo> interceptionPrincipalInfo; + if (loadInfo->TriggeringPrincipal()) { + principalInfo.emplace(); + interceptionPrincipalInfo.emplace(); + MOZ_ALWAYS_SUCCEEDS(PrincipalToPrincipalInfo( + loadInfo->TriggeringPrincipal(), principalInfo.ptr())); + MOZ_ALWAYS_SUCCEEDS(PrincipalToPrincipalInfo( + loadInfo->TriggeringPrincipal(), interceptionPrincipalInfo.ptr())); + } + + nsTArray<RedirectHistoryEntryInfo> redirectChain; + for (const nsCOMPtr<nsIRedirectHistoryEntry>& redirectEntry : + loadInfo->RedirectChain()) { + RedirectHistoryEntryInfo* entry = redirectChain.AppendElement(); + MOZ_ALWAYS_SUCCEEDS(RHEntryToRHEntryInfo(redirectEntry, entry)); + } + + bool isThirdPartyChannel; + // ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); + nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil = + do_GetService(THIRDPARTYUTIL_CONTRACTID); + if (thirdPartyUtil) { + nsCOMPtr<nsIURI> uri; + MOZ_TRY(underlyingChannel->GetURI(getter_AddRefs(uri))); + MOZ_TRY(thirdPartyUtil->IsThirdPartyChannel(underlyingChannel, uri, + &isThirdPartyChannel)); + } + + // Note: all the arguments are copied rather than moved, which would be more + // efficient, because there's no move-friendly constructor generated. + return IPCInternalRequest( + method, {spec}, ipcHeadersGuard, ipcHeaders, Nothing(), -1, + alternativeDataType, contentPolicyType, referrer, referrerPolicy, + requestMode, requestCredentials, cacheMode, requestRedirect, integrity, + fragment, principalInfo, interceptionPrincipalInfo, contentPolicyType, + redirectChain, isThirdPartyChannel); +} + +nsresult MaybeStoreStreamForBackgroundThread(nsIInterceptedChannel* aChannel, + IPCInternalRequest& aIPCRequest) { + nsCOMPtr<nsIChannel> channel; + MOZ_ALWAYS_SUCCEEDS(aChannel->GetChannel(getter_AddRefs(channel))); + + Maybe<BodyStreamVariant> body; + nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(channel); + + if (uploadChannel) { + nsCOMPtr<nsIInputStream> uploadStream; + MOZ_TRY(uploadChannel->CloneUploadStream(&aIPCRequest.bodySize(), + getter_AddRefs(uploadStream))); + + if (uploadStream) { + Maybe<BodyStreamVariant>& body = aIPCRequest.body(); + body.emplace(ParentToParentStream()); + + MOZ_TRY( + nsID::GenerateUUIDInPlace(body->get_ParentToParentStream().uuid())); + + auto storageOrErr = RemoteLazyInputStreamStorage::Get(); + if (NS_WARN_IF(storageOrErr.isErr())) { + return storageOrErr.unwrapErr(); + } + + auto storage = storageOrErr.unwrap(); + storage->AddStream(uploadStream, body->get_ParentToParentStream().uuid()); + } + } + + return NS_OK; +} + +} // anonymous namespace + +/** + * ServiceWorkerPrivate + */ +ServiceWorkerPrivate::ServiceWorkerPrivate(ServiceWorkerInfo* aInfo) + : mInfo(aInfo), mDebuggerCount(0), mTokenCount(0) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aInfo); + MOZ_ASSERT(!mControllerChild); + + mIdleWorkerTimer = NS_NewTimer(); + MOZ_ASSERT(mIdleWorkerTimer); + + // Assert in all debug builds as well as non-debug Nightly and Dev Edition. +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(Initialize())); +#else + MOZ_ALWAYS_SUCCEEDS(Initialize()); +#endif +} + +ServiceWorkerPrivate::~ServiceWorkerPrivate() { + MOZ_ASSERT(!mTokenCount); + MOZ_ASSERT(!mInfo); + MOZ_ASSERT(!mControllerChild); + MOZ_ASSERT(mIdlePromiseHolder.IsEmpty()); + + mIdleWorkerTimer->Cancel(); +} + +nsresult ServiceWorkerPrivate::Initialize() { + AssertIsOnMainThread(); + MOZ_ASSERT(mInfo); + + nsCOMPtr<nsIPrincipal> principal = mInfo->Principal(); + + nsCOMPtr<nsIURI> uri; + auto* basePrin = BasePrincipal::Cast(principal); + nsresult rv = basePrin->GetURI(getter_AddRefs(uri)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!uri)) { + return NS_ERROR_FAILURE; + } + + URIParams baseScriptURL; + SerializeURI(uri, baseScriptURL); + + nsString id; + rv = mInfo->GetId(id); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + PrincipalInfo principalInfo; + rv = PrincipalToPrincipalInfo(principal, &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + if (NS_WARN_IF(!swm)) { + return NS_ERROR_DOM_ABORT_ERR; + } + + RefPtr<ServiceWorkerRegistrationInfo> regInfo = + swm->GetRegistration(principal, mInfo->Scope()); + + if (NS_WARN_IF(!regInfo)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + net::CookieJarSettings::Create(principal); + MOZ_ASSERT(cookieJarSettings); + + // We can populate the partitionKey from the originAttribute of the principal + // if it has partitionKey set. It's because ServiceWorker is using the foreign + // partitioned principal and it implies that it's a third-party service + // worker. So, the cookieJarSettings can directly use the partitionKey from + // it. For first-party case, we can populate the partitionKey from the + // principal URI. + if (!principal->OriginAttributesRef().mPartitionKey.IsEmpty()) { + net::CookieJarSettings::Cast(cookieJarSettings) + ->SetPartitionKey(principal->OriginAttributesRef().mPartitionKey); + } else { + net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); + } + + net::CookieJarSettingsArgs cjsData; + net::CookieJarSettings::Cast(cookieJarSettings)->Serialize(cjsData); + + nsCOMPtr<nsIPrincipal> partitionedPrincipal; + rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker( + principal, cookieJarSettings, getter_AddRefs(partitionedPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + PrincipalInfo partitionedPrincipalInfo; + rv = + PrincipalToPrincipalInfo(partitionedPrincipal, &partitionedPrincipalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + StorageAccess storageAccess = + StorageAllowedForServiceWorker(principal, cookieJarSettings); + + ServiceWorkerData serviceWorkerData; + serviceWorkerData.cacheName() = mInfo->CacheName(); + serviceWorkerData.loadFlags() = static_cast<uint32_t>( + mInfo->GetImportsLoadFlags() | nsIChannel::LOAD_BYPASS_SERVICE_WORKER); + serviceWorkerData.id() = std::move(id); + + nsAutoCString domain; + rv = uri->GetHost(domain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + auto remoteType = RemoteWorkerManager::GetRemoteType( + principal, WorkerKind::WorkerKindService); + if (NS_WARN_IF(remoteType.isErr())) { + return remoteType.unwrapErr(); + } + + // Determine if the service worker is registered under a third-party context + // by checking if it's running under a partitioned principal. + bool isThirdPartyContextToTopWindow = + !principal->OriginAttributesRef().mPartitionKey.IsEmpty(); + + mRemoteWorkerData = RemoteWorkerData( + NS_ConvertUTF8toUTF16(mInfo->ScriptSpec()), baseScriptURL, baseScriptURL, + /* name */ VoidString(), + /* loading principal */ principalInfo, principalInfo, + partitionedPrincipalInfo, + /* useRegularPrincipal */ true, + + // ServiceWorkers run as first-party, no storage-access permission needed. + /* hasStorageAccessPermissionGranted */ false, + + cjsData, domain, + /* isSecureContext */ true, + /* clientInfo*/ Nothing(), + + // The RemoteWorkerData CTOR doesn't allow to set the referrerInfo via + // already_AddRefed<>. Let's set it to null. + /* referrerInfo */ nullptr, + + storageAccess, isThirdPartyContextToTopWindow, + nsContentUtils::ShouldResistFingerprinting_dangerous( + principal, + "Service Workers exist outside a Document or Channel; as a property " + "of the domain (and origin attributes). We don't have a " + "CookieJarSettings to perform the nested check, but we can rely on" + "the FPI/dFPI partition key check."), + // Origin trials are associated to a window, so it doesn't make sense on + // service workers. + OriginTrials(), std::move(serviceWorkerData), regInfo->AgentClusterId(), + remoteType.unwrap()); + + mRemoteWorkerData.referrerInfo() = MakeAndAddRef<ReferrerInfo>(); + + // This fills in the rest of mRemoteWorkerData.serviceWorkerData(). + RefreshRemoteWorkerData(regInfo); + + return NS_OK; +} + +nsresult ServiceWorkerPrivate::CheckScriptEvaluation( + RefPtr<LifeCycleEventCallback> aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCallback); + + RefPtr<ServiceWorkerPrivate> self = this; + + /** + * We need to capture the actor associated with the current Service Worker so + * we can terminate it if script evaluation failed. + */ + nsresult rv = SpawnWorkerIfNeeded(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aCallback->SetResult(false); + aCallback->Run(); + + return rv; + } + + MOZ_ASSERT(mControllerChild); + + RefPtr<RAIIActorPtrHolder> holder = mControllerChild; + + return ExecServiceWorkerOp( + ServiceWorkerCheckScriptEvaluationOpArgs(), + [self = std::move(self), holder = std::move(holder), + callback = aCallback](ServiceWorkerOpResult&& aResult) mutable { + if (aResult.type() == ServiceWorkerOpResult:: + TServiceWorkerCheckScriptEvaluationOpResult) { + auto& result = + aResult.get_ServiceWorkerCheckScriptEvaluationOpResult(); + + if (result.workerScriptExecutedSuccessfully()) { + self->SetHandlesFetch(result.fetchHandlerWasAdded()); + if (self->mHandlesFetch == Unknown) { + self->mHandlesFetch = + result.fetchHandlerWasAdded() ? Enabled : Disabled; + // Update telemetry for # of running SW - the already-running SW + // handles fetch + if (self->mHandlesFetch == Enabled) { + self->UpdateRunning(0, 1); + } + } + + callback->SetResult(result.workerScriptExecutedSuccessfully()); + callback->Run(); + return; + } + } + + /** + * If script evaluation failed, first terminate the Service Worker + * before invoking the callback. + */ + MOZ_ASSERT_IF(aResult.type() == ServiceWorkerOpResult::Tnsresult, + NS_FAILED(aResult.get_nsresult())); + + // If a termination operation was already issued using `holder`... + if (self->mControllerChild != holder) { + holder->OnDestructor()->Then( + GetCurrentSerialEventTarget(), __func__, + [callback = std::move(callback)]( + const GenericPromise::ResolveOrRejectValue&) { + callback->SetResult(false); + callback->Run(); + }); + + return; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + MOZ_ASSERT(swm); + + auto shutdownStateId = swm->MaybeInitServiceWorkerShutdownProgress(); + + RefPtr<GenericNonExclusivePromise> promise = + self->ShutdownInternal(shutdownStateId); + + swm->BlockShutdownOn(promise, shutdownStateId); + + promise->Then( + GetCurrentSerialEventTarget(), __func__, + [callback = std::move(callback)]( + const GenericNonExclusivePromise::ResolveOrRejectValue&) { + callback->SetResult(false); + callback->Run(); + }); + }, + [callback = aCallback] { + callback->SetResult(false); + callback->Run(); + }); +} + +nsresult ServiceWorkerPrivate::SendMessageEvent( + RefPtr<ServiceWorkerCloneData>&& aData, + const ClientInfoAndState& aClientInfoAndState) { + AssertIsOnMainThread(); + MOZ_ASSERT(aData); + + auto scopeExit = MakeScopeExit([&] { Shutdown(); }); + + PBackgroundChild* bgChild = BackgroundChild::GetForCurrentThread(); + + if (NS_WARN_IF(!bgChild)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + ServiceWorkerMessageEventOpArgs args; + args.clientInfoAndState() = aClientInfoAndState; + if (!aData->BuildClonedMessageData(args.clonedData())) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + scopeExit.release(); + + return ExecServiceWorkerOp( + std::move(args), [](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); + }); +} + +nsresult ServiceWorkerPrivate::SendLifeCycleEvent( + const nsAString& aEventType, RefPtr<LifeCycleEventCallback> aCallback) { + AssertIsOnMainThread(); + MOZ_ASSERT(aCallback); + + return ExecServiceWorkerOp( + ServiceWorkerLifeCycleEventOpArgs(nsString(aEventType)), + [callback = aCallback](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); + + callback->SetResult(NS_SUCCEEDED(aResult.get_nsresult())); + callback->Run(); + }, + [callback = aCallback] { + callback->SetResult(false); + callback->Run(); + }); +} + +nsresult ServiceWorkerPrivate::SendPushEvent( + const nsAString& aMessageId, const Maybe<nsTArray<uint8_t>>& aData, + RefPtr<ServiceWorkerRegistrationInfo> aRegistration) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInfo); + MOZ_ASSERT(aRegistration); + + ServiceWorkerPushEventOpArgs args; + args.messageId() = nsString(aMessageId); + + if (aData) { + args.data() = aData.ref(); + } else { + args.data() = void_t(); + } + + if (mInfo->State() == ServiceWorkerState::Activating) { + UniquePtr<PendingFunctionalEvent> pendingEvent = + MakeUnique<PendingPushEvent>(this, std::move(aRegistration), + std::move(args)); + + mPendingFunctionalEvents.AppendElement(std::move(pendingEvent)); + + return NS_OK; + } + + MOZ_ASSERT(mInfo->State() == ServiceWorkerState::Activated); + + return SendPushEventInternal(std::move(aRegistration), std::move(args)); +} + +nsresult ServiceWorkerPrivate::SendPushEventInternal( + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ServiceWorkerPushEventOpArgs&& aArgs) { + AssertIsOnMainThread(); + MOZ_ASSERT(aRegistration); + + return ExecServiceWorkerOp( + std::move(aArgs), + [registration = aRegistration](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); + + registration->MaybeScheduleTimeCheckAndUpdate(); + }, + [registration = aRegistration]() { + registration->MaybeScheduleTimeCheckAndUpdate(); + }); +} + +nsresult ServiceWorkerPrivate::SendPushSubscriptionChangeEvent() { + AssertIsOnMainThread(); + + return ExecServiceWorkerOp( + ServiceWorkerPushSubscriptionChangeEventOpArgs(), + [](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); + }); +} + +nsresult ServiceWorkerPrivate::SendNotificationEvent( + const nsAString& aEventName, const nsAString& aID, const nsAString& aTitle, + const nsAString& aDir, const nsAString& aLang, const nsAString& aBody, + const nsAString& aTag, const nsAString& aIcon, const nsAString& aData, + const nsAString& aBehavior, const nsAString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aEventName.EqualsLiteral(NOTIFICATION_CLICK_EVENT_NAME)) { + gDOMDisableOpenClickDelay = + Preferences::GetInt("dom.serviceWorkers.disable_open_click_delay"); + } else if (!aEventName.EqualsLiteral(NOTIFICATION_CLOSE_EVENT_NAME)) { + MOZ_ASSERT_UNREACHABLE("Invalid notification event name"); + return NS_ERROR_FAILURE; + } + + ServiceWorkerNotificationEventOpArgs args; + args.eventName() = nsString(aEventName); + args.id() = nsString(aID); + args.title() = nsString(aTitle); + args.dir() = nsString(aDir); + args.lang() = nsString(aLang); + args.body() = nsString(aBody); + args.tag() = nsString(aTag); + args.icon() = nsString(aIcon); + args.data() = nsString(aData); + args.behavior() = nsString(aBehavior); + args.scope() = nsString(aScope); + args.disableOpenClickDelay() = gDOMDisableOpenClickDelay; + + return ExecServiceWorkerOp( + std::move(args), [](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); + }); +} + +nsresult ServiceWorkerPrivate::SendFetchEvent( + nsCOMPtr<nsIInterceptedChannel> aChannel, nsILoadGroup* aLoadGroup, + const nsAString& aClientId, const nsAString& aResultingClientId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aChannel); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (NS_WARN_IF(!mInfo || !swm)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIChannel> channel; + nsresult rv = aChannel->GetChannel(getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(rv, rv); + bool isNonSubresourceRequest = + nsContentUtils::IsNonSubresourceRequest(channel); + + RefPtr<ServiceWorkerRegistrationInfo> registration; + if (isNonSubresourceRequest) { + registration = swm->GetRegistration(mInfo->Principal(), mInfo->Scope()); + } else { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + + // We'll check for a null registration below rather than an error code here. + Unused << swm->GetClientRegistration(loadInfo->GetClientInfo().ref(), + getter_AddRefs(registration)); + } + + // Its possible the registration is removed between starting the interception + // and actually dispatching the fetch event. In these cases we simply + // want to restart the original network request. Since this is a normal + // condition we handle the reset here instead of returning an error which + // would in turn trigger a console report. + if (!registration) { + nsresult rv = aChannel->ResetInterception(false); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to resume intercepted network request"); + aChannel->CancelInterception(rv); + } + return NS_OK; + } + + // Handle Fetch algorithm - step 16. If the service worker didn't register + // any fetch event handlers, then abort the interception and maybe trigger + // the soft update algorithm. + if (!mInfo->HandlesFetch()) { + nsresult rv = aChannel->ResetInterception(false); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to resume intercepted network request"); + aChannel->CancelInterception(rv); + } + + // Trigger soft updates if necessary. + registration->MaybeScheduleTimeCheckAndUpdate(); + + return NS_OK; + } + + auto scopeExit = MakeScopeExit([&] { + aChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED); + Shutdown(); + }); + + IPCInternalRequest request; + MOZ_TRY_VAR(request, GetIPCInternalRequest(aChannel)); + + scopeExit.release(); + + bool preloadNavigation = isNonSubresourceRequest && + request.method().LowerCaseEqualsASCII("get") && + registration->GetNavigationPreloadState().enabled(); + + RefPtr<FetchServicePromises> preloadResponsePromises; + if (preloadNavigation) { + preloadResponsePromises = SetupNavigationPreload(aChannel, registration); + } + + ParentToParentServiceWorkerFetchEventOpArgs args( + ServiceWorkerFetchEventOpArgsCommon( + mInfo->ScriptSpec(), request, nsString(aClientId), + nsString(aResultingClientId), isNonSubresourceRequest, + preloadNavigation, mInfo->TestingInjectCancellation()), + Nothing(), Nothing()); + + if (mInfo->State() == ServiceWorkerState::Activating) { + UniquePtr<PendingFunctionalEvent> pendingEvent = + MakeUnique<PendingFetchEvent>(this, std::move(registration), + std::move(args), std::move(aChannel), + std::move(preloadResponsePromises)); + + mPendingFunctionalEvents.AppendElement(std::move(pendingEvent)); + + return NS_OK; + } + + MOZ_ASSERT(mInfo->State() == ServiceWorkerState::Activated); + + return SendFetchEventInternal(std::move(registration), std::move(args), + std::move(aChannel), + std::move(preloadResponsePromises)); +} + +nsresult ServiceWorkerPrivate::SendFetchEventInternal( + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel>&& aChannel, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises) { + AssertIsOnMainThread(); + + auto scopeExit = MakeScopeExit([&] { Shutdown(); }); + + if (NS_WARN_IF(!mInfo)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + MOZ_TRY(SpawnWorkerIfNeeded()); + MOZ_TRY(MaybeStoreStreamForBackgroundThread( + aChannel, aArgs.common().internalRequest())); + + scopeExit.release(); + + MOZ_ASSERT(mControllerChild); + + RefPtr<RAIIActorPtrHolder> holder = mControllerChild; + + FetchEventOpChild::SendFetchEvent( + mControllerChild->get(), std::move(aArgs), std::move(aChannel), + std::move(aRegistration), std::move(aPreloadResponseReadyPromises), + CreateEventKeepAliveToken()) + ->Then(GetCurrentSerialEventTarget(), __func__, + [holder = std::move(holder)]( + const GenericPromise::ResolveOrRejectValue& aResult) { + Unused << NS_WARN_IF(aResult.IsReject()); + }); + + return NS_OK; +} + +Result<RefPtr<ServiceWorkerPrivate::PromiseExtensionWorkerHasListener>, + nsresult> +ServiceWorkerPrivate::WakeForExtensionAPIEvent( + const nsAString& aExtensionAPINamespace, + const nsAString& aExtensionAPIEventName) { + AssertIsOnMainThread(); + + ServiceWorkerExtensionAPIEventOpArgs args; + args.apiNamespace() = nsString(aExtensionAPINamespace); + args.apiEventName() = nsString(aExtensionAPIEventName); + + auto promise = + MakeRefPtr<PromiseExtensionWorkerHasListener::Private>(__func__); + + nsresult rv = ExecServiceWorkerOp( + std::move(args), + [promise](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT( + aResult.type() == + ServiceWorkerOpResult::TServiceWorkerExtensionAPIEventOpResult); + auto& result = aResult.get_ServiceWorkerExtensionAPIEventOpResult(); + promise->Resolve(result.extensionAPIEventListenerWasAdded(), __func__); + }, + [promise]() { promise->Reject(NS_ERROR_FAILURE, __func__); }); + + if (NS_FAILED(rv)) { + promise->Reject(rv, __func__); + } + + RefPtr<PromiseExtensionWorkerHasListener> outPromise(promise); + return outPromise; +} + +nsresult ServiceWorkerPrivate::SpawnWorkerIfNeeded() { + AssertIsOnMainThread(); + + if (mControllerChild) { + RenewKeepAliveToken(); + return NS_OK; + } + + if (!mInfo) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + mServiceWorkerLaunchTimeStart = TimeStamp::Now(); + + PBackgroundChild* bgChild = BackgroundChild::GetForCurrentThread(); + + if (NS_WARN_IF(!bgChild)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + // If the worker principal is an extension principal, then we should not spawn + // a worker if there is no WebExtensionPolicy associated to that principal + // or if the WebExtensionPolicy is not active. + auto* principal = mInfo->Principal(); + if (principal->SchemeIs("moz-extension")) { + auto* addonPolicy = BasePrincipal::Cast(principal)->AddonPolicy(); + if (!addonPolicy || !addonPolicy->Active()) { + NS_WARNING( + "Trying to wake up a service worker for a disabled webextension."); + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + if (NS_WARN_IF(!swm)) { + return NS_ERROR_DOM_ABORT_ERR; + } + + RefPtr<ServiceWorkerRegistrationInfo> regInfo = + swm->GetRegistration(principal, mInfo->Scope()); + + if (NS_WARN_IF(!regInfo)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + RefreshRemoteWorkerData(regInfo); + + RefPtr<RemoteWorkerControllerChild> controllerChild = + new RemoteWorkerControllerChild(this); + + if (NS_WARN_IF(!bgChild->SendPRemoteWorkerControllerConstructor( + controllerChild, mRemoteWorkerData))) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + /** + * Manually `AddRef()` because `DeallocPRemoteWorkerControllerChild()` + * calls `Release()` and the `AllocPRemoteWorkerControllerChild()` function + * is not called. + */ + // NOLINTNEXTLINE(readability-redundant-smartptr-get) + controllerChild.get()->AddRef(); + + mControllerChild = new RAIIActorPtrHolder(controllerChild.forget()); + + // Update Running count here because we may Terminate before we get + // CreationSucceeded(). We'll update if it handles Fetch if that changes + // ( + UpdateRunning(1, mHandlesFetch == Enabled ? 1 : 0); + + return NS_OK; +} + +void ServiceWorkerPrivate::TerminateWorker() { + MOZ_ASSERT(NS_IsMainThread()); + mIdleWorkerTimer->Cancel(); + mIdleKeepAliveToken = nullptr; + Shutdown(); +} + +void ServiceWorkerPrivate::NoteDeadServiceWorkerInfo() { + MOZ_ASSERT(NS_IsMainThread()); + + TerminateWorker(); + mInfo = nullptr; +} + +void ServiceWorkerPrivate::UpdateState(ServiceWorkerState aState) { + AssertIsOnMainThread(); + + if (!mControllerChild) { + return; + } + + nsresult rv = ExecServiceWorkerOp( + ServiceWorkerUpdateStateOpArgs(aState), + [](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); + }); + + if (NS_WARN_IF(NS_FAILED(rv))) { + Shutdown(); + return; + } + + if (aState != ServiceWorkerState::Activated) { + return; + } + + for (auto& event : mPendingFunctionalEvents) { + Unused << NS_WARN_IF(NS_FAILED(event->Send())); + } + + mPendingFunctionalEvents.Clear(); +} + +nsresult ServiceWorkerPrivate::GetDebugger(nsIWorkerDebugger** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResult); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult ServiceWorkerPrivate::AttachDebugger() { + MOZ_ASSERT(NS_IsMainThread()); + + // When the first debugger attaches to a worker, we spawn a worker if needed, + // and cancel the idle timeout. The idle timeout should not be reset until + // the last debugger detached from the worker. + if (!mDebuggerCount) { + nsresult rv = SpawnWorkerIfNeeded(); + NS_ENSURE_SUCCESS(rv, rv); + + /** + * Renewing the idle KeepAliveToken for spawning workers happens + * asynchronously, rather than synchronously. + * The asynchronous renewal is because the actual spawning of workers occurs + * in a content process, so we will only renew once notified that the worker + * has been successfully created + * + * This means that the DevTools way of starting up a worker by calling + * `AttachDebugger` immediately followed by `DetachDebugger` will spawn and + * immediately terminate a worker (because `mTokenCount` is possibly 0 + * due to the idle KeepAliveToken being created asynchronously). So, just + * renew the KeepAliveToken right now. + */ + RenewKeepAliveToken(); + mIdleWorkerTimer->Cancel(); + } + + ++mDebuggerCount; + + return NS_OK; +} + +nsresult ServiceWorkerPrivate::DetachDebugger() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mDebuggerCount) { + return NS_ERROR_UNEXPECTED; + } + + --mDebuggerCount; + + // When the last debugger detaches from a worker, we either reset the idle + // timeout, or terminate the worker if there are no more active tokens. + if (!mDebuggerCount) { + if (mTokenCount) { + ResetIdleTimeout(); + } else { + TerminateWorker(); + } + } + + return NS_OK; +} + +bool ServiceWorkerPrivate::IsIdle() const { + MOZ_ASSERT(NS_IsMainThread()); + return mTokenCount == 0 || (mTokenCount == 1 && mIdleKeepAliveToken); +} + +RefPtr<GenericPromise> ServiceWorkerPrivate::GetIdlePromise() { +#ifdef DEBUG + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!IsIdle()); + MOZ_ASSERT(!mIdlePromiseObtained, "Idle promise may only be obtained once!"); + mIdlePromiseObtained = true; +#endif + + return mIdlePromiseHolder.Ensure(__func__); +} + +namespace { + +class ServiceWorkerPrivateTimerCallback final : public nsITimerCallback, + public nsINamed { + public: + using Method = void (ServiceWorkerPrivate::*)(nsITimer*); + + ServiceWorkerPrivateTimerCallback(ServiceWorkerPrivate* aServiceWorkerPrivate, + Method aMethod) + : mServiceWorkerPrivate(aServiceWorkerPrivate), mMethod(aMethod) {} + + NS_IMETHOD + Notify(nsITimer* aTimer) override { + (mServiceWorkerPrivate->*mMethod)(aTimer); + mServiceWorkerPrivate = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetName(nsACString& aName) override { + aName.AssignLiteral("ServiceWorkerPrivateTimerCallback"); + return NS_OK; + } + + private: + ~ServiceWorkerPrivateTimerCallback() = default; + + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + Method mMethod; + + NS_DECL_THREADSAFE_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(ServiceWorkerPrivateTimerCallback, nsITimerCallback, + nsINamed); + +} // anonymous namespace + +void ServiceWorkerPrivate::NoteIdleWorkerCallback(nsITimer* aTimer) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(aTimer == mIdleWorkerTimer, "Invalid timer!"); + + // Release ServiceWorkerPrivate's token, since the grace period has ended. + mIdleKeepAliveToken = nullptr; + + if (mControllerChild) { + // If we still have a living worker at this point it means that either there + // are pending waitUntil promises or the worker is doing some long-running + // computation. Wait a bit more until we forcibly terminate the worker. + uint32_t timeout = + Preferences::GetInt("dom.serviceWorkers.idle_extended_timeout"); + nsCOMPtr<nsITimerCallback> cb = new ServiceWorkerPrivateTimerCallback( + this, &ServiceWorkerPrivate::TerminateWorkerCallback); + DebugOnly<nsresult> rv = mIdleWorkerTimer->InitWithCallback( + cb, timeout, nsITimer::TYPE_ONE_SHOT); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void ServiceWorkerPrivate::TerminateWorkerCallback(nsITimer* aTimer) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(aTimer == this->mIdleWorkerTimer, "Invalid timer!"); + + // mInfo must be non-null at this point because NoteDeadServiceWorkerInfo + // which zeroes it calls TerminateWorker which cancels our timer which will + // ensure we don't get invoked even if the nsTimerEvent is in the event queue. + ServiceWorkerManager::LocalizeAndReportToAllClients( + mInfo->Scope(), "ServiceWorkerGraceTimeoutTermination", + nsTArray<nsString>{NS_ConvertUTF8toUTF16(mInfo->Scope())}); + + TerminateWorker(); +} + +void ServiceWorkerPrivate::RenewKeepAliveToken() { + // We should have an active worker if we're renewing the keep alive token. + MOZ_ASSERT(mControllerChild); + + // If there is at least one debugger attached to the worker, the idle worker + // timeout was canceled when the first debugger attached to the worker. It + // should not be reset until the last debugger detaches from the worker. + if (!mDebuggerCount) { + ResetIdleTimeout(); + } + + if (!mIdleKeepAliveToken) { + mIdleKeepAliveToken = new KeepAliveToken(this); + } +} + +void ServiceWorkerPrivate::ResetIdleTimeout() { + uint32_t timeout = Preferences::GetInt("dom.serviceWorkers.idle_timeout"); + nsCOMPtr<nsITimerCallback> cb = new ServiceWorkerPrivateTimerCallback( + this, &ServiceWorkerPrivate::NoteIdleWorkerCallback); + DebugOnly<nsresult> rv = + mIdleWorkerTimer->InitWithCallback(cb, timeout, nsITimer::TYPE_ONE_SHOT); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void ServiceWorkerPrivate::AddToken() { + MOZ_ASSERT(NS_IsMainThread()); + ++mTokenCount; +} + +void ServiceWorkerPrivate::ReleaseToken() { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(mTokenCount > 0); + --mTokenCount; + + if (IsIdle()) { + mIdlePromiseHolder.ResolveIfExists(true, __func__); + + if (!mTokenCount) { + TerminateWorker(); + } + + // mInfo can be nullptr here if NoteDeadServiceWorkerInfo() is called while + // the KeepAliveToken is being proxy released as a runnable. + else if (mInfo) { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->WorkerIsIdle(mInfo); + } + } + } +} + +already_AddRefed<KeepAliveToken> +ServiceWorkerPrivate::CreateEventKeepAliveToken() { + MOZ_ASSERT(NS_IsMainThread()); + + // When the WorkerPrivate is in a separate process, we first hold a normal + // KeepAliveToken. Then, after we're notified that the worker is alive, we + // create the idle KeepAliveToken. + MOZ_ASSERT(mIdleKeepAliveToken || mControllerChild); + + RefPtr<KeepAliveToken> ref = new KeepAliveToken(this); + return ref.forget(); +} + +void ServiceWorkerPrivate::SetHandlesFetch(bool aValue) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!mInfo)) { + return; + } + + mInfo->SetHandlesFetch(aValue); +} + +RefPtr<GenericPromise> ServiceWorkerPrivate::SetSkipWaitingFlag() { + AssertIsOnMainThread(); + MOZ_ASSERT(mInfo); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + if (!swm) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + RefPtr<ServiceWorkerRegistrationInfo> regInfo = + swm->GetRegistration(mInfo->Principal(), mInfo->Scope()); + + if (!regInfo) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + mInfo->SetSkipWaitingFlag(); + + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + regInfo->TryToActivateAsync([promise] { promise->Resolve(true, __func__); }); + + return promise; +} + +/* static */ +void ServiceWorkerPrivate::UpdateRunning(int32_t aDelta, int32_t aFetchDelta) { + // Record values for time we were running at the current values + RefPtr<ServiceWorkerManager> manager(ServiceWorkerManager::GetInstance()); + manager->RecordTelemetry(sRunningServiceWorkers, sRunningServiceWorkersFetch); + + MOZ_ASSERT(((int64_t)sRunningServiceWorkers) + aDelta >= 0); + sRunningServiceWorkers += aDelta; + if (sRunningServiceWorkers > sRunningServiceWorkersMax) { + sRunningServiceWorkersMax = sRunningServiceWorkers; + LOG(("ServiceWorker max now %d", sRunningServiceWorkersMax)); + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_RUNNING_MAX, + u"All"_ns, sRunningServiceWorkersMax); + } + MOZ_ASSERT(((int64_t)sRunningServiceWorkersFetch) + aFetchDelta >= 0); + sRunningServiceWorkersFetch += aFetchDelta; + if (sRunningServiceWorkersFetch > sRunningServiceWorkersFetchMax) { + sRunningServiceWorkersFetchMax = sRunningServiceWorkersFetch; + LOG(("ServiceWorker Fetch max now %d", sRunningServiceWorkersFetchMax)); + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_RUNNING_MAX, + u"Fetch"_ns, sRunningServiceWorkersFetchMax); + } + LOG(("ServiceWorkers running now %d/%d", sRunningServiceWorkers, + sRunningServiceWorkersFetch)); +} + +void ServiceWorkerPrivate::CreationFailed() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mControllerChild); + + if (mRemoteWorkerData.remoteType().Find(SERVICEWORKER_REMOTE_TYPE) != + kNotFound) { + Telemetry::AccumulateTimeDelta( + Telemetry::SERVICE_WORKER_ISOLATED_LAUNCH_TIME, + mServiceWorkerLaunchTimeStart); + } else { + Telemetry::AccumulateTimeDelta(Telemetry::SERVICE_WORKER_LAUNCH_TIME_2, + mServiceWorkerLaunchTimeStart); + } + + Shutdown(); +} + +void ServiceWorkerPrivate::CreationSucceeded() { + AssertIsOnMainThread(); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInfo); + MOZ_ASSERT(mControllerChild); + + if (mRemoteWorkerData.remoteType().Find(SERVICEWORKER_REMOTE_TYPE) != + kNotFound) { + Telemetry::AccumulateTimeDelta( + Telemetry::SERVICE_WORKER_ISOLATED_LAUNCH_TIME, + mServiceWorkerLaunchTimeStart); + } else { + Telemetry::AccumulateTimeDelta(Telemetry::SERVICE_WORKER_LAUNCH_TIME_2, + mServiceWorkerLaunchTimeStart); + } + + RenewKeepAliveToken(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + nsCOMPtr<nsIPrincipal> principal = mInfo->Principal(); + RefPtr<ServiceWorkerRegistrationInfo> regInfo = + swm->GetRegistration(principal, mInfo->Scope()); + if (regInfo) { + // If it's already set, we're done and the running count is already set + if (mHandlesFetch == Unknown) { + if (regInfo->GetActive()) { + mHandlesFetch = + regInfo->GetActive()->HandlesFetch() ? Enabled : Disabled; + if (mHandlesFetch == Enabled) { + UpdateRunning(0, 1); + } + } + // else we're likely still in Evaluating state, and don't know if it + // handles fetch. If so, defer updating the counter for Fetch until we + // finish evaluation. We already updated the Running count for All in + // SpawnWorkerIfNeeded(). + } + } +} + +void ServiceWorkerPrivate::ErrorReceived(const ErrorValue& aError) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInfo); + MOZ_ASSERT(mControllerChild); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + MOZ_ASSERT(swm); + + ServiceWorkerInfo* info = mInfo; + + swm->HandleError(nullptr, info->Principal(), info->Scope(), + NS_ConvertUTF8toUTF16(info->ScriptSpec()), u""_ns, u""_ns, + u""_ns, 0, 0, nsIScriptError::errorFlag, JSEXN_ERR); +} + +void ServiceWorkerPrivate::Terminated() { + AssertIsOnMainThread(); + MOZ_ASSERT(mInfo); + MOZ_ASSERT(mControllerChild); + + Shutdown(); +} + +void ServiceWorkerPrivate::RefreshRemoteWorkerData( + const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration) { + AssertIsOnMainThread(); + MOZ_ASSERT(mInfo); + + ServiceWorkerData& serviceWorkerData = + mRemoteWorkerData.serviceWorkerData().get_ServiceWorkerData(); + serviceWorkerData.descriptor() = mInfo->Descriptor().ToIPC(); + serviceWorkerData.registrationDescriptor() = + aRegistration->Descriptor().ToIPC(); +} + +RefPtr<FetchServicePromises> ServiceWorkerPrivate::SetupNavigationPreload( + nsCOMPtr<nsIInterceptedChannel>& aChannel, + const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration) { + MOZ_ASSERT(XRE_IsParentProcess()); + AssertIsOnMainThread(); + + // create IPC request from the intercepted channel. + auto result = GetIPCInternalRequest(aChannel); + if (result.isErr()) { + return nullptr; + } + IPCInternalRequest ipcRequest = result.unwrap(); + + // Step 1. Clone the request for preload + // Create the InternalResponse from the created IPCRequest. + SafeRefPtr<InternalRequest> preloadRequest = + MakeSafeRefPtr<InternalRequest>(ipcRequest); + // Copy the request body from uploadChannel + nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(aChannel); + if (uploadChannel) { + nsCOMPtr<nsIInputStream> uploadStream; + nsresult rv = uploadChannel->CloneUploadStream( + &ipcRequest.bodySize(), getter_AddRefs(uploadStream)); + // Fail to get the request's body, stop navigation preload by returning + // nullptr. + if (NS_WARN_IF(NS_FAILED(rv))) { + return FetchService::NetworkErrorResponse(rv); + } + preloadRequest->SetBody(uploadStream, ipcRequest.bodySize()); + } + + // Set SkipServiceWorker for the navigation preload request + preloadRequest->SetSkipServiceWorker(); + + // Step 2. Append Service-Worker-Navigation-Preload header with + // registration->GetNavigationPreloadState().headerValue() on + // request's header list. + IgnoredErrorResult err; + auto headersGuard = preloadRequest->Headers()->Guard(); + preloadRequest->Headers()->SetGuard(HeadersGuardEnum::None, err); + preloadRequest->Headers()->Append( + "Service-Worker-Navigation-Preload"_ns, + aRegistration->GetNavigationPreloadState().headerValue(), err); + preloadRequest->Headers()->SetGuard(headersGuard, err); + + // Step 3. Perform fetch through FetchService with the cloned request + if (!err.Failed()) { + nsCOMPtr<nsIChannel> underlyingChannel; + MOZ_ALWAYS_SUCCEEDS( + aChannel->GetChannel(getter_AddRefs(underlyingChannel))); + RefPtr<FetchService> fetchService = FetchService::GetInstance(); + return fetchService->Fetch(std::move(preloadRequest), underlyingChannel); + } + return FetchService::NetworkErrorResponse(NS_ERROR_UNEXPECTED); +} + +void ServiceWorkerPrivate::Shutdown() { + AssertIsOnMainThread(); + + if (mControllerChild) { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + MOZ_ASSERT(swm, + "All Service Workers should start shutting down before the " + "ServiceWorkerManager does!"); + + auto shutdownStateId = swm->MaybeInitServiceWorkerShutdownProgress(); + + RefPtr<GenericNonExclusivePromise> promise = + ShutdownInternal(shutdownStateId); + swm->BlockShutdownOn(promise, shutdownStateId); + } + + MOZ_ASSERT(!mControllerChild); +} + +RefPtr<GenericNonExclusivePromise> ServiceWorkerPrivate::ShutdownInternal( + uint32_t aShutdownStateId) { + AssertIsOnMainThread(); + MOZ_ASSERT(mControllerChild); + + mPendingFunctionalEvents.Clear(); + + mControllerChild->get()->RevokeObserver(this); + + if (StaticPrefs::dom_serviceWorkers_testing_enabled()) { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "service-worker-shutdown", nullptr); + } + } + + RefPtr<GenericNonExclusivePromise::Private> promise = + new GenericNonExclusivePromise::Private(__func__); + + Unused << ExecServiceWorkerOp( + ServiceWorkerTerminateWorkerOpArgs(aShutdownStateId), + [promise](ServiceWorkerOpResult&& aResult) { + MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); + promise->Resolve(true, __func__); + }, + [promise]() { promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); }); + + /** + * After dispatching a termination operation, no new operations should + * be routed through this actor anymore. + */ + mControllerChild = nullptr; + + // Update here, since Evaluation failures directly call ShutdownInternal + UpdateRunning(-1, mHandlesFetch == Enabled ? -1 : 0); + + return promise; +} + +nsresult ServiceWorkerPrivate::ExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, + std::function<void(ServiceWorkerOpResult&&)>&& aSuccessCallback, + std::function<void()>&& aFailureCallback) { + AssertIsOnMainThread(); + MOZ_ASSERT( + aArgs.type() != + ServiceWorkerOpArgs::TParentToChildServiceWorkerFetchEventOpArgs, + "FetchEvent operations should be sent through FetchEventOp(Proxy) " + "actors!"); + MOZ_ASSERT(aSuccessCallback); + + nsresult rv = SpawnWorkerIfNeeded(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aFailureCallback(); + return rv; + } + + MOZ_ASSERT(mControllerChild); + + RefPtr<ServiceWorkerPrivate> self = this; + RefPtr<RAIIActorPtrHolder> holder = mControllerChild; + RefPtr<KeepAliveToken> token = + aArgs.type() == ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs + ? nullptr + : CreateEventKeepAliveToken(); + + /** + * NOTE: moving `aArgs` won't do anything until IPDL `SendMethod()` methods + * can accept rvalue references rather than just const references. + */ + mControllerChild->get()->SendExecServiceWorkerOp(aArgs)->Then( + GetCurrentSerialEventTarget(), __func__, + [self = std::move(self), holder = std::move(holder), + token = std::move(token), onSuccess = std::move(aSuccessCallback), + onFailure = std::move(aFailureCallback)]( + PRemoteWorkerControllerChild::ExecServiceWorkerOpPromise:: + ResolveOrRejectValue&& aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + onFailure(); + return; + } + + onSuccess(std::move(aResult.ResolveValue())); + }); + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerPrivate.h b/dom/serviceworkers/ServiceWorkerPrivate.h new file mode 100644 index 0000000000..24bdc1666c --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerPrivate.h @@ -0,0 +1,380 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerprivate_h +#define mozilla_dom_serviceworkerprivate_h + +#include <functional> +#include <type_traits> + +#include "mozilla/Attributes.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/FetchService.h" +#include "mozilla/dom/RemoteWorkerController.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" +#include "nsCOMPtr.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +#define NOTIFICATION_CLICK_EVENT_NAME u"notificationclick" +#define NOTIFICATION_CLOSE_EVENT_NAME u"notificationclose" + +class nsIInterceptedChannel; +class nsIWorkerDebugger; + +namespace mozilla { + +template <typename T> +class Maybe; + +class JSObjectHolder; + +namespace dom { + +class ClientInfoAndState; +class RemoteWorkerControllerChild; +class ServiceWorkerCloneData; +class ServiceWorkerInfo; +class ServiceWorkerPrivate; +class ServiceWorkerRegistrationInfo; + +namespace ipc { +class StructuredCloneData; +} // namespace ipc + +class LifeCycleEventCallback : public Runnable { + public: + LifeCycleEventCallback() : Runnable("dom::LifeCycleEventCallback") {} + + // Called on the worker thread. + virtual void SetResult(bool aResult) = 0; +}; + +// Used to keep track of pending waitUntil as well as in-flight extendable +// events. When the last token is released, we attempt to terminate the worker. +class KeepAliveToken final : public nsISupports { + public: + NS_DECL_ISUPPORTS + + explicit KeepAliveToken(ServiceWorkerPrivate* aPrivate); + + private: + ~KeepAliveToken(); + + RefPtr<ServiceWorkerPrivate> mPrivate; +}; + +class ServiceWorkerPrivate final : public RemoteWorkerObserver { + friend class KeepAliveToken; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerPrivate, override); + + using PromiseExtensionWorkerHasListener = MozPromise<bool, nsresult, false>; + + public: + explicit ServiceWorkerPrivate(ServiceWorkerInfo* aInfo); + + nsresult SendMessageEvent(RefPtr<ServiceWorkerCloneData>&& aData, + const ClientInfoAndState& aClientInfoAndState); + + // This is used to validate the worker script and continue the installation + // process. + nsresult CheckScriptEvaluation(RefPtr<LifeCycleEventCallback> aCallback); + + nsresult SendLifeCycleEvent(const nsAString& aEventType, + RefPtr<LifeCycleEventCallback> aCallback); + + nsresult SendPushEvent(const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData, + RefPtr<ServiceWorkerRegistrationInfo> aRegistration); + + nsresult SendPushSubscriptionChangeEvent(); + + nsresult SendNotificationEvent(const nsAString& aEventName, + const nsAString& aID, const nsAString& aTitle, + const nsAString& aDir, const nsAString& aLang, + const nsAString& aBody, const nsAString& aTag, + const nsAString& aIcon, const nsAString& aData, + const nsAString& aBehavior, + const nsAString& aScope); + + nsresult SendFetchEvent(nsCOMPtr<nsIInterceptedChannel> aChannel, + nsILoadGroup* aLoadGroup, const nsAString& aClientId, + const nsAString& aResultingClientId); + + Result<RefPtr<PromiseExtensionWorkerHasListener>, nsresult> + WakeForExtensionAPIEvent(const nsAString& aExtensionAPINamespace, + const nsAString& aEXtensionAPIEventName); + + // This will terminate the current running worker thread and drop the + // workerPrivate reference. + // Called by ServiceWorkerInfo when [[Clear Registration]] is invoked + // or whenever the spec mandates that we terminate the worker. + // This is a no-op if the worker has already been stopped. + void TerminateWorker(); + + void NoteDeadServiceWorkerInfo(); + + void NoteStoppedControllingDocuments(); + + void UpdateState(ServiceWorkerState aState); + + nsresult GetDebugger(nsIWorkerDebugger** aResult); + + nsresult AttachDebugger(); + + nsresult DetachDebugger(); + + bool IsIdle() const; + + // This promise is used schedule clearing of the owning registrations and its + // associated Service Workers if that registration becomes "unreachable" by + // the ServiceWorkerManager. This occurs under two conditions, which are the + // preconditions to calling this method: + // - The owning registration must be unregistered. + // - The associated Service Worker must *not* be controlling clients. + // + // Additionally, perhaps stating the obvious, the associated Service Worker + // must *not* be idle (whatever must be done "when idle" can just be done + // immediately). + RefPtr<GenericPromise> GetIdlePromise(); + + void SetHandlesFetch(bool aValue); + + RefPtr<GenericPromise> SetSkipWaitingFlag(); + + static void RunningShutdown() { + // Force a final update of the number of running ServiceWorkers + UpdateRunning(0, 0); + MOZ_ASSERT(sRunningServiceWorkers == 0); + MOZ_ASSERT(sRunningServiceWorkersFetch == 0); + } + + /** + * Update Telemetry for # of running ServiceWorkers + */ + static void UpdateRunning(int32_t aDelta, int32_t aFetchDelta); + + private: + // Timer callbacks + void NoteIdleWorkerCallback(nsITimer* aTimer); + + void TerminateWorkerCallback(nsITimer* aTimer); + + void RenewKeepAliveToken(); + + void ResetIdleTimeout(); + + void AddToken(); + + void ReleaseToken(); + + already_AddRefed<KeepAliveToken> CreateEventKeepAliveToken(); + + nsresult SpawnWorkerIfNeeded(); + + ~ServiceWorkerPrivate(); + + nsresult Initialize(); + + /** + * RemoteWorkerObserver + */ + void CreationFailed() override; + + void CreationSucceeded() override; + + void ErrorReceived(const ErrorValue& aError) override; + + void LockNotified(bool aCreated) final { + // no-op for service workers + } + + void Terminated() override; + + // Refreshes only the parts of mRemoteWorkerData that may change over time. + void RefreshRemoteWorkerData( + const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration); + + nsresult SendPushEventInternal( + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ServiceWorkerPushEventOpArgs&& aArgs); + + // Setup the navigation preload by the intercepted channel and the + // RegistrationInfo. + RefPtr<FetchServicePromises> SetupNavigationPreload( + nsCOMPtr<nsIInterceptedChannel>& aChannel, + const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration); + + nsresult SendFetchEventInternal( + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel>&& aChannel, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises); + + void Shutdown(); + + RefPtr<GenericNonExclusivePromise> ShutdownInternal( + uint32_t aShutdownStateId); + + nsresult ExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, + std::function<void(ServiceWorkerOpResult&&)>&& aSuccessCallback, + std::function<void()>&& aFailureCallback = [] {}); + + class PendingFunctionalEvent { + public: + PendingFunctionalEvent( + ServiceWorkerPrivate* aOwner, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration); + + virtual ~PendingFunctionalEvent(); + + virtual nsresult Send() = 0; + + protected: + ServiceWorkerPrivate* const MOZ_NON_OWNING_REF mOwner; + RefPtr<ServiceWorkerRegistrationInfo> mRegistration; + }; + + class PendingPushEvent final : public PendingFunctionalEvent { + public: + PendingPushEvent(ServiceWorkerPrivate* aOwner, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ServiceWorkerPushEventOpArgs&& aArgs); + + nsresult Send() override; + + private: + ServiceWorkerPushEventOpArgs mArgs; + }; + + class PendingFetchEvent final : public PendingFunctionalEvent { + public: + PendingFetchEvent( + ServiceWorkerPrivate* aOwner, + RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration, + ParentToParentServiceWorkerFetchEventOpArgs&& aArgs, + nsCOMPtr<nsIInterceptedChannel>&& aChannel, + RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises); + + nsresult Send() override; + + ~PendingFetchEvent(); + + private: + ParentToParentServiceWorkerFetchEventOpArgs mArgs; + nsCOMPtr<nsIInterceptedChannel> mChannel; + // The promises from FetchService. It indicates if the preload response is + // ready or not. The promise's resolve/reject value should be handled in + // FetchEventOpChild, such that the preload result can be propagated to the + // ServiceWorker through IPC. However, FetchEventOpChild creation could be + // pending here, so this member is needed. And it will be forwarded to + // FetchEventOpChild when crearting the FetchEventOpChild. + RefPtr<FetchServicePromises> mPreloadResponseReadyPromises; + }; + + nsTArray<UniquePtr<PendingFunctionalEvent>> mPendingFunctionalEvents; + + /** + * It's possible that there are still in-progress operations when a + * a termination operation is issued. In this case, it's important to keep + * the RemoteWorkerControllerChild actor alive until all pending operations + * have completed before destroying it with Send__delete__(). + * + * RAIIActorPtrHolder holds a singular, owning reference to a + * RemoteWorkerControllerChild actor and is responsible for destroying the + * actor in its (i.e. the holder's) destructor. This implies that all + * in-progress operations must maintain a strong reference to their + * corresponding holders and release the reference once completed/canceled. + * + * Additionally a RAIIActorPtrHolder must be initialized with a non-null actor + * and cannot be moved or copied. Therefore, the identities of two held + * actors can be compared by simply comparing their holders' addresses. + */ + class RAIIActorPtrHolder final { + public: + NS_INLINE_DECL_REFCOUNTING(RAIIActorPtrHolder) + + explicit RAIIActorPtrHolder( + already_AddRefed<RemoteWorkerControllerChild> aActor); + + RAIIActorPtrHolder(const RAIIActorPtrHolder& aOther) = delete; + RAIIActorPtrHolder& operator=(const RAIIActorPtrHolder& aOther) = delete; + + RAIIActorPtrHolder(RAIIActorPtrHolder&& aOther) = delete; + RAIIActorPtrHolder& operator=(RAIIActorPtrHolder&& aOther) = delete; + + RemoteWorkerControllerChild* operator->() const + MOZ_NO_ADDREF_RELEASE_ON_RETURN; + + RemoteWorkerControllerChild* get() const; + + RefPtr<GenericPromise> OnDestructor(); + + private: + ~RAIIActorPtrHolder(); + + MozPromiseHolder<GenericPromise> mDestructorPromiseHolder; + + const RefPtr<RemoteWorkerControllerChild> mActor; + }; + + RefPtr<RAIIActorPtrHolder> mControllerChild; + + RemoteWorkerData mRemoteWorkerData; + + TimeStamp mServiceWorkerLaunchTimeStart; + + // Counters for Telemetry - totals running simultaneously, and those that + // handle Fetch, plus Max values for each + static uint32_t sRunningServiceWorkers; + static uint32_t sRunningServiceWorkersFetch; + static uint32_t sRunningServiceWorkersMax; + static uint32_t sRunningServiceWorkersFetchMax; + + // We know the state after we've evaluated the worker, and we then store + // it in the registration. The only valid state transition should be + // from Unknown to Enabled or Disabled. + enum { Unknown, Enabled, Disabled } mHandlesFetch{Unknown}; + + // The info object owns us. It is possible to outlive it for a brief period + // of time if there are pending waitUntil promises, in which case it + // will be null and |SpawnWorkerIfNeeded| will always fail. + ServiceWorkerInfo* MOZ_NON_OWNING_REF mInfo; + + nsCOMPtr<nsITimer> mIdleWorkerTimer; + + // We keep a token for |dom.serviceWorkers.idle_timeout| seconds to give the + // worker a grace period after each event. + RefPtr<KeepAliveToken> mIdleKeepAliveToken; + + uint64_t mDebuggerCount; + + uint64_t mTokenCount; + + // Used by the owning `ServiceWorkerRegistrationInfo` when it wants to call + // `Clear` after being unregistered and isn't controlling any clients but this + // worker (i.e. the registration's active worker) isn't idle yet. Note that + // such an event should happen at most once in a + // `ServiceWorkerRegistrationInfo`s lifetime, so this promise should also only + // be obtained at most once. + MozPromiseHolder<GenericPromise> mIdlePromiseHolder; + +#ifdef DEBUG + bool mIdlePromiseObtained = false; +#endif +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_serviceworkerprivate_h diff --git a/dom/serviceworkers/ServiceWorkerProxy.cpp b/dom/serviceworkers/ServiceWorkerProxy.cpp new file mode 100644 index 0000000000..aa6b77c1fa --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerProxy.cpp @@ -0,0 +1,122 @@ +/* -*- 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 "ServiceWorkerProxy.h" +#include "ServiceWorkerCloneData.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerParent.h" + +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/ClientState.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "ServiceWorkerInfo.h" + +namespace mozilla::dom { + +using mozilla::ipc::AssertIsOnBackgroundThread; + +ServiceWorkerProxy::~ServiceWorkerProxy() { + // Any thread + MOZ_DIAGNOSTIC_ASSERT(!mActor); + MOZ_DIAGNOSTIC_ASSERT(!mInfo); +} + +void ServiceWorkerProxy::MaybeShutdownOnBGThread() { + AssertIsOnBackgroundThread(); + if (!mActor) { + return; + } + mActor->MaybeSendDelete(); +} + +void ServiceWorkerProxy::InitOnMainThread() { + AssertIsOnMainThread(); + + auto scopeExit = MakeScopeExit([&] { MaybeShutdownOnMainThread(); }); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + RefPtr<ServiceWorkerRegistrationInfo> reg = + swm->GetRegistration(mDescriptor.PrincipalInfo(), mDescriptor.Scope()); + NS_ENSURE_TRUE_VOID(reg); + + RefPtr<ServiceWorkerInfo> info = reg->GetByDescriptor(mDescriptor); + NS_ENSURE_TRUE_VOID(info); + + scopeExit.release(); + + mInfo = new nsMainThreadPtrHolder<ServiceWorkerInfo>( + "ServiceWorkerProxy::mInfo", info); +} + +void ServiceWorkerProxy::MaybeShutdownOnMainThread() { + AssertIsOnMainThread(); + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + __func__, this, &ServiceWorkerProxy::MaybeShutdownOnBGThread); + + MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); +} + +void ServiceWorkerProxy::StopListeningOnMainThread() { + AssertIsOnMainThread(); + mInfo = nullptr; +} + +ServiceWorkerProxy::ServiceWorkerProxy( + const ServiceWorkerDescriptor& aDescriptor) + : mEventTarget(GetCurrentSerialEventTarget()), mDescriptor(aDescriptor) {} + +void ServiceWorkerProxy::Init(ServiceWorkerParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(aActor); + MOZ_DIAGNOSTIC_ASSERT(!mActor); + MOZ_DIAGNOSTIC_ASSERT(mEventTarget); + + mActor = aActor; + + // Note, this must be done from a separate Init() method and not in + // the constructor. If done from the constructor the runnable can + // execute, complete, and release its reference before the constructor + // returns. + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + "ServiceWorkerProxy::Init", this, &ServiceWorkerProxy::InitOnMainThread); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); +} + +void ServiceWorkerProxy::RevokeActor(ServiceWorkerParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor = nullptr; + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + __func__, this, &ServiceWorkerProxy::StopListeningOnMainThread); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); +} + +void ServiceWorkerProxy::PostMessage(RefPtr<ServiceWorkerCloneData>&& aData, + const ClientInfo& aClientInfo, + const ClientState& aClientState) { + AssertIsOnBackgroundThread(); + RefPtr<ServiceWorkerProxy> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, + [self, data = std::move(aData), aClientInfo, aClientState]() mutable { + if (!self->mInfo) { + return; + } + self->mInfo->PostMessage(std::move(data), aClientInfo, aClientState); + }); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerProxy.h b/dom/serviceworkers/ServiceWorkerProxy.h new file mode 100644 index 0000000000..b8b5d7145e --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerProxy.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#ifndef moz_dom_ServiceWorkerProxy_h +#define moz_dom_ServiceWorkerProxy_h + +#include "nsProxyRelease.h" +#include "ServiceWorkerDescriptor.h" + +namespace mozilla::dom { + +class ClientInfo; +class ClientState; +class ServiceWorkerCloneData; +class ServiceWorkerInfo; +class ServiceWorkerParent; + +class ServiceWorkerProxy final { + // Background thread only + RefPtr<ServiceWorkerParent> mActor; + + // Written on background thread and read on main thread + nsCOMPtr<nsISerialEventTarget> mEventTarget; + + // Main thread only + ServiceWorkerDescriptor mDescriptor; + nsMainThreadPtrHandle<ServiceWorkerInfo> mInfo; + + ~ServiceWorkerProxy(); + + // Background thread methods + void MaybeShutdownOnBGThread(); + + void SetStateOnBGThread(ServiceWorkerState aState); + + // Main thread methods + void InitOnMainThread(); + + void MaybeShutdownOnMainThread(); + + void StopListeningOnMainThread(); + + public: + explicit ServiceWorkerProxy(const ServiceWorkerDescriptor& aDescriptor); + + void Init(ServiceWorkerParent* aActor); + + void RevokeActor(ServiceWorkerParent* aActor); + + void PostMessage(RefPtr<ServiceWorkerCloneData>&& aData, + const ClientInfo& aClientInfo, const ClientState& aState); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerProxy); +}; + +} // namespace mozilla::dom + +#endif // moz_dom_ServiceWorkerProxy_h diff --git a/dom/serviceworkers/ServiceWorkerQuotaUtils.cpp b/dom/serviceworkers/ServiceWorkerQuotaUtils.cpp new file mode 100644 index 0000000000..65e051eb40 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerQuotaUtils.cpp @@ -0,0 +1,327 @@ +/* -*- 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 "MainThreadUtils.h" +#include "ServiceWorkerQuotaUtils.h" + +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/quota/QuotaManagerService.h" +#include "nsIClearDataService.h" +#include "nsID.h" +#include "nsIPrincipal.h" +#include "nsIQuotaCallbacks.h" +#include "nsIQuotaRequests.h" +#include "nsIQuotaResults.h" +#include "nsISupports.h" +#include "nsIVariant.h" +#include "nsServiceManagerUtils.h" + +using mozilla::dom::quota::QuotaManagerService; + +namespace mozilla::dom { + +/* + * QuotaUsageChecker implements the quota usage checking algorithm. + * + * 1. Getting the given origin/group usage through QuotaManagerService. + * QuotaUsageCheck::Start() implements this step. + * 2. Checking if the group usage headroom is satisfied. + * It could be following three situations. + * a. Group headroom is satisfied without any usage mitigation. + * b. Group headroom is satisfied after origin usage mitigation. + * This invokes nsIClearDataService::DeleteDataFromPrincipal(). + * c. Group headroom is satisfied after group usage mitigation. + * This invokes nsIClearDataService::DeleteDataFromBaseDomain(). + * QuotaUsageChecker::CheckQuotaHeadroom() implements this step. + * + * If the algorithm is done or error out, the QuotaUsageCheck::mCallback will + * be called with a bool result for external handling. + */ +class QuotaUsageChecker final : public nsIQuotaCallback, + public nsIQuotaUsageCallback, + public nsIClearDataCallback { + public: + NS_DECL_ISUPPORTS + // For QuotaManagerService::Estimate() + NS_DECL_NSIQUOTACALLBACK + + // For QuotaManagerService::GetUsageForPrincipal() + NS_DECL_NSIQUOTAUSAGECALLBACK + + // For nsIClearDataService::DeleteDataFromPrincipal() and + // nsIClearDataService::DeleteDataFromBaseDomain() + NS_DECL_NSICLEARDATACALLBACK + + QuotaUsageChecker(nsIPrincipal* aPrincipal, + ServiceWorkerQuotaMitigationCallback&& aCallback); + + void Start(); + + void RunCallback(bool aResult); + + private: + ~QuotaUsageChecker() = default; + + // This is a general help method to get nsIQuotaResult/nsIQuotaUsageResult + // from nsIQuotaRequest/nsIQuotaUsageRequest + template <typename T, typename U> + nsresult GetResult(T* aRequest, U&); + + void CheckQuotaHeadroom(); + + nsCOMPtr<nsIPrincipal> mPrincipal; + + // The external callback. Calling RunCallback(bool) instead of calling it + // directly, RunCallback(bool) handles the internal status. + ServiceWorkerQuotaMitigationCallback mCallback; + bool mGettingOriginUsageDone; + bool mGettingGroupUsageDone; + bool mIsChecking; + uint64_t mOriginUsage; + uint64_t mGroupUsage; + uint64_t mGroupLimit; +}; + +NS_IMPL_ISUPPORTS(QuotaUsageChecker, nsIQuotaCallback, nsIQuotaUsageCallback, + nsIClearDataCallback) + +QuotaUsageChecker::QuotaUsageChecker( + nsIPrincipal* aPrincipal, ServiceWorkerQuotaMitigationCallback&& aCallback) + : mPrincipal(aPrincipal), + mCallback(std::move(aCallback)), + mGettingOriginUsageDone(false), + mGettingGroupUsageDone(false), + mIsChecking(false), + mOriginUsage(0), + mGroupUsage(0), + mGroupLimit(0) {} + +void QuotaUsageChecker::Start() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mIsChecking) { + return; + } + mIsChecking = true; + + RefPtr<QuotaUsageChecker> self = this; + auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); }); + + RefPtr<QuotaManagerService> qms = QuotaManagerService::GetOrCreate(); + MOZ_ASSERT(qms); + + // Asynchronious getting quota usage for principal + nsCOMPtr<nsIQuotaUsageRequest> usageRequest; + if (NS_WARN_IF(NS_FAILED(qms->GetUsageForPrincipal( + mPrincipal, this, false, getter_AddRefs(usageRequest))))) { + return; + } + + // Asynchronious getting group usage and limit + nsCOMPtr<nsIQuotaRequest> request; + if (NS_WARN_IF( + NS_FAILED(qms->Estimate(mPrincipal, getter_AddRefs(request))))) { + return; + } + request->SetCallback(this); + + scopeExit.release(); +} + +void QuotaUsageChecker::RunCallback(bool aResult) { + MOZ_ASSERT(mIsChecking && mCallback); + if (!mIsChecking) { + return; + } + mIsChecking = false; + mGettingOriginUsageDone = false; + mGettingGroupUsageDone = false; + + mCallback(aResult); + mCallback = nullptr; +} + +template <typename T, typename U> +nsresult QuotaUsageChecker::GetResult(T* aRequest, U& aResult) { + nsCOMPtr<nsIVariant> result; + nsresult rv = aRequest->GetResult(getter_AddRefs(result)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsID* iid; + nsCOMPtr<nsISupports> supports; + rv = result->GetAsInterface(&iid, getter_AddRefs(supports)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + free(iid); + + aResult = do_QueryInterface(supports); + return NS_OK; +} + +void QuotaUsageChecker::CheckQuotaHeadroom() { + MOZ_ASSERT(NS_IsMainThread()); + + const uint64_t groupHeadroom = + static_cast<uint64_t>( + StaticPrefs:: + dom_serviceWorkers_mitigations_group_usage_headroom_kb()) * + 1024; + const uint64_t groupAvailable = mGroupLimit - mGroupUsage; + + // Group usage head room is satisfied, does not need the usage mitigation. + if (groupAvailable > groupHeadroom) { + RunCallback(true); + return; + } + + RefPtr<QuotaUsageChecker> self = this; + auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); }); + + nsCOMPtr<nsIClearDataService> csd = + do_GetService("@mozilla.org/clear-data-service;1"); + MOZ_ASSERT(csd); + + // Group usage headroom is not satisfied even removing the origin usage, + // clear all group usage. + if ((groupAvailable + mOriginUsage) < groupHeadroom) { + nsAutoCString host; + nsresult rv = mPrincipal->GetHost(host); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = csd->DeleteDataFromBaseDomain( + host, false, nsIClearDataService::CLEAR_DOM_QUOTA, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // clear the origin usage since it makes group usage headroom be satisifed. + } else { + nsresult rv = csd->DeleteDataFromPrincipal( + mPrincipal, false, nsIClearDataService::CLEAR_DOM_QUOTA, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + scopeExit.release(); +} + +// nsIQuotaUsageCallback implementation + +NS_IMETHODIMP QuotaUsageChecker::OnUsageResult( + nsIQuotaUsageRequest* aUsageRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aUsageRequest); + + RefPtr<QuotaUsageChecker> self = this; + auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); }); + + nsresult resultCode; + nsresult rv = aUsageRequest->GetResultCode(&resultCode); + if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(resultCode))) { + return rv; + } + + nsCOMPtr<nsIQuotaOriginUsageResult> usageResult; + rv = GetResult(aUsageRequest, usageResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(usageResult); + + rv = usageResult->GetUsage(&mOriginUsage); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!mGettingOriginUsageDone); + mGettingOriginUsageDone = true; + + scopeExit.release(); + + // Call CheckQuotaHeadroom() when both + // QuotaManagerService::GetUsageForPrincipal() and + // QuotaManagerService::Estimate() are done. + if (mGettingOriginUsageDone && mGettingGroupUsageDone) { + CheckQuotaHeadroom(); + } + return NS_OK; +} + +// nsIQuotaCallback implementation + +NS_IMETHODIMP QuotaUsageChecker::OnComplete(nsIQuotaRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + + RefPtr<QuotaUsageChecker> self = this; + auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); }); + + nsresult resultCode; + nsresult rv = aRequest->GetResultCode(&resultCode); + if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(resultCode))) { + return rv; + } + + nsCOMPtr<nsIQuotaEstimateResult> estimateResult; + rv = GetResult(aRequest, estimateResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(estimateResult); + + rv = estimateResult->GetUsage(&mGroupUsage); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = estimateResult->GetLimit(&mGroupLimit); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!mGettingGroupUsageDone); + mGettingGroupUsageDone = true; + + scopeExit.release(); + + // Call CheckQuotaHeadroom() when both + // QuotaManagerService::GetUsageForPrincipal() and + // QuotaManagerService::Estimate() are done. + if (mGettingOriginUsageDone && mGettingGroupUsageDone) { + CheckQuotaHeadroom(); + } + return NS_OK; +} + +// nsIClearDataCallback implementation + +NS_IMETHODIMP QuotaUsageChecker::OnDataDeleted(uint32_t aFailedFlags) { + RunCallback(true); + return NS_OK; +} + +// Helper methods in ServiceWorkerQuotaUtils.h + +void ClearQuotaUsageIfNeeded(nsIPrincipal* aPrincipal, + ServiceWorkerQuotaMitigationCallback&& aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + RefPtr<QuotaUsageChecker> checker = + MakeRefPtr<QuotaUsageChecker>(aPrincipal, std::move(aCallback)); + checker->Start(); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerQuotaUtils.h b/dom/serviceworkers/ServiceWorkerQuotaUtils.h new file mode 100644 index 0000000000..9f16f367ac --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerQuotaUtils.h @@ -0,0 +1,23 @@ +/* -*- 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/. */ +#ifndef _mozilla_dom_ServiceWorkerQuotaUtils_h +#define _mozilla_dom_ServiceWorkerQuotaUtils_h + +#include <functional> + +class nsIPrincipal; +class nsIQuotaUsageRequest; + +namespace mozilla::dom { + +using ServiceWorkerQuotaMitigationCallback = std::function<void(bool)>; + +void ClearQuotaUsageIfNeeded(nsIPrincipal* aPrincipal, + ServiceWorkerQuotaMitigationCallback&& aCallback); + +} // namespace mozilla::dom + +#endif diff --git a/dom/serviceworkers/ServiceWorkerRegisterJob.cpp b/dom/serviceworkers/ServiceWorkerRegisterJob.cpp new file mode 100644 index 0000000000..9d5f6db098 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegisterJob.cpp @@ -0,0 +1,57 @@ +/* -*- 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 "ServiceWorkerRegisterJob.h" + +#include "mozilla/dom/WorkerCommon.h" +#include "ServiceWorkerManager.h" + +namespace mozilla::dom { + +ServiceWorkerRegisterJob::ServiceWorkerRegisterJob( + nsIPrincipal* aPrincipal, const nsACString& aScope, + const nsACString& aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache) + : ServiceWorkerUpdateJob(Type::Register, aPrincipal, aScope, + nsCString(aScriptSpec), aUpdateViaCache) {} + +void ServiceWorkerRegisterJob::AsyncExecute() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mPrincipal, mScope); + + if (registration) { + bool sameUVC = GetUpdateViaCache() == registration->GetUpdateViaCache(); + registration->SetUpdateViaCache(GetUpdateViaCache()); + + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (newest && mScriptSpec.Equals(newest->ScriptSpec()) && sameUVC) { + SetRegistration(registration); + Finish(NS_OK); + return; + } + } else { + registration = + swm->CreateNewRegistration(mScope, mPrincipal, GetUpdateViaCache()); + if (!registration) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + } + + SetRegistration(registration); + Update(); +} + +ServiceWorkerRegisterJob::~ServiceWorkerRegisterJob() = default; + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegisterJob.h b/dom/serviceworkers/ServiceWorkerRegisterJob.h new file mode 100644 index 0000000000..ab4259e606 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegisterJob.h @@ -0,0 +1,33 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerregisterjob_h +#define mozilla_dom_serviceworkerregisterjob_h + +#include "ServiceWorkerUpdateJob.h" + +namespace mozilla::dom { + +// The register job. This implements the steps in the spec Register algorithm, +// but then uses ServiceWorkerUpdateJob to implement the Update and Install +// spec algorithms. +class ServiceWorkerRegisterJob final : public ServiceWorkerUpdateJob { + public: + ServiceWorkerRegisterJob(nsIPrincipal* aPrincipal, const nsACString& aScope, + const nsACString& aScriptSpec, + ServiceWorkerUpdateViaCache aUpdateViaCache); + + private: + // Implement the Register algorithm steps and then call the parent class + // Update() to complete the job execution. + virtual void AsyncExecute() override; + + virtual ~ServiceWorkerRegisterJob(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerregisterjob_h diff --git a/dom/serviceworkers/ServiceWorkerRegistrar.cpp b/dom/serviceworkers/ServiceWorkerRegistrar.cpp new file mode 100644 index 0000000000..cc2b490c5c --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrar.cpp @@ -0,0 +1,1458 @@ +/* -*- 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 "ServiceWorkerRegistrar.h" +#include "ServiceWorkerManager.h" +#include "mozilla/dom/ServiceWorkerRegistrarTypes.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/net/MozURL.h" +#include "mozilla/StaticPrefs_dom.h" + +#include "nsIEventTarget.h" +#include "nsIInputStream.h" +#include "nsILineInputStream.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsIServiceWorkerManager.h" +#include "nsIWritablePropertyBag2.h" + +#include "MainThreadUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/dom/StorageActivityService.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsDirectoryServiceUtils.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "ServiceWorkerUtils.h" + +using namespace mozilla::ipc; + +extern mozilla::LazyLogModule sWorkerTelemetryLog; + +#ifdef LOG +# undef LOG +#endif +#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args); + +namespace mozilla::dom { + +namespace { + +static const char* gSupportedRegistrarVersions[] = { + SERVICEWORKERREGISTRAR_VERSION, "8", "7", "6", "5", "4", "3", "2"}; + +static const uint32_t kInvalidGeneration = static_cast<uint32_t>(-1); + +StaticRefPtr<ServiceWorkerRegistrar> gServiceWorkerRegistrar; + +nsresult GetOriginAndBaseDomain(const nsACString& aURL, nsACString& aOrigin, + nsACString& aBaseDomain) { + RefPtr<net::MozURL> url; + nsresult rv = net::MozURL::Init(getter_AddRefs(url), aURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + url->Origin(aOrigin); + + rv = url->BaseDomain(aBaseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult ReadLine(nsILineInputStream* aStream, nsACString& aValue) { + bool hasMoreLines; + nsresult rv = aStream->ReadLine(aValue, &hasMoreLines); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!hasMoreLines)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult CreatePrincipalInfo(nsILineInputStream* aStream, + ServiceWorkerRegistrationData* aEntry, + bool aSkipSpec = false) { + nsAutoCString suffix; + nsresult rv = ReadLine(aStream, suffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + OriginAttributes attrs; + if (!attrs.PopulateFromSuffix(suffix)) { + return NS_ERROR_INVALID_ARG; + } + + if (aSkipSpec) { + nsAutoCString unused; + nsresult rv = ReadLine(aStream, unused); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = ReadLine(aStream, aEntry->scope()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString origin; + nsCString baseDomain; + rv = GetOriginAndBaseDomain(aEntry->scope(), origin, baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aEntry->principal() = mozilla::ipc::ContentPrincipalInfo( + attrs, origin, aEntry->scope(), Nothing(), baseDomain); + + return NS_OK; +} + +const IPCNavigationPreloadState gDefaultNavigationPreloadState(false, + "true"_ns); + +} // namespace + +NS_IMPL_ISUPPORTS(ServiceWorkerRegistrar, nsIObserver, nsIAsyncShutdownBlocker) + +void ServiceWorkerRegistrar::Initialize() { + MOZ_ASSERT(!gServiceWorkerRegistrar); + + if (!XRE_IsParentProcess()) { + return; + } + + gServiceWorkerRegistrar = new ServiceWorkerRegistrar(); + ClearOnShutdown(&gServiceWorkerRegistrar); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + DebugOnly<nsresult> rv = obs->AddObserver(gServiceWorkerRegistrar, + "profile-after-change", false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +/* static */ +already_AddRefed<ServiceWorkerRegistrar> ServiceWorkerRegistrar::Get() { + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(gServiceWorkerRegistrar); + RefPtr<ServiceWorkerRegistrar> service = gServiceWorkerRegistrar.get(); + return service.forget(); +} + +ServiceWorkerRegistrar::ServiceWorkerRegistrar() + : mMonitor("ServiceWorkerRegistrar.mMonitor"), + mDataLoaded(false), + mDataGeneration(kInvalidGeneration), + mFileGeneration(kInvalidGeneration), + mRetryCount(0), + mShuttingDown(false), + mSaveDataRunnableDispatched(false) { + MOZ_ASSERT(NS_IsMainThread()); +} + +ServiceWorkerRegistrar::~ServiceWorkerRegistrar() { + MOZ_ASSERT(!mSaveDataRunnableDispatched); +} + +void ServiceWorkerRegistrar::GetRegistrations( + nsTArray<ServiceWorkerRegistrationData>& aValues) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aValues.IsEmpty()); + + MonitorAutoLock lock(mMonitor); + + // If we don't have the profile directory, profile is not started yet (and + // probably we are in a utest). + if (!mProfileDir) { + return; + } + + // We care just about the first execution because this can be blocked by + // loading data from disk. + static bool firstTime = true; + TimeStamp startTime; + + if (firstTime) { + startTime = TimeStamp::NowLoRes(); + } + + // Waiting for data loaded. + mMonitor.AssertCurrentThreadOwns(); + while (!mDataLoaded) { + mMonitor.Wait(); + } + + aValues.AppendElements(mData); + + MaybeResetGeneration(); + MOZ_DIAGNOSTIC_ASSERT(mDataGeneration != kInvalidGeneration); + MOZ_DIAGNOSTIC_ASSERT(mFileGeneration != kInvalidGeneration); + + if (firstTime) { + firstTime = false; + Telemetry::AccumulateTimeDelta( + Telemetry::SERVICE_WORKER_REGISTRATION_LOADING, startTime); + } +} + +namespace { + +bool Equivalent(const ServiceWorkerRegistrationData& aLeft, + const ServiceWorkerRegistrationData& aRight) { + MOZ_ASSERT(aLeft.principal().type() == + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + MOZ_ASSERT(aRight.principal().type() == + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + + const auto& leftPrincipal = aLeft.principal().get_ContentPrincipalInfo(); + const auto& rightPrincipal = aRight.principal().get_ContentPrincipalInfo(); + + // Only compare the attributes, not the spec part of the principal. + // The scope comparison above already covers the origin and codebase + // principals include the full path in their spec which is not what + // we want here. + return aLeft.scope() == aRight.scope() && + leftPrincipal.attrs() == rightPrincipal.attrs(); +} + +} // anonymous namespace + +void ServiceWorkerRegistrar::RegisterServiceWorker( + const ServiceWorkerRegistrationData& aData) { + AssertIsOnBackgroundThread(); + + if (mShuttingDown) { + NS_WARNING("Failed to register a serviceWorker during shutting down."); + return; + } + + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mDataLoaded); + RegisterServiceWorkerInternal(aData); + } + + MaybeScheduleSaveData(); + StorageActivityService::SendActivity(aData.principal()); +} + +void ServiceWorkerRegistrar::UnregisterServiceWorker( + const PrincipalInfo& aPrincipalInfo, const nsACString& aScope) { + AssertIsOnBackgroundThread(); + + if (mShuttingDown) { + NS_WARNING("Failed to unregister a serviceWorker during shutting down."); + return; + } + + bool deleted = false; + + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mDataLoaded); + + ServiceWorkerRegistrationData tmp; + tmp.principal() = aPrincipalInfo; + tmp.scope() = aScope; + + for (uint32_t i = 0; i < mData.Length(); ++i) { + if (Equivalent(tmp, mData[i])) { + gServiceWorkersRegistered--; + if (mData[i].currentWorkerHandlesFetch()) { + gServiceWorkersRegisteredFetch--; + } + // Update Telemetry + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS, + u"All"_ns, gServiceWorkersRegistered); + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS, + u"Fetch"_ns, gServiceWorkersRegisteredFetch); + LOG(("Unregister ServiceWorker: %u, fetch %u\n", + gServiceWorkersRegistered, gServiceWorkersRegisteredFetch)); + + mData.RemoveElementAt(i); + mDataGeneration = GetNextGeneration(); + deleted = true; + break; + } + } + } + + if (deleted) { + MaybeScheduleSaveData(); + StorageActivityService::SendActivity(aPrincipalInfo); + } +} + +void ServiceWorkerRegistrar::RemoveAll() { + AssertIsOnBackgroundThread(); + + if (mShuttingDown) { + NS_WARNING("Failed to remove all the serviceWorkers during shutting down."); + return; + } + + bool deleted = false; + + nsTArray<ServiceWorkerRegistrationData> data; + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mDataLoaded); + + // Let's take a copy in order to inform StorageActivityService. + data = mData.Clone(); + + deleted = !mData.IsEmpty(); + mData.Clear(); + + mDataGeneration = GetNextGeneration(); + } + + if (!deleted) { + return; + } + + MaybeScheduleSaveData(); + + for (uint32_t i = 0, len = data.Length(); i < len; ++i) { + StorageActivityService::SendActivity(data[i].principal()); + } +} + +void ServiceWorkerRegistrar::LoadData() { + MOZ_ASSERT(!NS_IsMainThread()); +#ifdef DEBUG + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(!mDataLoaded); + } +#endif + + nsresult rv = ReadData(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + DeleteData(); + // Also if the reading failed we have to notify what is waiting for data. + } + + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(!mDataLoaded); + mDataLoaded = true; + mMonitor.Notify(); +} + +bool ServiceWorkerRegistrar::ReloadDataForTest() { + if (NS_WARN_IF(!StaticPrefs::dom_serviceWorkers_testing_enabled())) { + return false; + } + + MOZ_ASSERT(NS_IsMainThread()); + MonitorAutoLock lock(mMonitor); + mData.Clear(); + mDataLoaded = false; + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target, "Must have stream transport service"); + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("dom::ServiceWorkerRegistrar::LoadData", this, + &ServiceWorkerRegistrar::LoadData); + nsresult rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the LoadDataRunnable."); + return false; + } + + mMonitor.AssertCurrentThreadOwns(); + while (!mDataLoaded) { + mMonitor.Wait(); + } + + return mDataLoaded; +} + +nsresult ServiceWorkerRegistrar::ReadData() { + // We cannot assert about the correct thread because normally this method + // runs on a IO thread, but in gTests we call it from the main-thread. + + nsCOMPtr<nsIFile> file; + + { + MonitorAutoLock lock(mMonitor); + + if (!mProfileDir) { + return NS_ERROR_FAILURE; + } + + nsresult rv = mProfileDir->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsresult rv = file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + return NS_OK; + } + + nsCOMPtr<nsIInputStream> stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(stream); + MOZ_ASSERT(lineInputStream); + + nsAutoCString version; + bool hasMoreLines; + rv = lineInputStream->ReadLine(version, &hasMoreLines); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!IsSupportedVersion(version)) { + nsContentUtils::LogMessageToConsole( + nsPrintfCString("Unsupported service worker registrar version: %s", + version.get()) + .get()); + return NS_ERROR_FAILURE; + } + + nsTArray<ServiceWorkerRegistrationData> tmpData; + + bool overwrite = false; + bool dedupe = false; + while (hasMoreLines) { + ServiceWorkerRegistrationData* entry = tmpData.AppendElement(); + +#define GET_LINE(x) \ + rv = lineInputStream->ReadLine(x, &hasMoreLines); \ + if (NS_WARN_IF(NS_FAILED(rv))) { \ + return rv; \ + } \ + if (NS_WARN_IF(!hasMoreLines)) { \ + return NS_ERROR_FAILURE; \ + } + + nsAutoCString line; + if (version.EqualsLiteral(SERVICEWORKERREGISTRAR_VERSION)) { + rv = CreatePrincipalInfo(lineInputStream, entry); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString fetchFlag; + GET_LINE(fetchFlag); + if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) && + !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) { + return NS_ERROR_INVALID_ARG; + } + entry->currentWorkerHandlesFetch() = + fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + nsAutoCString updateViaCache; + GET_LINE(updateViaCache); + entry->updateViaCache() = updateViaCache.ToInteger(&rv, 16); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (entry->updateViaCache() > + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE) { + return NS_ERROR_INVALID_ARG; + } + + nsAutoCString installedTimeStr; + GET_LINE(installedTimeStr); + int64_t installedTime = installedTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->currentWorkerInstalledTime() = installedTime; + + nsAutoCString activatedTimeStr; + GET_LINE(activatedTimeStr); + int64_t activatedTime = activatedTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->currentWorkerActivatedTime() = activatedTime; + + nsAutoCString lastUpdateTimeStr; + GET_LINE(lastUpdateTimeStr); + int64_t lastUpdateTime = lastUpdateTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->lastUpdateTime() = lastUpdateTime; + + nsAutoCString navigationPreloadEnabledStr; + GET_LINE(navigationPreloadEnabledStr); + bool navigationPreloadEnabled = + navigationPreloadEnabledStr.ToInteger(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->navigationPreloadState().enabled() = navigationPreloadEnabled; + + GET_LINE(entry->navigationPreloadState().headerValue()); + } else if (version.EqualsLiteral("8")) { + rv = CreatePrincipalInfo(lineInputStream, entry); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString fetchFlag; + GET_LINE(fetchFlag); + if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) && + !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) { + return NS_ERROR_INVALID_ARG; + } + entry->currentWorkerHandlesFetch() = + fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + nsAutoCString updateViaCache; + GET_LINE(updateViaCache); + entry->updateViaCache() = updateViaCache.ToInteger(&rv, 16); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (entry->updateViaCache() > + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE) { + return NS_ERROR_INVALID_ARG; + } + + nsAutoCString installedTimeStr; + GET_LINE(installedTimeStr); + int64_t installedTime = installedTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->currentWorkerInstalledTime() = installedTime; + + nsAutoCString activatedTimeStr; + GET_LINE(activatedTimeStr); + int64_t activatedTime = activatedTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->currentWorkerActivatedTime() = activatedTime; + + nsAutoCString lastUpdateTimeStr; + GET_LINE(lastUpdateTimeStr); + int64_t lastUpdateTime = lastUpdateTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->lastUpdateTime() = lastUpdateTime; + + entry->navigationPreloadState() = gDefaultNavigationPreloadState; + } else if (version.EqualsLiteral("7")) { + rv = CreatePrincipalInfo(lineInputStream, entry); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString fetchFlag; + GET_LINE(fetchFlag); + if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) && + !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) { + return NS_ERROR_INVALID_ARG; + } + entry->currentWorkerHandlesFetch() = + fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + nsAutoCString loadFlags; + GET_LINE(loadFlags); + entry->updateViaCache() = + loadFlags.ToInteger(&rv, 16) == nsIRequest::LOAD_NORMAL + ? nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL + : nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString installedTimeStr; + GET_LINE(installedTimeStr); + int64_t installedTime = installedTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->currentWorkerInstalledTime() = installedTime; + + nsAutoCString activatedTimeStr; + GET_LINE(activatedTimeStr); + int64_t activatedTime = activatedTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->currentWorkerActivatedTime() = activatedTime; + + nsAutoCString lastUpdateTimeStr; + GET_LINE(lastUpdateTimeStr); + int64_t lastUpdateTime = lastUpdateTimeStr.ToInteger64(&rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + entry->lastUpdateTime() = lastUpdateTime; + + entry->navigationPreloadState() = gDefaultNavigationPreloadState; + } else if (version.EqualsLiteral("6")) { + rv = CreatePrincipalInfo(lineInputStream, entry); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString fetchFlag; + GET_LINE(fetchFlag); + if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) && + !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) { + return NS_ERROR_INVALID_ARG; + } + entry->currentWorkerHandlesFetch() = + fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + nsAutoCString loadFlags; + GET_LINE(loadFlags); + entry->updateViaCache() = + loadFlags.ToInteger(&rv, 16) == nsIRequest::LOAD_NORMAL + ? nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL + : nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + entry->currentWorkerInstalledTime() = 0; + entry->currentWorkerActivatedTime() = 0; + entry->lastUpdateTime() = 0; + + entry->navigationPreloadState() = gDefaultNavigationPreloadState; + } else if (version.EqualsLiteral("5")) { + overwrite = true; + dedupe = true; + + rv = CreatePrincipalInfo(lineInputStream, entry); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString fetchFlag; + GET_LINE(fetchFlag); + if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) && + !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) { + return NS_ERROR_INVALID_ARG; + } + entry->currentWorkerHandlesFetch() = + fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + entry->updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + entry->currentWorkerInstalledTime() = 0; + entry->currentWorkerActivatedTime() = 0; + entry->lastUpdateTime() = 0; + + entry->navigationPreloadState() = gDefaultNavigationPreloadState; + } else if (version.EqualsLiteral("4")) { + overwrite = true; + dedupe = true; + + rv = CreatePrincipalInfo(lineInputStream, entry); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + GET_LINE(entry->currentWorkerURL()); + + // default handlesFetch flag to Enabled + entry->currentWorkerHandlesFetch() = true; + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + entry->updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + entry->currentWorkerInstalledTime() = 0; + entry->currentWorkerActivatedTime() = 0; + entry->lastUpdateTime() = 0; + + entry->navigationPreloadState() = gDefaultNavigationPreloadState; + } else if (version.EqualsLiteral("3")) { + overwrite = true; + dedupe = true; + + rv = CreatePrincipalInfo(lineInputStream, entry, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + GET_LINE(entry->currentWorkerURL()); + + // default handlesFetch flag to Enabled + entry->currentWorkerHandlesFetch() = true; + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + entry->updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + entry->currentWorkerInstalledTime() = 0; + entry->currentWorkerActivatedTime() = 0; + entry->lastUpdateTime() = 0; + + entry->navigationPreloadState() = gDefaultNavigationPreloadState; + } else if (version.EqualsLiteral("2")) { + overwrite = true; + dedupe = true; + + rv = CreatePrincipalInfo(lineInputStream, entry, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // scriptSpec is no more used in latest version. + nsAutoCString unused; + GET_LINE(unused); + + GET_LINE(entry->currentWorkerURL()); + + // default handlesFetch flag to Enabled + entry->currentWorkerHandlesFetch() = true; + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + // waitingCacheName is no more used in latest version. + GET_LINE(unused); + + entry->updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + entry->currentWorkerInstalledTime() = 0; + entry->currentWorkerActivatedTime() = 0; + entry->lastUpdateTime() = 0; + + entry->navigationPreloadState() = gDefaultNavigationPreloadState; + } else { + MOZ_ASSERT_UNREACHABLE("Should never get here!"); + } + +#undef GET_LINE + + rv = lineInputStream->ReadLine(line, &hasMoreLines); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!line.EqualsLiteral(SERVICEWORKERREGISTRAR_TERMINATOR)) { + return NS_ERROR_FAILURE; + } + } + + stream->Close(); + + // We currently only call this at startup where we block the main thread + // preventing further operation until it completes, however take the lock + // in case that changes + + { + MonitorAutoLock lock(mMonitor); + // Copy data over to mData. + for (uint32_t i = 0; i < tmpData.Length(); ++i) { + // Older versions could sometimes write out empty, useless entries. + // Prune those here. + if (!ServiceWorkerRegistrationDataIsValid(tmpData[i])) { + continue; + } + + bool match = false; + if (dedupe) { + MOZ_ASSERT(overwrite); + // If this is an old profile, then we might need to deduplicate. In + // theory this can be removed in the future (Bug 1248449) + for (uint32_t j = 0; j < mData.Length(); ++j) { + // Use same comparison as RegisterServiceWorker. Scope contains + // basic origin information. Combine with any principal attributes. + if (Equivalent(tmpData[i], mData[j])) { + // Last match wins, just like legacy loading used to do in + // the ServiceWorkerManager. + mData[j] = tmpData[i]; + // Dupe found, so overwrite file with reduced list. + match = true; + break; + } + } + } else { +#ifdef DEBUG + // Otherwise assert no duplications in debug builds. + for (uint32_t j = 0; j < mData.Length(); ++j) { + MOZ_ASSERT(!Equivalent(tmpData[i], mData[j])); + } +#endif + } + if (!match) { + mData.AppendElement(tmpData[i]); + } + } + } + // Overwrite previous version. + // Cannot call SaveData directly because gtest uses main-thread. + + // XXX NOTE: if we could be accessed multi-threaded here, we would need to + // find a way to lock around access to mData. Since we can't, suppress the + // thread-safety warnings. + MOZ_PUSH_IGNORE_THREAD_SAFETY + if (overwrite && NS_FAILED(WriteData(mData))) { + NS_WARNING("Failed to write data for the ServiceWorker Registations."); + DeleteData(); + } + MOZ_POP_THREAD_SAFETY + + return NS_OK; +} + +void ServiceWorkerRegistrar::DeleteData() { + // We cannot assert about the correct thread because normally this method + // runs on a IO thread, but in gTests we call it from the main-thread. + + nsCOMPtr<nsIFile> file; + + { + MonitorAutoLock lock(mMonitor); + mData.Clear(); + + if (!mProfileDir) { + return; + } + + nsresult rv = mProfileDir->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + nsresult rv = file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = file->Remove(false); + if (rv == NS_ERROR_FILE_NOT_FOUND) { + return; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +void ServiceWorkerRegistrar::RegisterServiceWorkerInternal( + const ServiceWorkerRegistrationData& aData) { + bool found = false; + for (uint32_t i = 0, len = mData.Length(); i < len; ++i) { + if (Equivalent(aData, mData[i])) { + found = true; + if (mData[i].currentWorkerHandlesFetch()) { + // Decrement here if we found it, in case the new registration no + // longer handles Fetch. If it continues to handle fetch, we'll + // bump it back later. + gServiceWorkersRegisteredFetch--; + } + mData[i] = aData; + break; + } + } + + if (!found) { + MOZ_ASSERT(ServiceWorkerRegistrationDataIsValid(aData)); + mData.AppendElement(aData); + // We didn't find an entry to update, so we have 1 more + gServiceWorkersRegistered++; + } + // Handles bumping both for new registrations and updates + if (aData.currentWorkerHandlesFetch()) { + gServiceWorkersRegisteredFetch++; + } + // Update Telemetry + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS, + u"All"_ns, gServiceWorkersRegistered); + Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS, + u"Fetch"_ns, gServiceWorkersRegisteredFetch); + LOG(("Register: %u, fetch %u\n", gServiceWorkersRegistered, + gServiceWorkersRegisteredFetch)); + + mDataGeneration = GetNextGeneration(); +} + +class ServiceWorkerRegistrarSaveDataRunnable final : public Runnable { + nsCOMPtr<nsIEventTarget> mEventTarget; + const nsTArray<ServiceWorkerRegistrationData> mData; + const uint32_t mGeneration; + + public: + ServiceWorkerRegistrarSaveDataRunnable( + nsTArray<ServiceWorkerRegistrationData>&& aData, uint32_t aGeneration) + : Runnable("dom::ServiceWorkerRegistrarSaveDataRunnable"), + mEventTarget(GetCurrentEventTarget()), + mData(std::move(aData)), + mGeneration(aGeneration) { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mGeneration != kInvalidGeneration); + } + + NS_IMETHOD + Run() override { + RefPtr<ServiceWorkerRegistrar> service = ServiceWorkerRegistrar::Get(); + MOZ_ASSERT(service); + + uint32_t fileGeneration = kInvalidGeneration; + + if (NS_SUCCEEDED(service->SaveData(mData))) { + fileGeneration = mGeneration; + } + + RefPtr<Runnable> runnable = NewRunnableMethod<uint32_t>( + "ServiceWorkerRegistrar::DataSaved", service, + &ServiceWorkerRegistrar::DataSaved, fileGeneration); + MOZ_ALWAYS_SUCCEEDS( + mEventTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); + + return NS_OK; + } +}; + +void ServiceWorkerRegistrar::MaybeScheduleSaveData() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShuttingDown); + + if (mShuttingDown || mSaveDataRunnableDispatched || + mDataGeneration <= mFileGeneration) { + return; + } + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target, "Must have stream transport service"); + + uint32_t generation = kInvalidGeneration; + nsTArray<ServiceWorkerRegistrationData> data; + + { + MonitorAutoLock lock(mMonitor); + generation = mDataGeneration; + data.AppendElements(mData); + } + + RefPtr<Runnable> runnable = + new ServiceWorkerRegistrarSaveDataRunnable(std::move(data), generation); + nsresult rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS_VOID(rv); + + mSaveDataRunnableDispatched = true; +} + +void ServiceWorkerRegistrar::ShutdownCompleted() { + MOZ_ASSERT(NS_IsMainThread()); + + DebugOnly<nsresult> rv = GetShutdownPhase()->RemoveBlocker(this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +nsresult ServiceWorkerRegistrar::SaveData( + const nsTArray<ServiceWorkerRegistrationData>& aData) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = WriteData(aData); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to write data for the ServiceWorker Registations."); + // Don't touch the file or in-memory state. Writing files can + // sometimes fail due to virus scanning, etc. We should just leave + // things as is so the next save operation can pick up any changes + // without losing data. + } + return rv; +} + +void ServiceWorkerRegistrar::DataSaved(uint32_t aFileGeneration) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mSaveDataRunnableDispatched); + + mSaveDataRunnableDispatched = false; + + // Check for shutdown before possibly triggering any more saves + // runnables. + MaybeScheduleShutdownCompleted(); + if (mShuttingDown) { + return; + } + + // If we got a valid generation, then the save was successful. + if (aFileGeneration != kInvalidGeneration) { + // Update the file generation. We also check to see if we + // can reset the generation back to zero if the file and data + // are now in sync. This allows us to avoid dealing with wrap + // around of the generation count. + mFileGeneration = aFileGeneration; + MaybeResetGeneration(); + + // Successful write resets the retry count. + mRetryCount = 0; + + // Possibly schedule another save operation if more data + // has come in while processing this one. + MaybeScheduleSaveData(); + + return; + } + + // Otherwise, the save failed since the generation is invalid. We + // want to retry the save, but only a limited number of times. + static const uint32_t kMaxRetryCount = 2; + if (mRetryCount >= kMaxRetryCount) { + return; + } + + mRetryCount += 1; + MaybeScheduleSaveData(); +} + +void ServiceWorkerRegistrar::MaybeScheduleShutdownCompleted() { + AssertIsOnBackgroundThread(); + + if (mSaveDataRunnableDispatched || !mShuttingDown) { + return; + } + + RefPtr<Runnable> runnable = + NewRunnableMethod("dom::ServiceWorkerRegistrar::ShutdownCompleted", this, + &ServiceWorkerRegistrar::ShutdownCompleted); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); +} + +uint32_t ServiceWorkerRegistrar::GetNextGeneration() { + uint32_t ret = mDataGeneration + 1; + if (ret == kInvalidGeneration) { + ret += 1; + } + return ret; +} + +void ServiceWorkerRegistrar::MaybeResetGeneration() { + if (mDataGeneration != mFileGeneration) { + return; + } + mDataGeneration = mFileGeneration = 0; +} + +bool ServiceWorkerRegistrar::IsSupportedVersion( + const nsACString& aVersion) const { + uint32_t numVersions = ArrayLength(gSupportedRegistrarVersions); + for (uint32_t i = 0; i < numVersions; i++) { + if (aVersion.EqualsASCII(gSupportedRegistrarVersions[i])) { + return true; + } + } + return false; +} + +nsresult ServiceWorkerRegistrar::WriteData( + const nsTArray<ServiceWorkerRegistrationData>& aData) { + // We cannot assert about the correct thread because normally this method + // runs on a IO thread, but in gTests we call it from the main-thread. + + nsCOMPtr<nsIFile> file; + + { + MonitorAutoLock lock(mMonitor); + + if (!mProfileDir) { + return NS_ERROR_FAILURE; + } + + nsresult rv = mProfileDir->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsresult rv = file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIOutputStream> stream; + rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString buffer; + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_VERSION); + buffer.Append('\n'); + + uint32_t count; + rv = stream->Write(buffer.Data(), buffer.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (count != buffer.Length()) { + return NS_ERROR_UNEXPECTED; + } + + for (uint32_t i = 0, len = aData.Length(); i < len; ++i) { + // We have an assertion further up the stack, but as a last + // resort avoid writing out broken entries here. + if (!ServiceWorkerRegistrationDataIsValid(aData[i])) { + continue; + } + + const mozilla::ipc::PrincipalInfo& info = aData[i].principal(); + + MOZ_ASSERT(info.type() == + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + + const mozilla::ipc::ContentPrincipalInfo& cInfo = + info.get_ContentPrincipalInfo(); + + nsAutoCString suffix; + cInfo.attrs().CreateSuffix(suffix); + + buffer.Truncate(); + buffer.Append(suffix.get()); + buffer.Append('\n'); + + buffer.Append(aData[i].scope()); + buffer.Append('\n'); + + buffer.Append(aData[i].currentWorkerURL()); + buffer.Append('\n'); + + buffer.Append(aData[i].currentWorkerHandlesFetch() + ? SERVICEWORKERREGISTRAR_TRUE + : SERVICEWORKERREGISTRAR_FALSE); + buffer.Append('\n'); + + buffer.Append(NS_ConvertUTF16toUTF8(aData[i].cacheName())); + buffer.Append('\n'); + + buffer.AppendInt(aData[i].updateViaCache(), 16); + buffer.Append('\n'); + MOZ_DIAGNOSTIC_ASSERT( + aData[i].updateViaCache() == + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS || + aData[i].updateViaCache() == + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL || + aData[i].updateViaCache() == + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE); + + static_assert(nsIRequest::LOAD_NORMAL == 0, + "LOAD_NORMAL matches serialized value."); + static_assert(nsIRequest::VALIDATE_ALWAYS == (1 << 11), + "VALIDATE_ALWAYS matches serialized value"); + + buffer.AppendInt(aData[i].currentWorkerInstalledTime()); + buffer.Append('\n'); + + buffer.AppendInt(aData[i].currentWorkerActivatedTime()); + buffer.Append('\n'); + + buffer.AppendInt(aData[i].lastUpdateTime()); + buffer.Append('\n'); + + buffer.AppendInt( + static_cast<int32_t>(aData[i].navigationPreloadState().enabled())); + buffer.Append('\n'); + + buffer.Append(aData[i].navigationPreloadState().headerValue()); + buffer.Append('\n'); + + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR); + buffer.Append('\n'); + + rv = stream->Write(buffer.Data(), buffer.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (count != buffer.Length()) { + return NS_ERROR_UNEXPECTED; + } + } + + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream); + MOZ_ASSERT(safeStream); + + rv = safeStream->Finish(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void ServiceWorkerRegistrar::ProfileStarted() { + MOZ_ASSERT(NS_IsMainThread()); + + MonitorAutoLock lock(mMonitor); + MOZ_DIAGNOSTIC_ASSERT(!mProfileDir); + + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsAutoString blockerName; + MOZ_ALWAYS_SUCCEEDS(GetName(blockerName)); + + rv = GetShutdownPhase()->AddBlocker( + this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, blockerName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target, "Must have stream transport service"); + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("dom::ServiceWorkerRegistrar::LoadData", this, + &ServiceWorkerRegistrar::LoadData); + rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the LoadDataRunnable."); + } +} + +void ServiceWorkerRegistrar::ProfileStopped() { + MOZ_ASSERT(NS_IsMainThread()); + + MonitorAutoLock lock(mMonitor); + + if (!mProfileDir) { + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If we do not have a profile directory, we are somehow screwed. + MOZ_DIAGNOSTIC_ASSERT( + false, + "NS_GetSpecialDirectory for NS_APP_USER_PROFILE_50_DIR failed!"); + } + } + + // Mutations to the ServiceWorkerRegistrar happen on the PBackground thread, + // issued by the ServiceWorkerManagerService, so the appropriate place to + // trigger shutdown is on that thread. + // + // However, it's quite possible that the PBackground thread was not brought + // into existence for xpcshell tests. We don't cause it to be created + // ourselves for any reason, for example. + // + // In this scenario, we know that: + // - We will receive exactly one call to ourself from BlockShutdown() and + // BlockShutdown() will be called (at most) once. + // - The only way our Shutdown() method gets called is via + // BackgroundParentImpl::RecvShutdownServiceWorkerRegistrar() being + // invoked, which only happens if we get to that send below here that we + // can't get to. + // - All Shutdown() does is set mShuttingDown=true (essential for + // invariants) and invoke MaybeScheduleShutdownCompleted(). + // - Since there is no PBackground thread, mSaveDataRunnableDispatched must + // be false because only MaybeScheduleSaveData() set it and it only runs + // on the background thread, so it cannot have run. And so we would + // expect MaybeScheduleShutdownCompleted() to schedule an invocation of + // ShutdownCompleted on the main thread. + PBackgroundChild* child = BackgroundChild::GetForCurrentThread(); + if (mProfileDir && child) { + if (child->SendShutdownServiceWorkerRegistrar()) { + // Normal shutdown sequence has been initiated, go home. + return; + } + // If we get here, the PBackground thread has probably gone nuts and we + // want to know it. + MOZ_DIAGNOSTIC_ASSERT( + false, "Unable to send the ShutdownServiceWorkerRegistrar message."); + } + + // On any error it's appropriate to set mShuttingDown=true (as Shutdown + // would do) and directly invoke ShutdownCompleted() (as Shutdown would + // indirectly do via MaybeScheduleShutdownCompleted) in order to unblock + // shutdown. + mShuttingDown = true; + ShutdownCompleted(); +} + +// Async shutdown blocker methods + +NS_IMETHODIMP +ServiceWorkerRegistrar::BlockShutdown(nsIAsyncShutdownClient* aClient) { + ProfileStopped(); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrar::GetName(nsAString& aName) { + aName = u"ServiceWorkerRegistrar: Flushing data"_ns; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrar::GetState(nsIPropertyBag** aBagOut) { + nsCOMPtr<nsIWritablePropertyBag2> propertyBag = + do_CreateInstance("@mozilla.org/hash-property-bag;1"); + + MOZ_TRY(propertyBag->SetPropertyAsBool(u"shuttingDown"_ns, mShuttingDown)); + + MOZ_TRY(propertyBag->SetPropertyAsBool(u"saveDataRunnableDispatched"_ns, + mSaveDataRunnableDispatched)); + + propertyBag.forget(aBagOut); + + return NS_OK; +} + +#define RELEASE_ASSERT_SUCCEEDED(rv, name) \ + do { \ + if (NS_FAILED(rv)) { \ + if (rv == NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS) { \ + if (auto* context = CycleCollectedJSContext::Get()) { \ + if (RefPtr<Exception> exn = context->GetPendingException()) { \ + MOZ_CRASH_UNSAFE_PRINTF("Failed to get " name ": %s", \ + exn->GetMessageMoz().get()); \ + } \ + } \ + } \ + \ + nsAutoCString errorName; \ + GetErrorName(rv, errorName); \ + MOZ_CRASH_UNSAFE_PRINTF("Failed to get " name ": %s", errorName.get()); \ + } \ + } while (0) + +nsCOMPtr<nsIAsyncShutdownClient> ServiceWorkerRegistrar::GetShutdownPhase() + const { + nsresult rv; + nsCOMPtr<nsIAsyncShutdownService> svc = + do_GetService("@mozilla.org/async-shutdown-service;1", &rv); + // If this fails, something is very wrong on the JS side (or we're out of + // memory), and there's no point in continuing startup. Include as much + // information as possible in the crash report. + RELEASE_ASSERT_SUCCEEDED(rv, "async shutdown service"); + + nsCOMPtr<nsIAsyncShutdownClient> client; + rv = svc->GetProfileBeforeChange(getter_AddRefs(client)); + RELEASE_ASSERT_SUCCEEDED(rv, "profileBeforeChange shutdown blocker"); + return client; +} + +#undef RELEASE_ASSERT_SUCCEEDED + +void ServiceWorkerRegistrar::Shutdown() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShuttingDown); + + mShuttingDown = true; + MaybeScheduleShutdownCompleted(); +} + +NS_IMETHODIMP +ServiceWorkerRegistrar::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, "profile-after-change")) { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + observerService->RemoveObserver(this, "profile-after-change"); + + // The profile is fully loaded, now we can proceed with the loading of data + // from disk. + ProfileStarted(); + + return NS_OK; + } + + MOZ_ASSERT(false, "ServiceWorkerRegistrar got unexpected topic!"); + return NS_ERROR_UNEXPECTED; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegistrar.h b/dom/serviceworkers/ServiceWorkerRegistrar.h new file mode 100644 index 0000000000..20c1cc530c --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrar.h @@ -0,0 +1,119 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerRegistrar_h +#define mozilla_dom_ServiceWorkerRegistrar_h + +#include "mozilla/Monitor.h" +#include "mozilla/Telemetry.h" +#include "nsClassHashtable.h" +#include "nsIAsyncShutdown.h" +#include "nsIObserver.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +#define SERVICEWORKERREGISTRAR_FILE u"serviceworker.txt" +#define SERVICEWORKERREGISTRAR_VERSION "9" +#define SERVICEWORKERREGISTRAR_TERMINATOR "#" +#define SERVICEWORKERREGISTRAR_TRUE "true" +#define SERVICEWORKERREGISTRAR_FALSE "false" + +class nsIFile; + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class ServiceWorkerRegistrationData; +} +} // namespace mozilla + +namespace mozilla::dom { + +class ServiceWorkerRegistrar : public nsIObserver, + public nsIAsyncShutdownBlocker { + friend class ServiceWorkerRegistrarSaveDataRunnable; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + + static void Initialize(); + + void Shutdown(); + + void DataSaved(uint32_t aFileGeneration); + + static already_AddRefed<ServiceWorkerRegistrar> Get(); + + void GetRegistrations(nsTArray<ServiceWorkerRegistrationData>& aValues); + + void RegisterServiceWorker(const ServiceWorkerRegistrationData& aData); + void UnregisterServiceWorker( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsACString& aScope); + void RemoveAll(); + + bool ReloadDataForTest(); + + protected: + // These methods are protected because we test this class using gTest + // subclassing it. + void LoadData(); + nsresult SaveData(const nsTArray<ServiceWorkerRegistrationData>& aData); + + nsresult ReadData(); + nsresult WriteData(const nsTArray<ServiceWorkerRegistrationData>& aData); + void DeleteData(); + + void RegisterServiceWorkerInternal(const ServiceWorkerRegistrationData& aData) + MOZ_REQUIRES(mMonitor); + + ServiceWorkerRegistrar(); + virtual ~ServiceWorkerRegistrar(); + + private: + void ProfileStarted(); + void ProfileStopped(); + + void MaybeScheduleSaveData(); + void ShutdownCompleted(); + void MaybeScheduleShutdownCompleted(); + + uint32_t GetNextGeneration(); + void MaybeResetGeneration(); + + nsCOMPtr<nsIAsyncShutdownClient> GetShutdownPhase() const; + + bool IsSupportedVersion(const nsACString& aVersion) const; + + protected: + mozilla::Monitor mMonitor; + + // protected by mMonitor. + nsCOMPtr<nsIFile> mProfileDir MOZ_GUARDED_BY(mMonitor); + // Read on mainthread, modified on background thread EXCEPT for + // ReloadDataForTest() AND for gtest, which modifies this on MainThread. + nsTArray<ServiceWorkerRegistrationData> mData MOZ_GUARDED_BY(mMonitor); + bool mDataLoaded MOZ_GUARDED_BY(mMonitor); + + // PBackground thread only + uint32_t mDataGeneration; + uint32_t mFileGeneration; + uint32_t mRetryCount; + bool mShuttingDown; + bool mSaveDataRunnableDispatched; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ServiceWorkerRegistrar_h diff --git a/dom/serviceworkers/ServiceWorkerRegistrarTypes.ipdlh b/dom/serviceworkers/ServiceWorkerRegistrarTypes.ipdlh new file mode 100644 index 0000000000..d84b324797 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrarTypes.ipdlh @@ -0,0 +1,33 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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 IPCNavigationPreloadState; +include PBackgroundSharedTypes; + +namespace mozilla { +namespace dom { + +struct ServiceWorkerRegistrationData +{ + nsCString scope; + nsCString currentWorkerURL; + bool currentWorkerHandlesFetch; + + nsString cacheName; + + PrincipalInfo principal; + + uint16_t updateViaCache; + + int64_t currentWorkerInstalledTime; + int64_t currentWorkerActivatedTime; + int64_t lastUpdateTime; + + IPCNavigationPreloadState navigationPreloadState; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/serviceworkers/ServiceWorkerRegistration.cpp b/dom/serviceworkers/ServiceWorkerRegistration.cpp new file mode 100644 index 0000000000..9225400139 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistration.cpp @@ -0,0 +1,688 @@ +/* -*- 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 "ServiceWorkerRegistration.h" + +#include "mozilla/dom/DOMMozPromiseRequestHolder.h" +#include "mozilla/dom/NavigationPreloadManager.h" +#include "mozilla/dom/NavigationPreloadManagerBinding.h" +#include "mozilla/dom/Notification.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PushManager.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/dom/ServiceWorker.h" +#include "mozilla/dom/ServiceWorkerRegistrationBinding.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ScopeExit.h" +#include "nsCycleCollectionParticipant.h" +#include "nsPIDOMWindow.h" +#include "ServiceWorkerRegistrationChild.h" + +using mozilla::ipc::ResponseRejectReason; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerRegistration, + DOMEventTargetHelper, mInstallingWorker, + mWaitingWorker, mActiveWorker, + mNavigationPreloadManager, mPushManager); + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerRegistration) + NS_INTERFACE_MAP_ENTRY(ServiceWorkerRegistration) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +namespace { +const uint64_t kInvalidUpdateFoundId = 0; +} // anonymous namespace + +ServiceWorkerRegistration::ServiceWorkerRegistration( + nsIGlobalObject* aGlobal, + const ServiceWorkerRegistrationDescriptor& aDescriptor) + : DOMEventTargetHelper(aGlobal), + mDescriptor(aDescriptor), + mShutdown(false), + mScheduledUpdateFoundId(kInvalidUpdateFoundId), + mDispatchedUpdateFoundId(kInvalidUpdateFoundId) { + ::mozilla::ipc::PBackgroundChild* parentActor = + ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!parentActor)) { + Shutdown(); + return; + } + + auto actor = ServiceWorkerRegistrationChild::Create(); + if (NS_WARN_IF(!actor)) { + Shutdown(); + return; + } + + PServiceWorkerRegistrationChild* sentActor = + parentActor->SendPServiceWorkerRegistrationConstructor( + actor, aDescriptor.ToIPC()); + if (NS_WARN_IF(!sentActor)) { + Shutdown(); + return; + } + MOZ_DIAGNOSTIC_ASSERT(sentActor == actor); + + mActor = std::move(actor); + mActor->SetOwner(this); + + KeepAliveIfHasListenersFor(nsGkAtoms::onupdatefound); +} + +ServiceWorkerRegistration::~ServiceWorkerRegistration() { Shutdown(); } + +JSObject* ServiceWorkerRegistration::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ServiceWorkerRegistration_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed<ServiceWorkerRegistration> +ServiceWorkerRegistration::CreateForMainThread( + nsPIDOMWindowInner* aWindow, + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerRegistration> registration = + new ServiceWorkerRegistration(aWindow->AsGlobal(), aDescriptor); + // This is not called from within the constructor, as it may call content code + // which can cause the deletion of the registration, so we need to keep a + // strong reference while calling it. + registration->UpdateState(aDescriptor); + + return registration.forget(); +} + +/* static */ +already_AddRefed<ServiceWorkerRegistration> +ServiceWorkerRegistration::CreateForWorker( + WorkerPrivate* aWorkerPrivate, nsIGlobalObject* aGlobal, + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); + MOZ_DIAGNOSTIC_ASSERT(aGlobal); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<ServiceWorkerRegistration> registration = + new ServiceWorkerRegistration(aGlobal, aDescriptor); + // This is not called from within the constructor, as it may call content code + // which can cause the deletion of the registration, so we need to keep a + // strong reference while calling it. + registration->UpdateState(aDescriptor); + + return registration.forget(); +} + +void ServiceWorkerRegistration::DisconnectFromOwner() { + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void ServiceWorkerRegistration::RegistrationCleared() { + // Its possible that the registration will fail to install and be + // immediately removed. In that case we may never receive the + // UpdateState() call if the actor was too slow to connect, etc. + // Ensure that we force all our known actors to redundant so that + // the appropriate statechange events are fired. If we got the + // UpdateState() already then this will be a no-op. + UpdateStateInternal(Maybe<ServiceWorkerDescriptor>(), + Maybe<ServiceWorkerDescriptor>(), + Maybe<ServiceWorkerDescriptor>()); + + // Our underlying registration was removed from SWM, so we + // will never get an updatefound event again. We can let + // the object GC if content is not holding it alive. + IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onupdatefound); +} + +already_AddRefed<ServiceWorker> ServiceWorkerRegistration::GetInstalling() + const { + RefPtr<ServiceWorker> ref = mInstallingWorker; + return ref.forget(); +} + +already_AddRefed<ServiceWorker> ServiceWorkerRegistration::GetWaiting() const { + RefPtr<ServiceWorker> ref = mWaitingWorker; + return ref.forget(); +} + +already_AddRefed<ServiceWorker> ServiceWorkerRegistration::GetActive() const { + RefPtr<ServiceWorker> ref = mActiveWorker; + return ref.forget(); +} + +already_AddRefed<NavigationPreloadManager> +ServiceWorkerRegistration::NavigationPreload() { + RefPtr<ServiceWorkerRegistration> reg = this; + if (!mNavigationPreloadManager) { + mNavigationPreloadManager = MakeRefPtr<NavigationPreloadManager>(reg); + } + RefPtr<NavigationPreloadManager> ref = mNavigationPreloadManager; + return ref.forget(); +} + +void ServiceWorkerRegistration::UpdateState( + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + MOZ_DIAGNOSTIC_ASSERT(MatchesDescriptor(aDescriptor)); + + mDescriptor = aDescriptor; + + UpdateStateInternal(aDescriptor.GetInstalling(), aDescriptor.GetWaiting(), + aDescriptor.GetActive()); + + nsTArray<UniquePtr<VersionCallback>> callbackList = + std::move(mVersionCallbackList); + for (auto& cb : callbackList) { + if (cb->mVersion > mDescriptor.Version()) { + mVersionCallbackList.AppendElement(std::move(cb)); + continue; + } + + cb->mFunc(cb->mVersion == mDescriptor.Version()); + } +} + +bool ServiceWorkerRegistration::MatchesDescriptor( + const ServiceWorkerRegistrationDescriptor& aDescriptor) const { + return aDescriptor.Id() == mDescriptor.Id() && + aDescriptor.PrincipalInfo() == mDescriptor.PrincipalInfo() && + aDescriptor.Scope() == mDescriptor.Scope(); +} + +void ServiceWorkerRegistration::GetScope(nsAString& aScope) const { + CopyUTF8toUTF16(mDescriptor.Scope(), aScope); +} + +ServiceWorkerUpdateViaCache ServiceWorkerRegistration::GetUpdateViaCache( + ErrorResult& aRv) const { + return mDescriptor.UpdateViaCache(); +} + +already_AddRefed<Promise> ServiceWorkerRegistration::Update(ErrorResult& aRv) { + nsIGlobalObject* global = GetParentObject(); + if (!global) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + RefPtr<Promise> outer = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // `ServiceWorker` objects are not exposed on worker threads yet, so calling + // `ServiceWorkerRegistration::Get{Installing,Waiting,Active}` won't work. + const Maybe<ServiceWorkerDescriptor> newestWorkerDescriptor = + mDescriptor.Newest(); + + // "If newestWorker is null, return a promise rejected with an + // "InvalidStateError" DOMException and abort these steps." + if (newestWorkerDescriptor.isNothing()) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return outer.forget(); + } + + // "If the context object’s relevant settings object’s global object + // globalObject is a ServiceWorkerGlobalScope object, and globalObject’s + // associated service worker's state is "installing", return a promise + // rejected with an "InvalidStateError" DOMException and abort these steps." + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + if (workerPrivate->IsServiceWorker() && + (workerPrivate->GetServiceWorkerDescriptor().State() == + ServiceWorkerState::Installing)) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return outer.forget(); + } + } + + RefPtr<ServiceWorkerRegistration> self = this; + + if (!mActor) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return outer.forget(); + } + + mActor->SendUpdate( + newestWorkerDescriptor.ref().ScriptURL(), + [outer, + self](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult& + aResult) { + if (aResult.type() == + IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult:: + TCopyableErrorResult) { + // application layer error + const auto& rv = aResult.get_CopyableErrorResult(); + MOZ_DIAGNOSTIC_ASSERT(rv.Failed()); + outer->MaybeReject(CopyableErrorResult(rv)); + return; + } + // success + const auto& ipcDesc = + aResult.get_IPCServiceWorkerRegistrationDescriptor(); + nsIGlobalObject* global = self->GetParentObject(); + // It's possible this binding was detached from the global. In cases + // where we use IPC with Promise callbacks, we use + // DOMMozPromiseRequestHolder in order to auto-disconnect the promise + // that would hold these callbacks. However in bug 1466681 we changed + // this call to use (synchronous) callbacks because the use of + // MozPromise introduced an additional runnable scheduling which made + // it very difficult to maintain ordering required by the standard. + // + // If we were to delete this actor at the time of DETH detaching, we + // would not need to do this check because the IPC callback of the + // RemoteServiceWorkerRegistrationImpl lambdas would never occur. + // However, its actors currently depend on asking the parent to delete + // the actor for us. Given relaxations in the IPC lifecyle, we could + // potentially issue a direct termination, but that requires additional + // evaluation. + if (!global) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + RefPtr<ServiceWorkerRegistration> ref = + global->GetOrCreateServiceWorkerRegistration( + ServiceWorkerRegistrationDescriptor(ipcDesc)); + if (!ref) { + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + outer->MaybeResolve(ref); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + }); + + return outer.forget(); +} + +already_AddRefed<Promise> ServiceWorkerRegistration::Unregister( + ErrorResult& aRv) { + nsIGlobalObject* global = GetParentObject(); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + RefPtr<Promise> outer = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (!mActor) { + outer->MaybeResolve(false); + return outer.forget(); + } + + mActor->SendUnregister( + [outer](Tuple<bool, CopyableErrorResult>&& aResult) { + if (Get<1>(aResult).Failed()) { + // application layer error + // register() should be resilient and resolve false instead of + // rejecting in most cases. + Get<1>(aResult).SuppressException(); + outer->MaybeResolve(false); + return; + } + // success + outer->MaybeResolve(Get<0>(aResult)); + }, + [outer](ResponseRejectReason&& aReason) { + // IPC layer error + outer->MaybeResolve(false); + }); + + return outer.forget(); +} + +already_AddRefed<PushManager> ServiceWorkerRegistration::GetPushManager( + JSContext* aCx, ErrorResult& aRv) { + if (!mPushManager) { + nsCOMPtr<nsIGlobalObject> globalObject = GetParentObject(); + + if (!globalObject) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + GlobalObject global(aCx, globalObject->GetGlobalJSObject()); + mPushManager = PushManager::Constructor( + global, NS_ConvertUTF8toUTF16(mDescriptor.Scope()), aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + RefPtr<PushManager> ret = mPushManager; + return ret.forget(); +} + +already_AddRefed<Promise> ServiceWorkerRegistration::ShowNotification( + JSContext* aCx, const nsAString& aTitle, + const NotificationOptions& aOptions, ErrorResult& aRv) { + nsIGlobalObject* global = GetParentObject(); + if (!global) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // Until we ship ServiceWorker objects on worker threads the active + // worker will always be nullptr. So limit this check to main + // thread for now. + if (mDescriptor.GetActive().isNothing() && NS_IsMainThread()) { + aRv.ThrowTypeError<MSG_NO_ACTIVE_WORKER>(mDescriptor.Scope()); + return nullptr; + } + + NS_ConvertUTF8toUTF16 scope(mDescriptor.Scope()); + + RefPtr<Promise> p = Notification::ShowPersistentNotification( + aCx, global, scope, aTitle, aOptions, mDescriptor, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return p.forget(); +} + +already_AddRefed<Promise> ServiceWorkerRegistration::GetNotifications( + const GetNotificationOptions& aOptions, ErrorResult& aRv) { + nsIGlobalObject* global = GetParentObject(); + if (!global) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + NS_ConvertUTF8toUTF16 scope(mDescriptor.Scope()); + + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + return Notification::Get(window, aOptions, scope, aRv); + } + + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + worker->AssertIsOnWorkerThread(); + return Notification::WorkerGet(worker, aOptions, scope, aRv); +} + +void ServiceWorkerRegistration::SetNavigationPreloadEnabled( + bool aEnabled, ServiceWorkerBoolCallback&& aSuccessCB, + ServiceWorkerFailureCallback&& aFailureCB) { + if (!mActor) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return; + } + + mActor->SendSetNavigationPreloadEnabled( + aEnabled, + [successCB = std::move(aSuccessCB), aFailureCB](bool aResult) { + if (!aResult) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return; + } + successCB(aResult); + }, + [aFailureCB](ResponseRejectReason&& aReason) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + }); +} + +void ServiceWorkerRegistration::SetNavigationPreloadHeader( + const nsCString& aHeader, ServiceWorkerBoolCallback&& aSuccessCB, + ServiceWorkerFailureCallback&& aFailureCB) { + if (!mActor) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return; + } + + mActor->SendSetNavigationPreloadHeader( + aHeader, + [successCB = std::move(aSuccessCB), aFailureCB](bool aResult) { + if (!aResult) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return; + } + successCB(aResult); + }, + [aFailureCB](ResponseRejectReason&& aReason) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + }); +} + +void ServiceWorkerRegistration::GetNavigationPreloadState( + NavigationPreloadGetStateCallback&& aSuccessCB, + ServiceWorkerFailureCallback&& aFailureCB) { + if (!mActor) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return; + } + + mActor->SendGetNavigationPreloadState( + [successCB = std::move(aSuccessCB), + aFailureCB](Maybe<IPCNavigationPreloadState>&& aState) { + if (NS_WARN_IF(!aState)) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return; + } + + NavigationPreloadState state; + state.mEnabled = aState.ref().enabled(); + state.mHeaderValue.Construct(std::move(aState.ref().headerValue())); + successCB(std::move(state)); + }, + [aFailureCB](ResponseRejectReason&& aReason) { + aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + }); +} + +const ServiceWorkerRegistrationDescriptor& +ServiceWorkerRegistration::Descriptor() const { + return mDescriptor; +} + +void ServiceWorkerRegistration::WhenVersionReached( + uint64_t aVersion, ServiceWorkerBoolCallback&& aCallback) { + if (aVersion <= mDescriptor.Version()) { + aCallback(aVersion == mDescriptor.Version()); + return; + } + + mVersionCallbackList.AppendElement( + MakeUnique<VersionCallback>(aVersion, std::move(aCallback))); +} + +void ServiceWorkerRegistration::MaybeScheduleUpdateFound( + const Maybe<ServiceWorkerDescriptor>& aInstallingDescriptor) { + // This function sets mScheduledUpdateFoundId to note when we were told about + // a new installing worker. We rely on a call to + // MaybeDispatchUpdateFoundRunnable (called indirectly from UpdateJobCallback) + // to actually fire the event. + uint64_t newId = aInstallingDescriptor.isSome() + ? aInstallingDescriptor.ref().Id() + : kInvalidUpdateFoundId; + + if (mScheduledUpdateFoundId != kInvalidUpdateFoundId) { + if (mScheduledUpdateFoundId == newId) { + return; + } + MaybeDispatchUpdateFound(); + MOZ_DIAGNOSTIC_ASSERT(mScheduledUpdateFoundId == kInvalidUpdateFoundId); + } + + bool updateFound = + newId != kInvalidUpdateFoundId && mDispatchedUpdateFoundId != newId; + + if (!updateFound) { + return; + } + + mScheduledUpdateFoundId = newId; +} + +void ServiceWorkerRegistration::MaybeDispatchUpdateFoundRunnable() { + if (mScheduledUpdateFoundId == kInvalidUpdateFoundId) { + return; + } + + nsIGlobalObject* global = GetParentObject(); + NS_ENSURE_TRUE_VOID(global); + + nsCOMPtr<nsIRunnable> r = NewCancelableRunnableMethod( + "ServiceWorkerRegistration::MaybeDispatchUpdateFound", this, + &ServiceWorkerRegistration::MaybeDispatchUpdateFound); + + Unused << global->EventTargetFor(TaskCategory::Other) + ->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void ServiceWorkerRegistration::MaybeDispatchUpdateFound() { + uint64_t scheduledId = mScheduledUpdateFoundId; + mScheduledUpdateFoundId = kInvalidUpdateFoundId; + + if (scheduledId == kInvalidUpdateFoundId || + scheduledId == mDispatchedUpdateFoundId) { + return; + } + + mDispatchedUpdateFoundId = scheduledId; + DispatchTrustedEvent(u"updatefound"_ns); +} + +void ServiceWorkerRegistration::UpdateStateInternal( + const Maybe<ServiceWorkerDescriptor>& aInstalling, + const Maybe<ServiceWorkerDescriptor>& aWaiting, + const Maybe<ServiceWorkerDescriptor>& aActive) { + // Do this immediately as it may flush an already pending updatefound + // event. In that case we want to fire the pending event before + // modifying any of the registration properties. + MaybeScheduleUpdateFound(aInstalling); + + // Move the currently exposed workers into a separate list + // of "old" workers. We will then potentially add them + // back to the registration properties below based on the + // given descriptor. Any that are not restored will need + // to be moved to the redundant state. + AutoTArray<RefPtr<ServiceWorker>, 3> oldWorkerList({ + std::move(mInstallingWorker), + std::move(mWaitingWorker), + std::move(mActiveWorker), + }); + + // Its important that all state changes are actually applied before + // dispatching any statechange events. Each ServiceWorker object + // should be in the correct state and the ServiceWorkerRegistration + // properties need to be set correctly as well. To accomplish this + // we use a ScopeExit to dispatch any statechange events. + auto scopeExit = MakeScopeExit([&] { + // Check to see if any of the "old" workers was completely discarded. + // Set these workers to the redundant state. + for (auto& oldWorker : oldWorkerList) { + if (!oldWorker || oldWorker == mInstallingWorker || + oldWorker == mWaitingWorker || oldWorker == mActiveWorker) { + continue; + } + + oldWorker->SetState(ServiceWorkerState::Redundant); + } + + // Check each worker to see if it needs a statechange event dispatched. + if (mInstallingWorker) { + mInstallingWorker->MaybeDispatchStateChangeEvent(); + } + if (mWaitingWorker) { + mWaitingWorker->MaybeDispatchStateChangeEvent(); + } + if (mActiveWorker) { + mActiveWorker->MaybeDispatchStateChangeEvent(); + } + + // We also check the "old" workers to see if they need a statechange + // event as well. Note, these may overlap with the known worker properties + // above, but MaybeDispatchStateChangeEvent() will ignore duplicated calls. + for (auto& oldWorker : oldWorkerList) { + if (!oldWorker) { + continue; + } + + oldWorker->MaybeDispatchStateChangeEvent(); + } + }); + + // Clear all workers if the registration has been detached from the global. + // Also, we cannot expose ServiceWorker objects on worker threads yet, so + // do the same on when off-main-thread. This main thread check should be + // removed as part of bug 1113522. + nsCOMPtr<nsIGlobalObject> global = GetParentObject(); + if (!global || !NS_IsMainThread()) { + return; + } + + if (aActive.isSome()) { + if ((mActiveWorker = global->GetOrCreateServiceWorker(aActive.ref()))) { + mActiveWorker->SetState(aActive.ref().State()); + } + } else { + mActiveWorker = nullptr; + } + + if (aWaiting.isSome()) { + if ((mWaitingWorker = global->GetOrCreateServiceWorker(aWaiting.ref()))) { + mWaitingWorker->SetState(aWaiting.ref().State()); + } + } else { + mWaitingWorker = nullptr; + } + + if (aInstalling.isSome()) { + if ((mInstallingWorker = + global->GetOrCreateServiceWorker(aInstalling.ref()))) { + mInstallingWorker->SetState(aInstalling.ref().State()); + } + } else { + mInstallingWorker = nullptr; + } +} + +void ServiceWorkerRegistration::RevokeActor( + ServiceWorkerRegistrationChild* aActor) { + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor->RevokeOwner(this); + mActor = nullptr; + + mShutdown = true; + + RegistrationCleared(); +} + +void ServiceWorkerRegistration::Shutdown() { + if (mShutdown) { + return; + } + mShutdown = true; + + if (mActor) { + mActor->RevokeOwner(this); + mActor->MaybeStartTeardown(); + mActor = nullptr; + } +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegistration.h b/dom/serviceworkers/ServiceWorkerRegistration.h new file mode 100644 index 0000000000..fa638d37b6 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistration.h @@ -0,0 +1,162 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerRegistration_h +#define mozilla_dom_ServiceWorkerRegistration_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/dom/ServiceWorkerRegistrationBinding.h" +#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h" +#include "mozilla/dom/ServiceWorkerUtils.h" + +// Support for Notification API extension. +#include "mozilla/dom/NotificationBinding.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +class NavigationPreloadManager; +class Promise; +class PushManager; +class WorkerPrivate; +class ServiceWorker; +class ServiceWorkerRegistrationChild; + +#define NS_DOM_SERVICEWORKERREGISTRATION_IID \ + { \ + 0x4578a90e, 0xa427, 0x4237, { \ + 0x98, 0x4a, 0xbd, 0x98, 0xe4, 0xcd, 0x5f, 0x3a \ + } \ + } + +class ServiceWorkerRegistration final : public DOMEventTargetHelper { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_SERVICEWORKERREGISTRATION_IID) + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerRegistration, + DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(updatefound) + + static already_AddRefed<ServiceWorkerRegistration> CreateForMainThread( + nsPIDOMWindowInner* aWindow, + const ServiceWorkerRegistrationDescriptor& aDescriptor); + + static already_AddRefed<ServiceWorkerRegistration> CreateForWorker( + WorkerPrivate* aWorkerPrivate, nsIGlobalObject* aGlobal, + const ServiceWorkerRegistrationDescriptor& aDescriptor); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void DisconnectFromOwner() override; + + void RegistrationCleared(); + + already_AddRefed<ServiceWorker> GetInstalling() const; + + already_AddRefed<ServiceWorker> GetWaiting() const; + + already_AddRefed<ServiceWorker> GetActive() const; + + already_AddRefed<NavigationPreloadManager> NavigationPreload(); + + void UpdateState(const ServiceWorkerRegistrationDescriptor& aDescriptor); + + bool MatchesDescriptor( + const ServiceWorkerRegistrationDescriptor& aDescriptor) const; + + void GetScope(nsAString& aScope) const; + + ServiceWorkerUpdateViaCache GetUpdateViaCache(ErrorResult& aRv) const; + + already_AddRefed<Promise> Update(ErrorResult& aRv); + + already_AddRefed<Promise> Unregister(ErrorResult& aRv); + + already_AddRefed<PushManager> GetPushManager(JSContext* aCx, + ErrorResult& aRv); + + already_AddRefed<Promise> ShowNotification( + JSContext* aCx, const nsAString& aTitle, + const NotificationOptions& aOptions, ErrorResult& aRv); + + already_AddRefed<Promise> GetNotifications( + const GetNotificationOptions& aOptions, ErrorResult& aRv); + + void SetNavigationPreloadEnabled(bool aEnabled, + ServiceWorkerBoolCallback&& aSuccessCB, + ServiceWorkerFailureCallback&& aFailureCB); + + void SetNavigationPreloadHeader(const nsCString& aHeader, + ServiceWorkerBoolCallback&& aSuccessCB, + ServiceWorkerFailureCallback&& aFailureCB); + + void GetNavigationPreloadState(NavigationPreloadGetStateCallback&& aSuccessCB, + ServiceWorkerFailureCallback&& aFailureCB); + + const ServiceWorkerRegistrationDescriptor& Descriptor() const; + + void WhenVersionReached(uint64_t aVersion, + ServiceWorkerBoolCallback&& aCallback); + + void MaybeDispatchUpdateFoundRunnable(); + + void RevokeActor(ServiceWorkerRegistrationChild* aActor); + + void FireUpdateFound(); + + private: + ServiceWorkerRegistration( + nsIGlobalObject* aGlobal, + const ServiceWorkerRegistrationDescriptor& aDescriptor); + + ~ServiceWorkerRegistration(); + + void UpdateStateInternal(const Maybe<ServiceWorkerDescriptor>& aInstalling, + const Maybe<ServiceWorkerDescriptor>& aWaiting, + const Maybe<ServiceWorkerDescriptor>& aActive); + + void MaybeScheduleUpdateFound( + const Maybe<ServiceWorkerDescriptor>& aInstallingDescriptor); + + void MaybeDispatchUpdateFound(); + + void Shutdown(); + + ServiceWorkerRegistrationDescriptor mDescriptor; + RefPtr<ServiceWorkerRegistrationChild> mActor; + bool mShutdown; + + RefPtr<ServiceWorker> mInstallingWorker; + RefPtr<ServiceWorker> mWaitingWorker; + RefPtr<ServiceWorker> mActiveWorker; + RefPtr<NavigationPreloadManager> mNavigationPreloadManager; + RefPtr<PushManager> mPushManager; + + struct VersionCallback { + uint64_t mVersion; + ServiceWorkerBoolCallback mFunc; + + VersionCallback(uint64_t aVersion, ServiceWorkerBoolCallback&& aFunc) + : mVersion(aVersion), mFunc(std::move(aFunc)) { + MOZ_DIAGNOSTIC_ASSERT(mFunc); + } + }; + nsTArray<UniquePtr<VersionCallback>> mVersionCallbackList; + + uint64_t mScheduledUpdateFoundId; + uint64_t mDispatchedUpdateFoundId; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(ServiceWorkerRegistration, + NS_DOM_SERVICEWORKERREGISTRATION_IID) + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ServiceWorkerRegistration_h */ diff --git a/dom/serviceworkers/ServiceWorkerRegistrationChild.cpp b/dom/serviceworkers/ServiceWorkerRegistrationChild.cpp new file mode 100644 index 0000000000..b382f6dcfa --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationChild.cpp @@ -0,0 +1,91 @@ +/* -*- 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 "ServiceWorkerRegistrationChild.h" + +#include "ServiceWorkerRegistration.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +using mozilla::ipc::IPCResult; + +void ServiceWorkerRegistrationChild::ActorDestroy(ActorDestroyReason aReason) { + mIPCWorkerRef = nullptr; + + if (mOwner) { + mOwner->RevokeActor(this); + MOZ_DIAGNOSTIC_ASSERT(!mOwner); + } +} + +IPCResult ServiceWorkerRegistrationChild::RecvUpdateState( + const IPCServiceWorkerRegistrationDescriptor& aDescriptor) { + if (mOwner) { + RefPtr<ServiceWorkerRegistration> owner = mOwner; + owner->UpdateState(ServiceWorkerRegistrationDescriptor(aDescriptor)); + } + return IPC_OK(); +} + +IPCResult ServiceWorkerRegistrationChild::RecvFireUpdateFound() { + if (mOwner) { + mOwner->MaybeDispatchUpdateFoundRunnable(); + } + return IPC_OK(); +} + +// static +RefPtr<ServiceWorkerRegistrationChild> +ServiceWorkerRegistrationChild::Create() { + RefPtr actor = new ServiceWorkerRegistrationChild; + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_DIAGNOSTIC_ASSERT(workerPrivate); + + RefPtr<IPCWorkerRefHelper<ServiceWorkerRegistrationChild>> helper = + new IPCWorkerRefHelper<ServiceWorkerRegistrationChild>(actor); + + actor->mIPCWorkerRef = IPCWorkerRef::Create( + workerPrivate, "ServiceWorkerRegistrationChild", + [helper] { helper->Actor()->MaybeStartTeardown(); }); + + if (NS_WARN_IF(!actor->mIPCWorkerRef)) { + return nullptr; + } + } + + return actor; +} + +ServiceWorkerRegistrationChild::ServiceWorkerRegistrationChild() + : mOwner(nullptr), mTeardownStarted(false) {} + +void ServiceWorkerRegistrationChild::SetOwner( + ServiceWorkerRegistration* aOwner) { + MOZ_DIAGNOSTIC_ASSERT(!mOwner); + MOZ_DIAGNOSTIC_ASSERT(aOwner); + mOwner = aOwner; +} + +void ServiceWorkerRegistrationChild::RevokeOwner( + ServiceWorkerRegistration* aOwner) { + MOZ_DIAGNOSTIC_ASSERT(mOwner); + MOZ_DIAGNOSTIC_ASSERT(aOwner == mOwner); + mOwner = nullptr; +} + +void ServiceWorkerRegistrationChild::MaybeStartTeardown() { + if (mTeardownStarted) { + return; + } + mTeardownStarted = true; + Unused << SendTeardown(); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegistrationChild.h b/dom/serviceworkers/ServiceWorkerRegistrationChild.h new file mode 100644 index 0000000000..a26dfd6de9 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationChild.h @@ -0,0 +1,52 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerregistrationchild_h__ +#define mozilla_dom_serviceworkerregistrationchild_h__ + +#include "mozilla/dom/PServiceWorkerRegistrationChild.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +class IPCWorkerRef; +class ServiceWorkerRegistration; + +class ServiceWorkerRegistrationChild final + : public PServiceWorkerRegistrationChild { + RefPtr<IPCWorkerRef> mIPCWorkerRef; + ServiceWorkerRegistration* mOwner; + bool mTeardownStarted; + + ServiceWorkerRegistrationChild(); + + ~ServiceWorkerRegistrationChild() = default; + + // PServiceWorkerRegistrationChild + void ActorDestroy(ActorDestroyReason aReason) override; + + mozilla::ipc::IPCResult RecvUpdateState( + const IPCServiceWorkerRegistrationDescriptor& aDescriptor) override; + + mozilla::ipc::IPCResult RecvFireUpdateFound() override; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerRegistrationChild, override); + + static RefPtr<ServiceWorkerRegistrationChild> Create(); + + void SetOwner(ServiceWorkerRegistration* aOwner); + + void RevokeOwner(ServiceWorkerRegistration* aOwner); + + void MaybeStartTeardown(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerregistrationchild_h__ diff --git a/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.cpp b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.cpp new file mode 100644 index 0000000000..1988df8c4a --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.cpp @@ -0,0 +1,274 @@ +/* -*- 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/ServiceWorkerRegistrationDescriptor.h" + +#include "mozilla/dom/IPCServiceWorkerRegistrationDescriptor.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "ServiceWorkerInfo.h" + +namespace mozilla::dom { + +using mozilla::ipc::PrincipalInfo; +using mozilla::ipc::PrincipalInfoToPrincipal; + +Maybe<IPCServiceWorkerDescriptor> +ServiceWorkerRegistrationDescriptor::NewestInternal() const { + Maybe<IPCServiceWorkerDescriptor> result; + if (mData->installing().isSome()) { + result.emplace(mData->installing().ref()); + } else if (mData->waiting().isSome()) { + result.emplace(mData->waiting().ref()); + } else if (mData->active().isSome()) { + result.emplace(mData->active().ref()); + } + return result; +} + +ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor( + uint64_t aId, uint64_t aVersion, nsIPrincipal* aPrincipal, + const nsACString& aScope, ServiceWorkerUpdateViaCache aUpdateViaCache) + : mData(MakeUnique<IPCServiceWorkerRegistrationDescriptor>()) { + MOZ_ALWAYS_SUCCEEDS( + PrincipalToPrincipalInfo(aPrincipal, &mData->principalInfo())); + + mData->id() = aId; + mData->version() = aVersion; + mData->scope() = aScope; + mData->updateViaCache() = aUpdateViaCache; + mData->installing() = Nothing(); + mData->waiting() = Nothing(); + mData->active() = Nothing(); +} + +ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor( + uint64_t aId, uint64_t aVersion, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const nsACString& aScope, + ServiceWorkerUpdateViaCache aUpdateViaCache) + : mData(MakeUnique<IPCServiceWorkerRegistrationDescriptor>( + aId, aVersion, aPrincipalInfo, nsCString(aScope), aUpdateViaCache, + Nothing(), Nothing(), Nothing())) {} + +ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor( + const IPCServiceWorkerRegistrationDescriptor& aDescriptor) + : mData(MakeUnique<IPCServiceWorkerRegistrationDescriptor>(aDescriptor)) { + MOZ_DIAGNOSTIC_ASSERT(IsValid()); +} + +ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor( + const ServiceWorkerRegistrationDescriptor& aRight) { + // UniquePtr doesn't have a default copy constructor, so we can't rely + // on default copy construction. Use the assignment operator to + // minimize duplication. + operator=(aRight); +} + +ServiceWorkerRegistrationDescriptor& +ServiceWorkerRegistrationDescriptor::operator=( + const ServiceWorkerRegistrationDescriptor& aRight) { + if (this == &aRight) { + return *this; + } + mData.reset(); + mData = MakeUnique<IPCServiceWorkerRegistrationDescriptor>(*aRight.mData); + MOZ_DIAGNOSTIC_ASSERT(IsValid()); + return *this; +} + +ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor( + ServiceWorkerRegistrationDescriptor&& aRight) + : mData(std::move(aRight.mData)) { + MOZ_DIAGNOSTIC_ASSERT(IsValid()); +} + +ServiceWorkerRegistrationDescriptor& +ServiceWorkerRegistrationDescriptor::operator=( + ServiceWorkerRegistrationDescriptor&& aRight) { + mData.reset(); + mData = std::move(aRight.mData); + MOZ_DIAGNOSTIC_ASSERT(IsValid()); + return *this; +} + +ServiceWorkerRegistrationDescriptor::~ServiceWorkerRegistrationDescriptor() { + // Non-default destructor to avoid exposing the IPC type in the header. +} + +bool ServiceWorkerRegistrationDescriptor::operator==( + const ServiceWorkerRegistrationDescriptor& aRight) const { + return *mData == *aRight.mData; +} + +uint64_t ServiceWorkerRegistrationDescriptor::Id() const { return mData->id(); } + +uint64_t ServiceWorkerRegistrationDescriptor::Version() const { + return mData->version(); +} + +ServiceWorkerUpdateViaCache +ServiceWorkerRegistrationDescriptor::UpdateViaCache() const { + return mData->updateViaCache(); +} + +const mozilla::ipc::PrincipalInfo& +ServiceWorkerRegistrationDescriptor::PrincipalInfo() const { + return mData->principalInfo(); +} + +Result<nsCOMPtr<nsIPrincipal>, nsresult> +ServiceWorkerRegistrationDescriptor::GetPrincipal() const { + AssertIsOnMainThread(); + return PrincipalInfoToPrincipal(mData->principalInfo()); +} + +const nsCString& ServiceWorkerRegistrationDescriptor::Scope() const { + return mData->scope(); +} + +Maybe<ServiceWorkerDescriptor> +ServiceWorkerRegistrationDescriptor::GetInstalling() const { + Maybe<ServiceWorkerDescriptor> result; + + if (mData->installing().isSome()) { + result.emplace(ServiceWorkerDescriptor(mData->installing().ref())); + } + + return result; +} + +Maybe<ServiceWorkerDescriptor> ServiceWorkerRegistrationDescriptor::GetWaiting() + const { + Maybe<ServiceWorkerDescriptor> result; + + if (mData->waiting().isSome()) { + result.emplace(ServiceWorkerDescriptor(mData->waiting().ref())); + } + + return result; +} + +Maybe<ServiceWorkerDescriptor> ServiceWorkerRegistrationDescriptor::GetActive() + const { + Maybe<ServiceWorkerDescriptor> result; + + if (mData->active().isSome()) { + result.emplace(ServiceWorkerDescriptor(mData->active().ref())); + } + + return result; +} + +Maybe<ServiceWorkerDescriptor> ServiceWorkerRegistrationDescriptor::Newest() + const { + Maybe<ServiceWorkerDescriptor> result; + Maybe<IPCServiceWorkerDescriptor> newest(NewestInternal()); + if (newest.isSome()) { + result.emplace(ServiceWorkerDescriptor(newest.ref())); + } + return result; +} + +bool ServiceWorkerRegistrationDescriptor::HasWorker( + const ServiceWorkerDescriptor& aDescriptor) const { + Maybe<ServiceWorkerDescriptor> installing = GetInstalling(); + Maybe<ServiceWorkerDescriptor> waiting = GetWaiting(); + Maybe<ServiceWorkerDescriptor> active = GetActive(); + return (installing.isSome() && installing.ref().Matches(aDescriptor)) || + (waiting.isSome() && waiting.ref().Matches(aDescriptor)) || + (active.isSome() && active.ref().Matches(aDescriptor)); +} + +namespace { + +bool IsValidWorker( + const Maybe<IPCServiceWorkerDescriptor>& aWorker, const nsACString& aScope, + const mozilla::ipc::ContentPrincipalInfo& aContentPrincipal) { + if (aWorker.isNothing()) { + return true; + } + + auto& worker = aWorker.ref(); + if (worker.scope() != aScope) { + return false; + } + + auto& principalInfo = worker.principalInfo(); + if (principalInfo.type() != + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) { + return false; + } + + auto& contentPrincipal = principalInfo.get_ContentPrincipalInfo(); + if (contentPrincipal.originNoSuffix() != aContentPrincipal.originNoSuffix() || + contentPrincipal.attrs() != aContentPrincipal.attrs()) { + return false; + } + + return true; +} + +} // anonymous namespace + +bool ServiceWorkerRegistrationDescriptor::IsValid() const { + auto& principalInfo = PrincipalInfo(); + if (principalInfo.type() != + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) { + return false; + } + + auto& contentPrincipal = principalInfo.get_ContentPrincipalInfo(); + if (!IsValidWorker(mData->installing(), Scope(), contentPrincipal) || + !IsValidWorker(mData->waiting(), Scope(), contentPrincipal) || + !IsValidWorker(mData->active(), Scope(), contentPrincipal)) { + return false; + } + + return true; +} + +void ServiceWorkerRegistrationDescriptor::SetUpdateViaCache( + ServiceWorkerUpdateViaCache aUpdateViaCache) { + mData->updateViaCache() = aUpdateViaCache; +} + +void ServiceWorkerRegistrationDescriptor::SetWorkers( + ServiceWorkerInfo* aInstalling, ServiceWorkerInfo* aWaiting, + ServiceWorkerInfo* aActive) { + if (aInstalling) { + aInstalling->SetRegistrationVersion(Version()); + mData->installing() = Some(aInstalling->Descriptor().ToIPC()); + } else { + mData->installing() = Nothing(); + } + + if (aWaiting) { + aWaiting->SetRegistrationVersion(Version()); + mData->waiting() = Some(aWaiting->Descriptor().ToIPC()); + } else { + mData->waiting() = Nothing(); + } + + if (aActive) { + aActive->SetRegistrationVersion(Version()); + mData->active() = Some(aActive->Descriptor().ToIPC()); + } else { + mData->active() = Nothing(); + } + + MOZ_DIAGNOSTIC_ASSERT(IsValid()); +} + +void ServiceWorkerRegistrationDescriptor::SetVersion(uint64_t aVersion) { + MOZ_DIAGNOSTIC_ASSERT(aVersion > mData->version()); + mData->version() = aVersion; +} + +const IPCServiceWorkerRegistrationDescriptor& +ServiceWorkerRegistrationDescriptor::ToIPC() const { + return *mData; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.h b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.h new file mode 100644 index 0000000000..ec0f969868 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.h @@ -0,0 +1,103 @@ +/* -*- 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/. */ +#ifndef _mozilla_dom_ServiceWorkerRegistrationDescriptor_h +#define _mozilla_dom_ServiceWorkerRegistrationDescriptor_h + +#include "mozilla/Maybe.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class IPCServiceWorkerRegistrationDescriptor; +class ServiceWorkerInfo; +enum class ServiceWorkerUpdateViaCache : uint8_t; + +// This class represents a snapshot of a particular +// ServiceWorkerRegistrationInfo object. It is threadsafe and can be +// transferred across processes. +class ServiceWorkerRegistrationDescriptor final { + // This class is largely a wrapper wround an IPDL generated struct. We + // need the wrapper class since IPDL generated code includes windows.h + // which is in turn incompatible with bindings code. + UniquePtr<IPCServiceWorkerRegistrationDescriptor> mData; + + Maybe<IPCServiceWorkerDescriptor> NewestInternal() const; + + public: + ServiceWorkerRegistrationDescriptor( + uint64_t aId, uint64_t aVersion, nsIPrincipal* aPrincipal, + const nsACString& aScope, ServiceWorkerUpdateViaCache aUpdateViaCache); + + ServiceWorkerRegistrationDescriptor( + uint64_t aId, uint64_t aVersion, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsACString& aScope, ServiceWorkerUpdateViaCache aUpdateViaCache); + + explicit ServiceWorkerRegistrationDescriptor( + const IPCServiceWorkerRegistrationDescriptor& aDescriptor); + + ServiceWorkerRegistrationDescriptor( + const ServiceWorkerRegistrationDescriptor& aRight); + + ServiceWorkerRegistrationDescriptor& operator=( + const ServiceWorkerRegistrationDescriptor& aRight); + + ServiceWorkerRegistrationDescriptor( + ServiceWorkerRegistrationDescriptor&& aRight); + + ServiceWorkerRegistrationDescriptor& operator=( + ServiceWorkerRegistrationDescriptor&& aRight); + + ~ServiceWorkerRegistrationDescriptor(); + + bool operator==(const ServiceWorkerRegistrationDescriptor& aRight) const; + + uint64_t Id() const; + + uint64_t Version() const; + + ServiceWorkerUpdateViaCache UpdateViaCache() const; + + const mozilla::ipc::PrincipalInfo& PrincipalInfo() const; + + Result<nsCOMPtr<nsIPrincipal>, nsresult> GetPrincipal() const; + + const nsCString& Scope() const; + + Maybe<ServiceWorkerDescriptor> GetInstalling() const; + + Maybe<ServiceWorkerDescriptor> GetWaiting() const; + + Maybe<ServiceWorkerDescriptor> GetActive() const; + + Maybe<ServiceWorkerDescriptor> Newest() const; + + bool HasWorker(const ServiceWorkerDescriptor& aDescriptor) const; + + bool IsValid() const; + + void SetUpdateViaCache(ServiceWorkerUpdateViaCache aUpdateViaCache); + + void SetWorkers(ServiceWorkerInfo* aInstalling, ServiceWorkerInfo* aWaiting, + ServiceWorkerInfo* aActive); + + void SetVersion(uint64_t aVersion); + + // Expose the underlying IPC type so that it can be passed via IPC. + const IPCServiceWorkerRegistrationDescriptor& ToIPC() const; +}; + +} // namespace dom +} // namespace mozilla + +#endif // _mozilla_dom_ServiceWorkerRegistrationDescriptor_h diff --git a/dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp b/dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp new file mode 100644 index 0000000000..cf8c7543ee --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp @@ -0,0 +1,907 @@ +/* -*- 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 "ServiceWorkerRegistrationInfo.h" + +#include "ServiceWorkerManager.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerRegistrationListener.h" + +#include "mozilla/Preferences.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticPrefs_dom.h" + +namespace mozilla::dom { + +namespace { + +class ContinueActivateRunnable final : public LifeCycleEventCallback { + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + bool mSuccess; + + public: + explicit ContinueActivateRunnable( + const nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration) + : mRegistration(aRegistration), mSuccess(false) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void SetResult(bool aResult) override { mSuccess = aResult; } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + mRegistration->FinishActivate(mSuccess); + mRegistration = nullptr; + return NS_OK; + } +}; + +} // anonymous namespace + +void ServiceWorkerRegistrationInfo::ShutdownWorkers() { + ForEachWorker([](RefPtr<ServiceWorkerInfo>& aWorker) { + aWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo(); + aWorker = nullptr; + }); +} + +void ServiceWorkerRegistrationInfo::Clear() { + ForEachWorker([](RefPtr<ServiceWorkerInfo>& aWorker) { + aWorker->UpdateState(ServiceWorkerState::Redundant); + aWorker->UpdateRedundantTime(); + }); + + // FIXME: Abort any inflight requests from installing worker. + + ShutdownWorkers(); + UpdateRegistrationState(); + NotifyChromeRegistrationListeners(); + NotifyCleared(); +} + +void ServiceWorkerRegistrationInfo::ClearAsCorrupt() { + mCorrupt = true; + Clear(); +} + +bool ServiceWorkerRegistrationInfo::IsCorrupt() const { return mCorrupt; } + +ServiceWorkerRegistrationInfo::ServiceWorkerRegistrationInfo( + const nsACString& aScope, nsIPrincipal* aPrincipal, + ServiceWorkerUpdateViaCache aUpdateViaCache, + IPCNavigationPreloadState&& aNavigationPreloadState) + : mPrincipal(aPrincipal), + mDescriptor(GetNextId(), GetNextVersion(), aPrincipal, aScope, + aUpdateViaCache), + mControlledClientsCounter(0), + mDelayMultiplier(0), + mUpdateState(NoUpdate), + mCreationTime(PR_Now()), + mCreationTimeStamp(TimeStamp::Now()), + mLastUpdateTime(0), + mUnregistered(false), + mCorrupt(false), + mNavigationPreloadState(std::move(aNavigationPreloadState)) { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); +} + +ServiceWorkerRegistrationInfo::~ServiceWorkerRegistrationInfo() { + MOZ_DIAGNOSTIC_ASSERT(!IsControllingClients()); +} + +void ServiceWorkerRegistrationInfo::AddInstance( + ServiceWorkerRegistrationListener* aInstance, + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + MOZ_DIAGNOSTIC_ASSERT(aInstance); + MOZ_ASSERT(!mInstanceList.Contains(aInstance)); + MOZ_DIAGNOSTIC_ASSERT(aDescriptor.Id() == mDescriptor.Id()); + MOZ_DIAGNOSTIC_ASSERT(aDescriptor.PrincipalInfo() == + mDescriptor.PrincipalInfo()); + MOZ_DIAGNOSTIC_ASSERT(aDescriptor.Scope() == mDescriptor.Scope()); + MOZ_DIAGNOSTIC_ASSERT(aDescriptor.Version() <= mDescriptor.Version()); + uint64_t lastVersion = aDescriptor.Version(); + for (auto& entry : mVersionList) { + if (lastVersion > entry->mDescriptor.Version()) { + continue; + } + lastVersion = entry->mDescriptor.Version(); + aInstance->UpdateState(entry->mDescriptor); + } + // Note, the mDescriptor may be contained in the version list. Since the + // version list is aged out, though, it may also not be in the version list. + // So always check for the mDescriptor update here. + if (lastVersion < mDescriptor.Version()) { + aInstance->UpdateState(mDescriptor); + } + mInstanceList.AppendElement(aInstance); +} + +void ServiceWorkerRegistrationInfo::RemoveInstance( + ServiceWorkerRegistrationListener* aInstance) { + MOZ_DIAGNOSTIC_ASSERT(aInstance); + DebugOnly<bool> removed = mInstanceList.RemoveElement(aInstance); + MOZ_ASSERT(removed); +} + +const nsCString& ServiceWorkerRegistrationInfo::Scope() const { + return mDescriptor.Scope(); +} + +nsIPrincipal* ServiceWorkerRegistrationInfo::Principal() const { + return mPrincipal; +} + +bool ServiceWorkerRegistrationInfo::IsUnregistered() const { + return mUnregistered; +} + +void ServiceWorkerRegistrationInfo::SetUnregistered() { +#ifdef DEBUG + MOZ_ASSERT(!mUnregistered); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + MOZ_ASSERT(swm); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(Principal(), Scope()); + MOZ_ASSERT(registration != this); +#endif + + mUnregistered = true; + NotifyChromeRegistrationListeners(); +} + +NS_IMPL_ISUPPORTS(ServiceWorkerRegistrationInfo, + nsIServiceWorkerRegistrationInfo) + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetPrincipal(nsIPrincipal** aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ADDREF(*aPrincipal = mPrincipal); + return NS_OK; +} + +NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetUnregistered( + bool* aUnregistered) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aUnregistered); + *aUnregistered = mUnregistered; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetScope(nsAString& aScope) { + MOZ_ASSERT(NS_IsMainThread()); + CopyUTF8toUTF16(Scope(), aScope); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetScriptSpec(nsAString& aScriptSpec) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerInfo> newest = NewestIncludingEvaluating(); + if (newest) { + CopyUTF8toUTF16(newest->ScriptSpec(), aScriptSpec); + } + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetUpdateViaCache(uint16_t* aUpdateViaCache) { + *aUpdateViaCache = static_cast<uint16_t>(GetUpdateViaCache()); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetLastUpdateTime(PRTime* _retval) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(_retval); + *_retval = mLastUpdateTime; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetEvaluatingWorker( + nsIServiceWorkerInfo** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerInfo> info = mEvaluatingWorker; + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetInstallingWorker( + nsIServiceWorkerInfo** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerInfo> info = mInstallingWorker; + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetWaitingWorker( + nsIServiceWorkerInfo** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerInfo> info = mWaitingWorker; + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetActiveWorker(nsIServiceWorkerInfo** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerInfo> info = mActiveWorker; + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetQuotaUsageCheckCount( + int32_t* aQuotaUsageCheckCount) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aQuotaUsageCheckCount); + + // This value is actually stored on SWM's internal-only + // RegistrationDataPerPrincipal structure, but we expose it here for + // simplicity for our consumers, so we have to ask SWM to look it up for us. + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + MOZ_ASSERT(swm); + + *aQuotaUsageCheckCount = swm->GetPrincipalQuotaUsageCheckCount(mPrincipal); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetWorkerByID(uint64_t aID, + nsIServiceWorkerInfo** aResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aResult); + + RefPtr<ServiceWorkerInfo> info = GetServiceWorkerInfoById(aID); + // It is ok to return null for a missing service worker info. + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::AddListener( + nsIServiceWorkerRegistrationInfoListener* aListener) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aListener || mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::RemoveListener( + nsIServiceWorkerRegistrationInfoListener* aListener) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aListener || !mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::ForceShutdown() { + ClearInstalling(); + ShutdownWorkers(); + return NS_OK; +} + +already_AddRefed<ServiceWorkerInfo> +ServiceWorkerRegistrationInfo::GetServiceWorkerInfoById(uint64_t aId) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerInfo> serviceWorker; + if (mEvaluatingWorker && mEvaluatingWorker->ID() == aId) { + serviceWorker = mEvaluatingWorker; + } else if (mInstallingWorker && mInstallingWorker->ID() == aId) { + serviceWorker = mInstallingWorker; + } else if (mWaitingWorker && mWaitingWorker->ID() == aId) { + serviceWorker = mWaitingWorker; + } else if (mActiveWorker && mActiveWorker->ID() == aId) { + serviceWorker = mActiveWorker; + } + + return serviceWorker.forget(); +} + +void ServiceWorkerRegistrationInfo::TryToActivateAsync( + TryToActivateCallback&& aCallback) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread( + NewRunnableMethod<StoreCopyPassByRRef<TryToActivateCallback>>( + "ServiceWorkerRegistrationInfo::TryToActivate", this, + &ServiceWorkerRegistrationInfo::TryToActivate, + std::move(aCallback)))); +} + +/* + * TryToActivate should not be called directly, use TryToActivateAsync instead. + */ +void ServiceWorkerRegistrationInfo::TryToActivate( + TryToActivateCallback&& aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + bool controlling = IsControllingClients(); + bool skipWaiting = mWaitingWorker && mWaitingWorker->SkipWaitingFlag(); + bool idle = IsIdle(); + if (idle && (!controlling || skipWaiting)) { + Activate(); + } + + if (aCallback) { + aCallback(); + } +} + +void ServiceWorkerRegistrationInfo::Activate() { + if (!mWaitingWorker) { + return; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown began during async activation step + return; + } + + TransitionWaitingToActive(); + + // FIXME(nsm): Unlink appcache if there is one. + + // "Queue a task to fire a simple event named controllerchange..." + MOZ_DIAGNOSTIC_ASSERT(mActiveWorker); + swm->UpdateClientControllers(this); + + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> handle( + new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>( + "ServiceWorkerRegistrationInfoProxy", this)); + RefPtr<LifeCycleEventCallback> callback = + new ContinueActivateRunnable(handle); + + ServiceWorkerPrivate* workerPrivate = mActiveWorker->WorkerPrivate(); + MOZ_ASSERT(workerPrivate); + nsresult rv = workerPrivate->SendLifeCycleEvent(u"activate"_ns, callback); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsCOMPtr<nsIRunnable> failRunnable = NewRunnableMethod<bool>( + "dom::ServiceWorkerRegistrationInfo::FinishActivate", this, + &ServiceWorkerRegistrationInfo::FinishActivate, false /* success */); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(failRunnable.forget())); + return; + } +} + +void ServiceWorkerRegistrationInfo::FinishActivate(bool aSuccess) { + if (mUnregistered || !mActiveWorker || + mActiveWorker->State() != ServiceWorkerState::Activating) { + return; + } + + // Activation never fails, so aSuccess is ignored. + mActiveWorker->UpdateState(ServiceWorkerState::Activated); + mActiveWorker->UpdateActivatedTime(); + + UpdateRegistrationState(); + NotifyChromeRegistrationListeners(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown started during async activation completion step + return; + } + swm->StoreRegistration(mPrincipal, this); +} + +void ServiceWorkerRegistrationInfo::RefreshLastUpdateCheckTime() { + MOZ_ASSERT(NS_IsMainThread()); + + mLastUpdateTime = + mCreationTime + + static_cast<PRTime>( + (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds()); + NotifyChromeRegistrationListeners(); +} + +bool ServiceWorkerRegistrationInfo::IsLastUpdateCheckTimeOverOneDay() const { + MOZ_ASSERT(NS_IsMainThread()); + + // For testing. + if (Preferences::GetBool("dom.serviceWorkers.testUpdateOverOneDay")) { + return true; + } + + const int64_t kSecondsPerDay = 86400; + const int64_t nowMicros = + mCreationTime + + static_cast<PRTime>( + (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds()); + + // now < mLastUpdateTime if the system time is reset between storing + // and loading mLastUpdateTime from ServiceWorkerRegistrar. + if (nowMicros < mLastUpdateTime || + (nowMicros - mLastUpdateTime) / PR_USEC_PER_SEC > kSecondsPerDay) { + return true; + } + return false; +} + +void ServiceWorkerRegistrationInfo::UpdateRegistrationState() { + UpdateRegistrationState(mDescriptor.UpdateViaCache()); +} + +void ServiceWorkerRegistrationInfo::UpdateRegistrationState( + ServiceWorkerUpdateViaCache aUpdateViaCache) { + MOZ_ASSERT(NS_IsMainThread()); + + TimeStamp oldest = TimeStamp::Now() - TimeDuration::FromSeconds(30); + if (!mVersionList.IsEmpty() && mVersionList[0]->mTimeStamp < oldest) { + nsTArray<UniquePtr<VersionEntry>> list = std::move(mVersionList); + for (auto& entry : list) { + if (entry->mTimeStamp >= oldest) { + mVersionList.AppendElement(std::move(entry)); + } + } + } + mVersionList.AppendElement(MakeUnique<VersionEntry>(mDescriptor)); + + // We are going to modify the descriptor, so increase its version number. + mDescriptor.SetVersion(GetNextVersion()); + + // Note, this also sets the new version number on the ServiceWorkerInfo + // objects before we copy over their updated descriptors. + mDescriptor.SetWorkers(mInstallingWorker, mWaitingWorker, mActiveWorker); + + mDescriptor.SetUpdateViaCache(aUpdateViaCache); + + for (RefPtr<ServiceWorkerRegistrationListener> pinnedTarget : + mInstanceList.ForwardRange()) { + pinnedTarget->UpdateState(mDescriptor); + } +} + +void ServiceWorkerRegistrationInfo::NotifyChromeRegistrationListeners() { + nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> listeners( + mListeners.Clone()); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnChange(); + } +} + +void ServiceWorkerRegistrationInfo::MaybeScheduleTimeCheckAndUpdate() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // shutting down, do nothing + return; + } + + if (mUpdateState == NoUpdate) { + mUpdateState = NeedTimeCheckAndUpdate; + } + + swm->ScheduleUpdateTimer(mPrincipal, Scope()); +} + +void ServiceWorkerRegistrationInfo::MaybeScheduleUpdate() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // shutting down, do nothing + return; + } + + // When reach the navigation fault threshold, calling unregister instead of + // scheduling update. + if (mActiveWorker && !mUnregistered) { + uint32_t navigationFaultCount; + mActiveWorker->GetNavigationFaultCount(&navigationFaultCount); + const auto navigationFaultThreshold = StaticPrefs:: + dom_serviceWorkers_mitigations_navigation_fault_threshold(); + // Disable unregister mitigation when navigation fault threshold is 0. + if (navigationFaultThreshold <= navigationFaultCount && + navigationFaultThreshold != 0) { + CheckQuotaUsage(); + swm->Unregister(mPrincipal, nullptr, NS_ConvertUTF8toUTF16(Scope())); + return; + } + } + + mUpdateState = NeedUpdate; + + swm->ScheduleUpdateTimer(mPrincipal, Scope()); +} + +bool ServiceWorkerRegistrationInfo::CheckAndClearIfUpdateNeeded() { + MOZ_ASSERT(NS_IsMainThread()); + + bool result = + mUpdateState == NeedUpdate || (mUpdateState == NeedTimeCheckAndUpdate && + IsLastUpdateCheckTimeOverOneDay()); + + mUpdateState = NoUpdate; + + return result; +} + +ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetEvaluating() const { + MOZ_ASSERT(NS_IsMainThread()); + return mEvaluatingWorker; +} + +ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetInstalling() const { + MOZ_ASSERT(NS_IsMainThread()); + return mInstallingWorker; +} + +ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetWaiting() const { + MOZ_ASSERT(NS_IsMainThread()); + return mWaitingWorker; +} + +ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetActive() const { + MOZ_ASSERT(NS_IsMainThread()); + return mActiveWorker; +} + +ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetByDescriptor( + const ServiceWorkerDescriptor& aDescriptor) const { + if (mActiveWorker && mActiveWorker->Descriptor().Matches(aDescriptor)) { + return mActiveWorker; + } + if (mWaitingWorker && mWaitingWorker->Descriptor().Matches(aDescriptor)) { + return mWaitingWorker; + } + if (mInstallingWorker && + mInstallingWorker->Descriptor().Matches(aDescriptor)) { + return mInstallingWorker; + } + if (mEvaluatingWorker && + mEvaluatingWorker->Descriptor().Matches(aDescriptor)) { + return mEvaluatingWorker; + } + return nullptr; +} + +void ServiceWorkerRegistrationInfo::SetEvaluating( + ServiceWorkerInfo* aServiceWorker) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aServiceWorker); + MOZ_ASSERT(!mEvaluatingWorker); + MOZ_ASSERT(!mInstallingWorker); + MOZ_ASSERT(mWaitingWorker != aServiceWorker); + MOZ_ASSERT(mActiveWorker != aServiceWorker); + + mEvaluatingWorker = aServiceWorker; + + // We don't call UpdateRegistrationState() here because the evaluating worker + // is currently not exposed to content on the registration, so calling it here + // would produce redundant IPC traffic. + NotifyChromeRegistrationListeners(); +} + +void ServiceWorkerRegistrationInfo::ClearEvaluating() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mEvaluatingWorker) { + return; + } + + mEvaluatingWorker->UpdateState(ServiceWorkerState::Redundant); + // We don't update the redundant time for the sw here, since we've not expose + // evalutingWorker yet. + mEvaluatingWorker = nullptr; + + // As for SetEvaluating, UpdateRegistrationState() does not need to be called. + NotifyChromeRegistrationListeners(); +} + +void ServiceWorkerRegistrationInfo::ClearInstalling() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mInstallingWorker) { + return; + } + + RefPtr<ServiceWorkerInfo> installing = std::move(mInstallingWorker); + installing->UpdateState(ServiceWorkerState::Redundant); + installing->UpdateRedundantTime(); + + UpdateRegistrationState(); + NotifyChromeRegistrationListeners(); +} + +void ServiceWorkerRegistrationInfo::TransitionEvaluatingToInstalling() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mEvaluatingWorker); + MOZ_ASSERT(!mInstallingWorker); + + mInstallingWorker = std::move(mEvaluatingWorker); + mInstallingWorker->UpdateState(ServiceWorkerState::Installing); + + UpdateRegistrationState(); + NotifyChromeRegistrationListeners(); +} + +void ServiceWorkerRegistrationInfo::TransitionInstallingToWaiting() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInstallingWorker); + + if (mWaitingWorker) { + MOZ_ASSERT(mInstallingWorker->CacheName() != mWaitingWorker->CacheName()); + mWaitingWorker->UpdateState(ServiceWorkerState::Redundant); + mWaitingWorker->UpdateRedundantTime(); + } + + mWaitingWorker = std::move(mInstallingWorker); + mWaitingWorker->UpdateState(ServiceWorkerState::Installed); + mWaitingWorker->UpdateInstalledTime(); + + UpdateRegistrationState(); + NotifyChromeRegistrationListeners(); + + // TODO: When bug 1426401 is implemented we will need to call + // StoreRegistration() here to persist the waiting worker. +} + +void ServiceWorkerRegistrationInfo::SetActive( + ServiceWorkerInfo* aServiceWorker) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aServiceWorker); + + // TODO: Assert installing, waiting, and active are nullptr once the SWM + // moves to the parent process. After that happens this code will + // only run for browser initialization and not for cross-process + // overrides. + MOZ_ASSERT(mInstallingWorker != aServiceWorker); + MOZ_ASSERT(mWaitingWorker != aServiceWorker); + MOZ_ASSERT(mActiveWorker != aServiceWorker); + + if (mActiveWorker) { + MOZ_ASSERT(aServiceWorker->CacheName() != mActiveWorker->CacheName()); + mActiveWorker->UpdateState(ServiceWorkerState::Redundant); + mActiveWorker->UpdateRedundantTime(); + } + + // The active worker is being overriden due to initial load or + // another process activating a worker. Move straight to the + // Activated state. + mActiveWorker = aServiceWorker; + mActiveWorker->SetActivateStateUncheckedWithoutEvent( + ServiceWorkerState::Activated); + + // We don't need to update activated time when we load registration from + // registrar. + UpdateRegistrationState(); + NotifyChromeRegistrationListeners(); +} + +void ServiceWorkerRegistrationInfo::TransitionWaitingToActive() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mWaitingWorker); + + if (mActiveWorker) { + MOZ_ASSERT(mWaitingWorker->CacheName() != mActiveWorker->CacheName()); + mActiveWorker->UpdateState(ServiceWorkerState::Redundant); + mActiveWorker->UpdateRedundantTime(); + } + + // We are transitioning from waiting to active normally, so go to + // the activating state. + mActiveWorker = std::move(mWaitingWorker); + mActiveWorker->UpdateState(ServiceWorkerState::Activating); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "ServiceWorkerRegistrationInfo::TransitionWaitingToActive", [] { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->CheckPendingReadyPromises(); + } + }); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + UpdateRegistrationState(); + NotifyChromeRegistrationListeners(); +} + +bool ServiceWorkerRegistrationInfo::IsIdle() const { + return !mActiveWorker || mActiveWorker->WorkerPrivate()->IsIdle(); +} + +ServiceWorkerUpdateViaCache ServiceWorkerRegistrationInfo::GetUpdateViaCache() + const { + return mDescriptor.UpdateViaCache(); +} + +void ServiceWorkerRegistrationInfo::SetUpdateViaCache( + ServiceWorkerUpdateViaCache aUpdateViaCache) { + UpdateRegistrationState(aUpdateViaCache); +} + +int64_t ServiceWorkerRegistrationInfo::GetLastUpdateTime() const { + return mLastUpdateTime; +} + +void ServiceWorkerRegistrationInfo::SetLastUpdateTime(const int64_t aTime) { + if (aTime == 0) { + return; + } + + mLastUpdateTime = aTime; +} + +const ServiceWorkerRegistrationDescriptor& +ServiceWorkerRegistrationInfo::Descriptor() const { + return mDescriptor; +} + +uint64_t ServiceWorkerRegistrationInfo::Id() const { return mDescriptor.Id(); } + +uint64_t ServiceWorkerRegistrationInfo::Version() const { + return mDescriptor.Version(); +} + +uint32_t ServiceWorkerRegistrationInfo::GetUpdateDelay( + const bool aWithMultiplier) { + uint32_t delay = Preferences::GetInt("dom.serviceWorkers.update_delay", 1000); + + if (!aWithMultiplier) { + return delay; + } + + // This can potentially happen if you spam registration->Update(). We don't + // want to wrap to a lower value. + if (mDelayMultiplier >= INT_MAX / (delay ? delay : 1)) { + return INT_MAX; + } + + delay *= mDelayMultiplier; + + if (!mControlledClientsCounter && mDelayMultiplier < (INT_MAX / 30)) { + mDelayMultiplier = (mDelayMultiplier ? mDelayMultiplier : 1) * 30; + } + + return delay; +} + +void ServiceWorkerRegistrationInfo::FireUpdateFound() { + for (RefPtr<ServiceWorkerRegistrationListener> pinnedTarget : + mInstanceList.ForwardRange()) { + pinnedTarget->FireUpdateFound(); + } +} + +void ServiceWorkerRegistrationInfo::NotifyCleared() { + for (RefPtr<ServiceWorkerRegistrationListener> pinnedTarget : + mInstanceList.ForwardRange()) { + pinnedTarget->RegistrationCleared(); + } +} + +void ServiceWorkerRegistrationInfo::ClearWhenIdle() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsUnregistered()); + MOZ_ASSERT(!IsControllingClients()); + MOZ_ASSERT(!IsIdle(), "Already idle!"); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + MOZ_ASSERT(swm); + + swm->AddOrphanedRegistration(this); + + /** + * Although a Service Worker will transition to idle many times during its + * lifetime, the promise is only resolved once `GetIdlePromise` has been + * called, populating the `MozPromiseHolder`. Additionally, this is the only + * time this method will be called for the given ServiceWorker. This means we + * will be notified to the transition we are interested in, and there are no + * other callers to get confused. + * + * Note that because we are using `MozPromise`, our callback will be invoked + * as a separate task, so there is a small potential for races in the event + * code if things are still holding onto the ServiceWorker binding and using + * `postMessage()` or other mechanisms to schedule new events on it, which + * would make it non-idle. However, this is a race inherent in the spec which + * does not deal with the reality of multiple threads in "Try Clear + * Registration". + */ + GetActive()->WorkerPrivate()->GetIdlePromise()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<ServiceWorkerRegistrationInfo>(this)]( + const GenericPromise::ResolveOrRejectValue& aResult) { + MOZ_ASSERT(aResult.IsResolve()); + // This registration was already unregistered and not controlling + // clients when `ClearWhenIdle` was called, so there should be no way + // that more clients were acquired. + MOZ_ASSERT(!self->IsControllingClients()); + MOZ_ASSERT(self->IsIdle()); + self->Clear(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->RemoveOrphanedRegistration(self); + } + }); +} + +const nsID& ServiceWorkerRegistrationInfo::AgentClusterId() const { + return mAgentClusterId; +} + +void ServiceWorkerRegistrationInfo::SetNavigationPreloadEnabled( + const bool& aEnabled) { + MOZ_ASSERT(NS_IsMainThread()); + mNavigationPreloadState.enabled() = aEnabled; +} + +void ServiceWorkerRegistrationInfo::SetNavigationPreloadHeader( + const nsCString& aHeader) { + MOZ_ASSERT(NS_IsMainThread()); + mNavigationPreloadState.headerValue() = aHeader; +} + +IPCNavigationPreloadState +ServiceWorkerRegistrationInfo::GetNavigationPreloadState() const { + MOZ_ASSERT(NS_IsMainThread()); + return mNavigationPreloadState; +} + +// static +uint64_t ServiceWorkerRegistrationInfo::GetNextId() { + MOZ_ASSERT(NS_IsMainThread()); + static uint64_t sNextId = 0; + return ++sNextId; +} + +// static +uint64_t ServiceWorkerRegistrationInfo::GetNextVersion() { + MOZ_ASSERT(NS_IsMainThread()); + static uint64_t sNextVersion = 0; + return ++sNextVersion; +} + +void ServiceWorkerRegistrationInfo::ForEachWorker( + void (*aFunc)(RefPtr<ServiceWorkerInfo>&)) { + if (mEvaluatingWorker) { + aFunc(mEvaluatingWorker); + } + + if (mInstallingWorker) { + aFunc(mInstallingWorker); + } + + if (mWaitingWorker) { + aFunc(mWaitingWorker); + } + + if (mActiveWorker) { + aFunc(mActiveWorker); + } +} + +void ServiceWorkerRegistrationInfo::CheckQuotaUsage() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + MOZ_ASSERT(swm); + + swm->CheckPrincipalQuotaUsage(mPrincipal, Scope()); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegistrationInfo.h b/dom/serviceworkers/ServiceWorkerRegistrationInfo.h new file mode 100644 index 0000000000..b8bd75cf71 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationInfo.h @@ -0,0 +1,268 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerregistrationinfo_h +#define mozilla_dom_serviceworkerregistrationinfo_h + +#include <functional> + +#include "mozilla/dom/IPCNavigationPreloadState.h" +#include "mozilla/dom/ServiceWorkerInfo.h" +#include "mozilla/dom/ServiceWorkerRegistrationBinding.h" +#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h" +#include "nsProxyRelease.h" +#include "nsTObserverArray.h" + +namespace mozilla::dom { + +class ServiceWorkerRegistrationListener; + +class ServiceWorkerRegistrationInfo final + : public nsIServiceWorkerRegistrationInfo { + nsCOMPtr<nsIPrincipal> mPrincipal; + ServiceWorkerRegistrationDescriptor mDescriptor; + nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> mListeners; + nsTObserverArray<ServiceWorkerRegistrationListener*> mInstanceList; + + struct VersionEntry { + const ServiceWorkerRegistrationDescriptor mDescriptor; + TimeStamp mTimeStamp; + + explicit VersionEntry( + const ServiceWorkerRegistrationDescriptor& aDescriptor) + : mDescriptor(aDescriptor), mTimeStamp(TimeStamp::Now()) {} + }; + nsTArray<UniquePtr<VersionEntry>> mVersionList; + + const nsID mAgentClusterId = nsID::GenerateUUID(); + + uint32_t mControlledClientsCounter; + uint32_t mDelayMultiplier; + + enum { NoUpdate, NeedTimeCheckAndUpdate, NeedUpdate } mUpdateState; + + // Timestamp to track SWR's last update time + PRTime mCreationTime; + TimeStamp mCreationTimeStamp; + // The time of update is 0, if SWR've never been updated yet. + PRTime mLastUpdateTime; + + RefPtr<ServiceWorkerInfo> mEvaluatingWorker; + RefPtr<ServiceWorkerInfo> mActiveWorker; + RefPtr<ServiceWorkerInfo> mWaitingWorker; + RefPtr<ServiceWorkerInfo> mInstallingWorker; + + virtual ~ServiceWorkerRegistrationInfo(); + + // When unregister() is called on a registration, it is removed from the + // "scope to registration map" but not immediately "cleared" (i.e. its workers + // terminated, updated to the redundant state, etc.) because it may still be + // controlling clients. It is marked as unregistered and when all controlled + // clients go away, cleared. This way we can tell if a registration + // is unregistered by querying the object itself rather than incurring a table + // lookup (in the case when the registrations are passed around as pointers). + bool mUnregistered; + + bool mCorrupt; + + IPCNavigationPreloadState mNavigationPreloadState; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVICEWORKERREGISTRATIONINFO + + using TryToActivateCallback = std::function<void()>; + + ServiceWorkerRegistrationInfo( + const nsACString& aScope, nsIPrincipal* aPrincipal, + ServiceWorkerUpdateViaCache aUpdateViaCache, + IPCNavigationPreloadState&& aNavigationPreloadState); + + void AddInstance(ServiceWorkerRegistrationListener* aInstance, + const ServiceWorkerRegistrationDescriptor& aDescriptor); + + void RemoveInstance(ServiceWorkerRegistrationListener* aInstance); + + const nsCString& Scope() const; + + nsIPrincipal* Principal() const; + + bool IsUnregistered() const; + + void SetUnregistered(); + + already_AddRefed<ServiceWorkerInfo> Newest() const { + RefPtr<ServiceWorkerInfo> newest; + if (mInstallingWorker) { + newest = mInstallingWorker; + } else if (mWaitingWorker) { + newest = mWaitingWorker; + } else { + newest = mActiveWorker; + } + + return newest.forget(); + } + + already_AddRefed<ServiceWorkerInfo> NewestIncludingEvaluating() const { + if (mEvaluatingWorker) { + RefPtr<ServiceWorkerInfo> newest = mEvaluatingWorker; + return newest.forget(); + } + return Newest(); + } + + already_AddRefed<ServiceWorkerInfo> GetServiceWorkerInfoById(uint64_t aId); + + void StartControllingClient() { + ++mControlledClientsCounter; + mDelayMultiplier = 0; + } + + void StopControllingClient() { + MOZ_ASSERT(mControlledClientsCounter); + --mControlledClientsCounter; + } + + bool IsControllingClients() const { + return mActiveWorker && mControlledClientsCounter; + } + + // As a side effect, this nullifies + // `m{Evaluating,Installing,Waiting,Active}Worker`s. + void ShutdownWorkers(); + + void Clear(); + + void ClearAsCorrupt(); + + bool IsCorrupt() const; + + void TryToActivateAsync(TryToActivateCallback&& aCallback = nullptr); + + void TryToActivate(TryToActivateCallback&& aCallback); + + void Activate(); + + void FinishActivate(bool aSuccess); + + void RefreshLastUpdateCheckTime(); + + bool IsLastUpdateCheckTimeOverOneDay() const; + + void MaybeScheduleTimeCheckAndUpdate(); + + void MaybeScheduleUpdate(); + + bool CheckAndClearIfUpdateNeeded(); + + ServiceWorkerInfo* GetEvaluating() const; + + ServiceWorkerInfo* GetInstalling() const; + + ServiceWorkerInfo* GetWaiting() const; + + ServiceWorkerInfo* GetActive() const; + + ServiceWorkerInfo* GetByDescriptor( + const ServiceWorkerDescriptor& aDescriptor) const; + + // Set the given worker as the evaluating service worker. The worker + // state is not changed. + void SetEvaluating(ServiceWorkerInfo* aServiceWorker); + + // Remove an existing evaluating worker, if present. The worker will + // be transitioned to the Redundant state. + void ClearEvaluating(); + + // Remove an existing installing worker, if present. The worker will + // be transitioned to the Redundant state. + void ClearInstalling(); + + // Transition the current evaluating worker to be the installing worker. The + // worker's state is update to Installing. + void TransitionEvaluatingToInstalling(); + + // Transition the current installing worker to be the waiting worker. The + // worker's state is updated to Installed. + void TransitionInstallingToWaiting(); + + // Override the current active worker. This is used during browser + // initialization to load persisted workers. Its also used to propagate + // active workers across child processes in e10s. This second use will + // go away once the ServiceWorkerManager moves to the parent process. + // The worker is transitioned to the Activated state. + void SetActive(ServiceWorkerInfo* aServiceWorker); + + // Transition the current waiting worker to be the new active worker. The + // worker is updated to the Activating state. + void TransitionWaitingToActive(); + + // Determine if the registration is actively performing work. + bool IsIdle() const; + + ServiceWorkerUpdateViaCache GetUpdateViaCache() const; + + void SetUpdateViaCache(ServiceWorkerUpdateViaCache aUpdateViaCache); + + int64_t GetLastUpdateTime() const; + + void SetLastUpdateTime(const int64_t aTime); + + const ServiceWorkerRegistrationDescriptor& Descriptor() const; + + uint64_t Id() const; + + uint64_t Version() const; + + uint32_t GetUpdateDelay(const bool aWithMultiplier = true); + + void FireUpdateFound(); + + void NotifyCleared(); + + void ClearWhenIdle(); + + const nsID& AgentClusterId() const; + + void SetNavigationPreloadEnabled(const bool& aEnabled); + + void SetNavigationPreloadHeader(const nsCString& aHeader); + + IPCNavigationPreloadState GetNavigationPreloadState() const; + + private: + // Roughly equivalent to [[Update Registration State algorithm]]. Make sure + // this is called *before* updating SW instances' state, otherwise they + // may get CC-ed. + void UpdateRegistrationState(); + + void UpdateRegistrationState(ServiceWorkerUpdateViaCache aUpdateViaCache); + + // Used by devtools to track changes to the properties of + // *nsIServiceWorkerRegistrationInfo*. Note, this doesn't necessarily need to + // be in sync with the DOM registration objects, but it does need to be called + // in the same task that changed |mInstallingWorker|, |mWaitingWorker| or + // |mActiveWorker|. + void NotifyChromeRegistrationListeners(); + + static uint64_t GetNextId(); + + static uint64_t GetNextVersion(); + + // `aFunc`'s argument will be a reference to + // `m{Evaluating,Installing,Waiting,Active}Worker` (not to copy of them). + // Additionally, a null check will be performed for each worker before each + // call to `aFunc`, so `aFunc` will always get a reference to a non-null + // pointer. + void ForEachWorker(void (*aFunc)(RefPtr<ServiceWorkerInfo>&)); + + void CheckQuotaUsage(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerregistrationinfo_h diff --git a/dom/serviceworkers/ServiceWorkerRegistrationListener.h b/dom/serviceworkers/ServiceWorkerRegistrationListener.h new file mode 100644 index 0000000000..2b90f049a8 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationListener.h @@ -0,0 +1,35 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerRegistrationListener_h +#define mozilla_dom_ServiceWorkerRegistrationListener_h + +namespace mozilla::dom { + +class ServiceWorkerRegistrationDescriptor; + +// Used by ServiceWorkerManager to notify ServiceWorkerRegistrations of +// updatefound event and invalidating ServiceWorker instances. +class ServiceWorkerRegistrationListener { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void UpdateState( + const ServiceWorkerRegistrationDescriptor& aDescriptor) = 0; + + virtual void FireUpdateFound() = 0; + + virtual void RegistrationCleared() = 0; + + virtual void GetScope(nsAString& aScope) const = 0; + + virtual bool MatchesDescriptor( + const ServiceWorkerRegistrationDescriptor& aDescriptor) = 0; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ServiceWorkerRegistrationListener_h */ diff --git a/dom/serviceworkers/ServiceWorkerRegistrationParent.cpp b/dom/serviceworkers/ServiceWorkerRegistrationParent.cpp new file mode 100644 index 0000000000..d90bec3514 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationParent.cpp @@ -0,0 +1,152 @@ +/* -*- 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 "ServiceWorkerRegistrationParent.h" + +#include <utility> + +#include "ServiceWorkerRegistrationProxy.h" + +namespace mozilla::dom { + +using mozilla::ipc::IPCResult; + +void ServiceWorkerRegistrationParent::ActorDestroy(ActorDestroyReason aReason) { + if (mProxy) { + mProxy->RevokeActor(this); + mProxy = nullptr; + } +} + +IPCResult ServiceWorkerRegistrationParent::RecvTeardown() { + MaybeSendDelete(); + return IPC_OK(); +} + +namespace { + +void ResolveUnregister( + PServiceWorkerRegistrationParent::UnregisterResolver&& aResolver, + bool aSuccess, nsresult aRv) { + aResolver(Tuple<const bool&, const CopyableErrorResult&>( + aSuccess, CopyableErrorResult(aRv))); +} + +} // anonymous namespace + +IPCResult ServiceWorkerRegistrationParent::RecvUnregister( + UnregisterResolver&& aResolver) { + if (!mProxy) { + ResolveUnregister(std::move(aResolver), false, + NS_ERROR_DOM_INVALID_STATE_ERR); + return IPC_OK(); + } + + mProxy->Unregister()->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](bool aSuccess) mutable { + ResolveUnregister(std::move(aResolver), aSuccess, NS_OK); + }, + [aResolver](nsresult aRv) mutable { + ResolveUnregister(std::move(aResolver), false, aRv); + }); + + return IPC_OK(); +} + +IPCResult ServiceWorkerRegistrationParent::RecvUpdate( + const nsACString& aNewestWorkerScriptUrl, UpdateResolver&& aResolver) { + if (!mProxy) { + aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); + return IPC_OK(); + } + + mProxy->Update(aNewestWorkerScriptUrl) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) { + aResolver(aDescriptor.ToIPC()); + }, + [aResolver](const CopyableErrorResult& aResult) { + aResolver(aResult); + }); + + return IPC_OK(); +} + +IPCResult ServiceWorkerRegistrationParent::RecvSetNavigationPreloadEnabled( + const bool& aEnabled, SetNavigationPreloadEnabledResolver&& aResolver) { + if (!mProxy) { + aResolver(false); + return IPC_OK(); + } + + mProxy->SetNavigationPreloadEnabled(aEnabled)->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](bool) { aResolver(true); }, + [aResolver](nsresult) { aResolver(false); }); + + return IPC_OK(); +} + +IPCResult ServiceWorkerRegistrationParent::RecvSetNavigationPreloadHeader( + const nsACString& aHeader, SetNavigationPreloadHeaderResolver&& aResolver) { + if (!mProxy) { + aResolver(false); + return IPC_OK(); + } + + mProxy->SetNavigationPreloadHeader(aHeader)->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](bool) { aResolver(true); }, + [aResolver](nsresult) { aResolver(false); }); + + return IPC_OK(); +} + +IPCResult ServiceWorkerRegistrationParent::RecvGetNavigationPreloadState( + GetNavigationPreloadStateResolver&& aResolver) { + if (!mProxy) { + aResolver(Nothing()); + return IPC_OK(); + } + + mProxy->GetNavigationPreloadState()->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolver](const IPCNavigationPreloadState& aState) { + aResolver(Some(aState)); + }, + [aResolver](const CopyableErrorResult& aResult) { + aResolver(Nothing()); + }); + + return IPC_OK(); +} + +ServiceWorkerRegistrationParent::ServiceWorkerRegistrationParent() + : mDeleteSent(false) {} + +ServiceWorkerRegistrationParent::~ServiceWorkerRegistrationParent() { + MOZ_DIAGNOSTIC_ASSERT(!mProxy); +} + +void ServiceWorkerRegistrationParent::Init( + const IPCServiceWorkerRegistrationDescriptor& aDescriptor) { + MOZ_DIAGNOSTIC_ASSERT(!mProxy); + mProxy = new ServiceWorkerRegistrationProxy( + ServiceWorkerRegistrationDescriptor(aDescriptor)); + mProxy->Init(this); +} + +void ServiceWorkerRegistrationParent::MaybeSendDelete() { + if (mDeleteSent) { + return; + } + mDeleteSent = true; + Unused << Send__delete__(this); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegistrationParent.h b/dom/serviceworkers/ServiceWorkerRegistrationParent.h new file mode 100644 index 0000000000..5a6d751961 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationParent.h @@ -0,0 +1,58 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerregistrationparent_h__ +#define mozilla_dom_serviceworkerregistrationparent_h__ + +#include "mozilla/dom/PServiceWorkerRegistrationParent.h" + +namespace mozilla::dom { + +class IPCServiceWorkerRegistrationDescriptor; +class ServiceWorkerRegistrationProxy; + +class ServiceWorkerRegistrationParent final + : public PServiceWorkerRegistrationParent { + RefPtr<ServiceWorkerRegistrationProxy> mProxy; + bool mDeleteSent; + + ~ServiceWorkerRegistrationParent(); + + // PServiceWorkerRegistrationParent + void ActorDestroy(ActorDestroyReason aReason) override; + + mozilla::ipc::IPCResult RecvTeardown() override; + + mozilla::ipc::IPCResult RecvUnregister( + UnregisterResolver&& aResolver) override; + + mozilla::ipc::IPCResult RecvUpdate(const nsACString& aNewestWorkerScriptUrl, + UpdateResolver&& aResolver) override; + + mozilla::ipc::IPCResult RecvSetNavigationPreloadEnabled( + const bool& aEnabled, + SetNavigationPreloadEnabledResolver&& aResolver) override; + + mozilla::ipc::IPCResult RecvSetNavigationPreloadHeader( + const nsACString& aHeader, + SetNavigationPreloadHeaderResolver&& aResolver) override; + + mozilla::ipc::IPCResult RecvGetNavigationPreloadState( + GetNavigationPreloadStateResolver&& aResolver) override; + + public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerRegistrationParent, override); + + ServiceWorkerRegistrationParent(); + + void Init(const IPCServiceWorkerRegistrationDescriptor& aDescriptor); + + void MaybeSendDelete(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerregistrationparent_h__ diff --git a/dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp b/dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp new file mode 100644 index 0000000000..39f845bb56 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp @@ -0,0 +1,490 @@ +/* -*- 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 "ServiceWorkerRegistrationProxy.h" + +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerRegistrationParent.h" +#include "ServiceWorkerUnregisterCallback.h" + +namespace mozilla::dom { + +using mozilla::ipc::AssertIsOnBackgroundThread; + +class ServiceWorkerRegistrationProxy::DelayedUpdate final + : public nsITimerCallback, + public nsINamed { + RefPtr<ServiceWorkerRegistrationProxy> mProxy; + RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise; + nsCOMPtr<nsITimer> mTimer; + nsCString mNewestWorkerScriptUrl; + + ~DelayedUpdate() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + DelayedUpdate(RefPtr<ServiceWorkerRegistrationProxy>&& aProxy, + RefPtr<ServiceWorkerRegistrationPromise::Private>&& aPromise, + nsCString&& aNewestWorkerScriptUrl, uint32_t delay); + + void ChainTo(RefPtr<ServiceWorkerRegistrationPromise::Private> aPromise); + + void Reject(); + + void SetNewestWorkerScriptUrl(nsCString&& aNewestWorkerScriptUrl); +}; + +ServiceWorkerRegistrationProxy::~ServiceWorkerRegistrationProxy() { + // Any thread + MOZ_DIAGNOSTIC_ASSERT(!mActor); + MOZ_DIAGNOSTIC_ASSERT(!mReg); +} + +void ServiceWorkerRegistrationProxy::MaybeShutdownOnBGThread() { + AssertIsOnBackgroundThread(); + if (!mActor) { + return; + } + mActor->MaybeSendDelete(); +} + +void ServiceWorkerRegistrationProxy::UpdateStateOnBGThread( + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + AssertIsOnBackgroundThread(); + if (!mActor) { + return; + } + Unused << mActor->SendUpdateState(aDescriptor.ToIPC()); +} + +void ServiceWorkerRegistrationProxy::FireUpdateFoundOnBGThread() { + AssertIsOnBackgroundThread(); + if (!mActor) { + return; + } + Unused << mActor->SendFireUpdateFound(); +} + +void ServiceWorkerRegistrationProxy::InitOnMainThread() { + AssertIsOnMainThread(); + + auto scopeExit = MakeScopeExit([&] { MaybeShutdownOnMainThread(); }); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + RefPtr<ServiceWorkerRegistrationInfo> reg = + swm->GetRegistration(mDescriptor.PrincipalInfo(), mDescriptor.Scope()); + NS_ENSURE_TRUE_VOID(reg); + + if (reg->Id() != mDescriptor.Id()) { + // This registration has already been replaced by another one. + return; + } + + scopeExit.release(); + + mReg = new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>( + "ServiceWorkerRegistrationProxy::mInfo", reg); + + mReg->AddInstance(this, mDescriptor); +} + +void ServiceWorkerRegistrationProxy::MaybeShutdownOnMainThread() { + AssertIsOnMainThread(); + + if (mDelayedUpdate) { + mDelayedUpdate->Reject(); + mDelayedUpdate = nullptr; + } + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + __func__, this, &ServiceWorkerRegistrationProxy::MaybeShutdownOnBGThread); + + MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); +} + +void ServiceWorkerRegistrationProxy::StopListeningOnMainThread() { + AssertIsOnMainThread(); + + if (!mReg) { + return; + } + + mReg->RemoveInstance(this); + mReg = nullptr; +} + +void ServiceWorkerRegistrationProxy::UpdateState( + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + AssertIsOnMainThread(); + + if (mDescriptor == aDescriptor) { + return; + } + mDescriptor = aDescriptor; + + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod<ServiceWorkerRegistrationDescriptor>( + __func__, this, + &ServiceWorkerRegistrationProxy::UpdateStateOnBGThread, aDescriptor); + + MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); +} + +void ServiceWorkerRegistrationProxy::FireUpdateFound() { + AssertIsOnMainThread(); + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + __func__, this, + &ServiceWorkerRegistrationProxy::FireUpdateFoundOnBGThread); + + MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL)); +} + +void ServiceWorkerRegistrationProxy::RegistrationCleared() { + MaybeShutdownOnMainThread(); +} + +void ServiceWorkerRegistrationProxy::GetScope(nsAString& aScope) const { + CopyUTF8toUTF16(mDescriptor.Scope(), aScope); +} + +bool ServiceWorkerRegistrationProxy::MatchesDescriptor( + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + AssertIsOnMainThread(); + return aDescriptor.Id() == mDescriptor.Id() && + aDescriptor.PrincipalInfo() == mDescriptor.PrincipalInfo() && + aDescriptor.Scope() == mDescriptor.Scope(); +} + +ServiceWorkerRegistrationProxy::ServiceWorkerRegistrationProxy( + const ServiceWorkerRegistrationDescriptor& aDescriptor) + : mEventTarget(GetCurrentSerialEventTarget()), mDescriptor(aDescriptor) {} + +void ServiceWorkerRegistrationProxy::Init( + ServiceWorkerRegistrationParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(aActor); + MOZ_DIAGNOSTIC_ASSERT(!mActor); + MOZ_DIAGNOSTIC_ASSERT(mEventTarget); + + mActor = aActor; + + // Note, this must be done from a separate Init() method and not in + // the constructor. If done from the constructor the runnable can + // execute, complete, and release its reference before the constructor + // returns. + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("ServiceWorkerRegistrationProxy::Init", this, + &ServiceWorkerRegistrationProxy::InitOnMainThread); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); +} + +void ServiceWorkerRegistrationProxy::RevokeActor( + ServiceWorkerRegistrationParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mActor); + MOZ_DIAGNOSTIC_ASSERT(mActor == aActor); + mActor = nullptr; + + nsCOMPtr<nsIRunnable> r = NewRunnableMethod( + __func__, this, + &ServiceWorkerRegistrationProxy::StopListeningOnMainThread); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); +} + +RefPtr<GenericPromise> ServiceWorkerRegistrationProxy::Unregister() { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationProxy> self = this; + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [self, promise]() mutable { + nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR; + auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); }); + + NS_ENSURE_TRUE_VOID(self->mReg); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + RefPtr<UnregisterCallback> cb = new UnregisterCallback(promise); + + rv = swm->Unregister(self->mReg->Principal(), cb, + NS_ConvertUTF8toUTF16(self->mReg->Scope())); + NS_ENSURE_SUCCESS_VOID(rv); + + scopeExit.release(); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +namespace { + +class UpdateCallback final : public ServiceWorkerUpdateFinishCallback { + RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise; + + ~UpdateCallback() = default; + + public: + explicit UpdateCallback( + RefPtr<ServiceWorkerRegistrationPromise::Private>&& aPromise) + : mPromise(std::move(aPromise)) { + MOZ_DIAGNOSTIC_ASSERT(mPromise); + } + + void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) override { + mPromise->Resolve(aInfo->Descriptor(), __func__); + } + + void UpdateFailed(ErrorResult& aResult) override { + mPromise->Reject(CopyableErrorResult(aResult), __func__); + } +}; + +} // anonymous namespace + +NS_IMPL_ISUPPORTS(ServiceWorkerRegistrationProxy::DelayedUpdate, + nsITimerCallback, nsINamed) + +ServiceWorkerRegistrationProxy::DelayedUpdate::DelayedUpdate( + RefPtr<ServiceWorkerRegistrationProxy>&& aProxy, + RefPtr<ServiceWorkerRegistrationPromise::Private>&& aPromise, + nsCString&& aNewestWorkerScriptUrl, uint32_t delay) + : mProxy(std::move(aProxy)), + mPromise(std::move(aPromise)), + mNewestWorkerScriptUrl(std::move(aNewestWorkerScriptUrl)) { + MOZ_DIAGNOSTIC_ASSERT(mProxy); + MOZ_DIAGNOSTIC_ASSERT(mPromise); + MOZ_ASSERT(!mNewestWorkerScriptUrl.IsEmpty()); + mProxy->mDelayedUpdate = this; + Result<nsCOMPtr<nsITimer>, nsresult> result = + NS_NewTimerWithCallback(this, delay, nsITimer::TYPE_ONE_SHOT); + mTimer = result.unwrapOr(nullptr); + MOZ_DIAGNOSTIC_ASSERT(mTimer); +} + +void ServiceWorkerRegistrationProxy::DelayedUpdate::ChainTo( + RefPtr<ServiceWorkerRegistrationPromise::Private> aPromise) { + AssertIsOnMainThread(); + MOZ_ASSERT(mProxy->mDelayedUpdate == this); + MOZ_ASSERT(mPromise); + + mPromise->ChainTo(aPromise.forget(), __func__); +} + +void ServiceWorkerRegistrationProxy::DelayedUpdate::Reject() { + MOZ_DIAGNOSTIC_ASSERT(mPromise); + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); +} + +void ServiceWorkerRegistrationProxy::DelayedUpdate::SetNewestWorkerScriptUrl( + nsCString&& aNewestWorkerScriptUrl) { + MOZ_ASSERT(NS_IsMainThread()); + mNewestWorkerScriptUrl = std::move(aNewestWorkerScriptUrl); +} + +NS_IMETHODIMP +ServiceWorkerRegistrationProxy::DelayedUpdate::Notify(nsITimer* aTimer) { + // Already shutting down. + if (mProxy->mDelayedUpdate != this) { + return NS_OK; + } + + auto scopeExit = MakeScopeExit( + [&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); + + NS_ENSURE_TRUE(mProxy->mReg, NS_ERROR_FAILURE); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE(swm, NS_ERROR_FAILURE); + + RefPtr<UpdateCallback> cb = new UpdateCallback(std::move(mPromise)); + swm->Update(mProxy->mReg->Principal(), mProxy->mReg->Scope(), + std::move(mNewestWorkerScriptUrl), cb); + + mTimer = nullptr; + mProxy->mDelayedUpdate = nullptr; + + scopeExit.release(); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationProxy::DelayedUpdate::GetName(nsACString& aName) { + aName.AssignLiteral("ServiceWorkerRegistrationProxy::DelayedUpdate"); + return NS_OK; +} + +RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerRegistrationProxy::Update( + const nsACString& aNewestWorkerScriptUrl) { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationProxy> self = this; + RefPtr<ServiceWorkerRegistrationPromise::Private> promise = + new ServiceWorkerRegistrationPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, + [self, promise, + newestWorkerScriptUrl = nsCString(aNewestWorkerScriptUrl)]() mutable { + auto scopeExit = MakeScopeExit( + [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); + + // Get the delay value for the update + NS_ENSURE_TRUE_VOID(self->mReg); + uint32_t delay = self->mReg->GetUpdateDelay(false); + + // If the delay value does not equal to 0, create a timer and a timer + // callback to perform the delayed update. Otherwise, update directly. + if (delay) { + if (self->mDelayedUpdate) { + // NOTE: if we `ChainTo(),` there will ultimately be a single + // update, and this update will resolve all promises that were + // issued while the update's timer was ticking down. + self->mDelayedUpdate->ChainTo(std::move(promise)); + + // Use the "newest newest worker"'s script URL. + self->mDelayedUpdate->SetNewestWorkerScriptUrl( + std::move(newestWorkerScriptUrl)); + } else { + RefPtr<ServiceWorkerRegistrationProxy::DelayedUpdate> du = + new ServiceWorkerRegistrationProxy::DelayedUpdate( + std::move(self), std::move(promise), + std::move(newestWorkerScriptUrl), delay); + } + } else { + RefPtr<ServiceWorkerManager> swm = + ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + + RefPtr<UpdateCallback> cb = new UpdateCallback(std::move(promise)); + swm->Update(self->mReg->Principal(), self->mReg->Scope(), + std::move(newestWorkerScriptUrl), cb); + } + scopeExit.release(); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +RefPtr<GenericPromise> +ServiceWorkerRegistrationProxy::SetNavigationPreloadEnabled( + const bool& aEnabled) { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationProxy> self = this; + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [aEnabled, self, promise]() mutable { + nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR; + auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); }); + + NS_ENSURE_TRUE_VOID(self->mReg); + NS_ENSURE_TRUE_VOID(self->mReg->GetActive()); + + auto reg = self->mReg; + reg->SetNavigationPreloadEnabled(aEnabled); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + swm->StoreRegistration(reg->Principal(), reg); + + scopeExit.release(); + + promise->Resolve(true, __func__); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +RefPtr<GenericPromise> +ServiceWorkerRegistrationProxy::SetNavigationPreloadHeader( + const nsACString& aHeader) { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationProxy> self = this; + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [aHeader = nsCString(aHeader), self, promise]() mutable { + nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR; + auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); }); + + NS_ENSURE_TRUE_VOID(self->mReg); + NS_ENSURE_TRUE_VOID(self->mReg->GetActive()); + + auto reg = self->mReg; + reg->SetNavigationPreloadHeader(aHeader); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + NS_ENSURE_TRUE_VOID(swm); + swm->StoreRegistration(reg->Principal(), reg); + + scopeExit.release(); + + promise->Resolve(true, __func__); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +RefPtr<NavigationPreloadStatePromise> +ServiceWorkerRegistrationProxy::GetNavigationPreloadState() { + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerRegistrationProxy> self = this; + RefPtr<NavigationPreloadStatePromise::Private> promise = + new NavigationPreloadStatePromise::Private(__func__); + + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [self, promise]() mutable { + nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR; + auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); }); + + NS_ENSURE_TRUE_VOID(self->mReg); + scopeExit.release(); + + promise->Resolve(self->mReg->GetNavigationPreloadState(), __func__); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return promise; +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerRegistrationProxy.h b/dom/serviceworkers/ServiceWorkerRegistrationProxy.h new file mode 100644 index 0000000000..4253d2f259 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerRegistrationProxy.h @@ -0,0 +1,92 @@ +/* -*- 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/. */ + +#ifndef moz_dom_ServiceWorkerRegistrationProxy_h +#define moz_dom_ServiceWorkerRegistrationProxy_h + +#include "mozilla/dom/PServiceWorkerRegistrationParent.h" +#include "nsProxyRelease.h" +#include "ServiceWorkerRegistrationDescriptor.h" +#include "ServiceWorkerRegistrationListener.h" +#include "ServiceWorkerUtils.h" + +namespace mozilla::dom { + +class ServiceWorkerRegistrationInfo; +class ServiceWorkerRegistrationParent; + +class ServiceWorkerRegistrationProxy final + : public ServiceWorkerRegistrationListener { + // Background thread only + RefPtr<ServiceWorkerRegistrationParent> mActor; + + // Written on background thread and read on main thread + nsCOMPtr<nsISerialEventTarget> mEventTarget; + + // Main thread only + ServiceWorkerRegistrationDescriptor mDescriptor; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mReg; + + ~ServiceWorkerRegistrationProxy(); + + // Background thread methods + void MaybeShutdownOnBGThread(); + + void UpdateStateOnBGThread( + const ServiceWorkerRegistrationDescriptor& aDescriptor); + + void FireUpdateFoundOnBGThread(); + + // Main thread methods + void InitOnMainThread(); + + void MaybeShutdownOnMainThread(); + + void StopListeningOnMainThread(); + + // The timer callback to perform the delayed update + class DelayedUpdate; + RefPtr<DelayedUpdate> mDelayedUpdate; + + // ServiceWorkerRegistrationListener interface + void UpdateState( + const ServiceWorkerRegistrationDescriptor& aDescriptor) override; + + void FireUpdateFound() override; + + void RegistrationCleared() override; + + void GetScope(nsAString& aScope) const override; + + bool MatchesDescriptor( + const ServiceWorkerRegistrationDescriptor& aDescriptor) override; + + public: + explicit ServiceWorkerRegistrationProxy( + const ServiceWorkerRegistrationDescriptor& aDescriptor); + + void Init(ServiceWorkerRegistrationParent* aActor); + + void RevokeActor(ServiceWorkerRegistrationParent* aActor); + + RefPtr<GenericPromise> Unregister(); + + RefPtr<ServiceWorkerRegistrationPromise> Update( + const nsACString& aNewestWorkerScriptUrl); + + RefPtr<GenericPromise> SetNavigationPreloadEnabled(const bool& aEnabled); + + RefPtr<GenericPromise> SetNavigationPreloadHeader(const nsACString& aHeader); + + RefPtr<NavigationPreloadStatePromise> GetNavigationPreloadState(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerRegistrationProxy, + override); +}; + +} // namespace mozilla::dom + +#endif // moz_dom_ServiceWorkerRegistrationProxy_h diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.cpp b/dom/serviceworkers/ServiceWorkerScriptCache.cpp new file mode 100644 index 0000000000..77a51b2427 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerScriptCache.cpp @@ -0,0 +1,1503 @@ +/* -*- 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 "ServiceWorkerScriptCache.h" + +#include "js/Array.h" // JS::GetArrayLength +#include "js/PropertyAndElement.h" // JS_GetElement +#include "mozilla/Unused.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/dom/cache/Cache.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "nsICacheInfoChannel.h" +#include "nsIHttpChannel.h" +#include "nsIStreamLoader.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIUUIDGenerator.h" +#include "nsIXPConnect.h" + +#include "nsIInputStreamPump.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "ServiceWorkerManager.h" +#include "nsStringStream.h" + +using mozilla::dom::cache::Cache; +using mozilla::dom::cache::CacheStorage; +using mozilla::ipc::PrincipalInfo; + +namespace mozilla::dom::serviceWorkerScriptCache { + +namespace { + +already_AddRefed<CacheStorage> CreateCacheStorage(JSContext* aCx, + nsIPrincipal* aPrincipal, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + JS::Rooted<JSObject*> sandbox(aCx); + aRv = xpc->CreateSandbox(aCx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // This is called when the JSContext is not in a realm, so CreateSandbox + // returned an unwrapped global. + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + + nsCOMPtr<nsIGlobalObject> sandboxGlobalObject = xpc::NativeGlobal(sandbox); + if (!sandboxGlobalObject) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // We assume private browsing is not enabled here. The ScriptLoader + // explicitly fails for private browsing so there should never be + // a service worker running in private browsing mode. Therefore if + // we are purging scripts or running a comparison algorithm we cannot + // be in private browsing. + // + // Also, bypass the CacheStorage trusted origin checks. The ServiceWorker + // has validated the origin prior to this point. All the information + // to revalidate is not available now. + return CacheStorage::CreateOnMainThread(cache::CHROME_ONLY_NAMESPACE, + sandboxGlobalObject, aPrincipal, + true /* force trusted origin */, aRv); +} + +class CompareManager; +class CompareCache; + +// This class downloads a URL from the network, compare the downloaded script +// with an existing cache if provided, and report to CompareManager via calling +// ComparisonFinished(). +class CompareNetwork final : public nsIStreamLoaderObserver, + public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + NS_DECL_NSIREQUESTOBSERVER + + CompareNetwork(CompareManager* aManager, + ServiceWorkerRegistrationInfo* aRegistration, + bool aIsMainScript) + : mManager(aManager), + mRegistration(aRegistration), + mInternalHeaders(new InternalHeaders()), + mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER), + mState(WaitingForInitialization), + mNetworkResult(NS_OK), + mCacheResult(NS_OK), + mIsMainScript(aIsMainScript), + mIsFromCache(false) { + MOZ_ASSERT(aManager); + MOZ_ASSERT(NS_IsMainThread()); + } + + nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + Cache* const aCache); + + void Abort(); + + void NetworkFinish(nsresult aRv); + + void CacheFinish(nsresult aRv); + + const nsString& URL() const { + MOZ_ASSERT(NS_IsMainThread()); + return mURL; + } + + const nsString& Buffer() const { + MOZ_ASSERT(NS_IsMainThread()); + return mBuffer; + } + + const ChannelInfo& GetChannelInfo() const { return mChannelInfo; } + + already_AddRefed<InternalHeaders> GetInternalHeaders() const { + RefPtr<InternalHeaders> internalHeaders = mInternalHeaders; + return internalHeaders.forget(); + } + + UniquePtr<PrincipalInfo> TakePrincipalInfo() { + return std::move(mPrincipalInfo); + } + + bool Succeeded() const { return NS_SUCCEEDED(mNetworkResult); } + + const nsTArray<nsCString>& URLList() const { return mURLList; } + + private: + ~CompareNetwork() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mCC); + } + + void Finish(); + + nsresult SetPrincipalInfo(nsIChannel* aChannel); + + RefPtr<CompareManager> mManager; + RefPtr<CompareCache> mCC; + RefPtr<ServiceWorkerRegistrationInfo> mRegistration; + + nsCOMPtr<nsIChannel> mChannel; + nsString mBuffer; + nsString mURL; + ChannelInfo mChannelInfo; + RefPtr<InternalHeaders> mInternalHeaders; + UniquePtr<PrincipalInfo> mPrincipalInfo; + nsTArray<nsCString> mURLList; + + nsCString mMaxScope; + nsLoadFlags mLoadFlags; + + enum { + WaitingForInitialization, + WaitingForBothFinished, + WaitingForNetworkFinished, + WaitingForCacheFinished, + Finished + } mState; + + nsresult mNetworkResult; + nsresult mCacheResult; + + const bool mIsMainScript; + bool mIsFromCache; +}; + +NS_IMPL_ISUPPORTS(CompareNetwork, nsIStreamLoaderObserver, nsIRequestObserver) + +// This class gets a cached Response from the CacheStorage and then it calls +// CacheFinish() in the CompareNetwork. +class CompareCache final : public PromiseNativeHandler, + public nsIStreamLoaderObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + explicit CompareCache(CompareNetwork* aCN) + : mCN(aCN), mState(WaitingForInitialization), mInCache(false) { + MOZ_ASSERT(aCN); + MOZ_ASSERT(NS_IsMainThread()); + } + + nsresult Initialize(Cache* const aCache, const nsAString& aURL); + + void Finish(nsresult aStatus, bool aInCache); + + void Abort(); + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + const nsString& Buffer() const { + MOZ_ASSERT(NS_IsMainThread()); + return mBuffer; + } + + bool InCache() { return mInCache; } + + private: + ~CompareCache() { MOZ_ASSERT(NS_IsMainThread()); } + + void ManageValueResult(JSContext* aCx, JS::Handle<JS::Value> aValue); + + RefPtr<CompareNetwork> mCN; + nsCOMPtr<nsIInputStreamPump> mPump; + + nsString mURL; + nsString mBuffer; + + enum { + WaitingForInitialization, + WaitingForScript, + Finished, + } mState; + + bool mInCache; +}; + +NS_IMPL_ISUPPORTS(CompareCache, nsIStreamLoaderObserver) + +class CompareManager final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + explicit CompareManager(ServiceWorkerRegistrationInfo* aRegistration, + CompareCallback* aCallback) + : mRegistration(aRegistration), + mCallback(aCallback), + mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER), + mState(WaitingForInitialization), + mPendingCount(0), + mOnFailure(OnFailure::DoNothing), + mAreScriptsEqual(true) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRegistration); + } + + nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + const nsAString& aCacheName); + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + CacheStorage* CacheStorage_() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCacheStorage); + return mCacheStorage; + } + + void ComparisonFinished(nsresult aStatus, bool aIsMainScript, bool aIsEqual, + const nsACString& aMaxScope, nsLoadFlags aLoadFlags) { + MOZ_ASSERT(NS_IsMainThread()); + if (mState == Finished) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForScriptOrComparisonResult); + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + Fail(aStatus); + return; + } + + mAreScriptsEqual = mAreScriptsEqual && aIsEqual; + + if (aIsMainScript) { + mMaxScope = aMaxScope; + mLoadFlags = aLoadFlags; + } + + // Check whether all CompareNetworks finished their jobs. + MOZ_DIAGNOSTIC_ASSERT(mPendingCount > 0); + if (--mPendingCount) { + return; + } + + if (mAreScriptsEqual) { + MOZ_ASSERT(mCallback); + mCallback->ComparisonResult(aStatus, true /* aSameScripts */, mOnFailure, + u""_ns, mMaxScope, mLoadFlags); + Cleanup(); + return; + } + + // Write to Cache so ScriptLoader reads succeed. + WriteNetworkBufferToNewCache(); + } + + private: + ~CompareManager() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCNList.Length() == 0); + } + + void Fail(nsresult aStatus); + + void Cleanup(); + + nsresult FetchScript(const nsAString& aURL, bool aIsMainScript, + Cache* const aCache = nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization || + mState == WaitingForScriptOrComparisonResult); + + RefPtr<CompareNetwork> cn = + new CompareNetwork(this, mRegistration, aIsMainScript); + mCNList.AppendElement(cn); + mPendingCount += 1; + + nsresult rv = cn->Initialize(mPrincipal, aURL, aCache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + void ManageOldCache(JSContext* aCx, JS::Handle<JS::Value> aValue) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForExistingOpen); + + // RAII Cleanup when fails. + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { Fail(rv); }); + + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + MOZ_ASSERT(!mOldCache); + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj) || + NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Cache, obj, mOldCache)))) { + return; + } + + Optional<RequestOrUSVString> request; + CacheQueryOptions options; + ErrorResult error; + RefPtr<Promise> promise = mOldCache->Keys(aCx, request, options, error); + if (NS_WARN_IF(error.Failed())) { + // No exception here because there are no ReadableStreams involved here. + MOZ_ASSERT(!error.IsJSException()); + rv = error.StealNSResult(); + return; + } + + mState = WaitingForExistingKeys; + promise->AppendNativeHandler(this); + guard.release(); + } + + void ManageOldKeys(JSContext* aCx, JS::Handle<JS::Value> aValue) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForExistingKeys); + + // RAII Cleanup when fails. + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { Fail(rv); }); + + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj)) { + return; + } + + uint32_t len = 0; + if (!JS::GetArrayLength(aCx, obj, &len)) { + return; + } + + // Fetch and compare the source scripts. + MOZ_ASSERT(mPendingCount == 0); + + mState = WaitingForScriptOrComparisonResult; + + bool hasMainScript = false; + AutoTArray<nsString, 8> urlList; + + // Extract the list of URLs in the old cache. + for (uint32_t i = 0; i < len; ++i) { + JS::Rooted<JS::Value> val(aCx); + if (NS_WARN_IF(!JS_GetElement(aCx, obj, i, &val)) || + NS_WARN_IF(!val.isObject())) { + return; + } + + Request* request; + JS::Rooted<JSObject*> requestObj(aCx, &val.toObject()); + if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Request, &requestObj, request)))) { + return; + }; + + nsString url; + request->GetUrl(url); + + if (!hasMainScript && url == mURL) { + hasMainScript = true; + } + + urlList.AppendElement(url); + } + + // If the main script is missing, then something has gone wrong. We + // will try to continue with the update process to trigger a new + // installation. If that fails, however, then uninstall the registration + // because it is broken in a way that cannot be fixed. + if (!hasMainScript) { + mOnFailure = OnFailure::Uninstall; + } + + // Always make sure to fetch the main script. If the old cache has + // no entries or the main script entry is missing, then the loop below + // may not trigger it. This should not really happen, but we handle it + // gracefully if it does occur. Its possible the bad cache state is due + // to a crash or shutdown during an update, etc. + rv = FetchScript(mURL, true /* aIsMainScript */, mOldCache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + for (const auto& url : urlList) { + // We explicitly start the fetch for the main script above. + if (mURL == url) { + continue; + } + + rv = FetchScript(url, false /* aIsMainScript */, mOldCache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + guard.release(); + } + + void ManageNewCache(JSContext* aCx, JS::Handle<JS::Value> aValue) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForOpen); + + // RAII Cleanup when fails. + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { Fail(rv); }); + + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj)) { + return; + } + + Cache* cache = nullptr; + rv = UNWRAP_OBJECT(Cache, &obj, cache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Just to be safe. + RefPtr<Cache> kungfuDeathGrip = cache; + + MOZ_ASSERT(mPendingCount == 0); + for (uint32_t i = 0; i < mCNList.Length(); ++i) { + // We bail out immediately when something goes wrong. + rv = WriteToCache(aCx, cache, mCNList[i]); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + mState = WaitingForPut; + guard.release(); + } + + void WriteNetworkBufferToNewCache() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCNList.Length() != 0); + MOZ_ASSERT(mCacheStorage); + MOZ_ASSERT(mNewCacheName.IsEmpty()); + + ErrorResult result; + result = serviceWorkerScriptCache::GenerateCacheName(mNewCacheName); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + RefPtr<Promise> cacheOpenPromise = + mCacheStorage->Open(mNewCacheName, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + mState = WaitingForOpen; + cacheOpenPromise->AppendNativeHandler(this); + } + + nsresult WriteToCache(JSContext* aCx, Cache* aCache, CompareNetwork* aCN) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCache); + MOZ_ASSERT(aCN); + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForOpen); + + // We don't have to save any information from a failed CompareNetwork. + if (!aCN->Succeeded()) { + return NS_OK; + } + + nsCOMPtr<nsIInputStream> body; + nsresult rv = NS_NewCStringInputStream( + getter_AddRefs(body), NS_ConvertUTF16toUTF8(aCN->Buffer())); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + SafeRefPtr<InternalResponse> ir = + MakeSafeRefPtr<InternalResponse>(200, "OK"_ns); + ir->SetBody(body, aCN->Buffer().Length()); + ir->SetURLList(aCN->URLList()); + + ir->InitChannelInfo(aCN->GetChannelInfo()); + UniquePtr<PrincipalInfo> principalInfo = aCN->TakePrincipalInfo(); + if (principalInfo) { + ir->SetPrincipalInfo(std::move(principalInfo)); + } + + RefPtr<InternalHeaders> internalHeaders = aCN->GetInternalHeaders(); + ir->Headers()->Fill(*(internalHeaders.get()), IgnoreErrors()); + + RefPtr<Response> response = + new Response(aCache->GetGlobalObject(), std::move(ir), nullptr); + + RequestOrUSVString request; + request.SetAsUSVString().ShareOrDependUpon(aCN->URL()); + + // For now we have to wait until the Put Promise is fulfilled before we can + // continue since Cache does not yet support starting a read that is being + // written to. + ErrorResult result; + RefPtr<Promise> cachePromise = aCache->Put(aCx, request, *response, result); + result.WouldReportJSException(); + if (NS_WARN_IF(result.Failed())) { + // No exception here because there are no ReadableStreams involved here. + MOZ_ASSERT(!result.IsJSException()); + MOZ_ASSERT(!result.IsErrorWithMessage()); + return result.StealNSResult(); + } + + mPendingCount += 1; + cachePromise->AppendNativeHandler(this); + return NS_OK; + } + + RefPtr<ServiceWorkerRegistrationInfo> mRegistration; + RefPtr<CompareCallback> mCallback; + RefPtr<CacheStorage> mCacheStorage; + + nsTArray<RefPtr<CompareNetwork>> mCNList; + + nsString mURL; + RefPtr<nsIPrincipal> mPrincipal; + + // Used for the old cache where saves the old source scripts. + RefPtr<Cache> mOldCache; + + // Only used if the network script has changed and needs to be cached. + nsString mNewCacheName; + + nsCString mMaxScope; + nsLoadFlags mLoadFlags; + + enum { + WaitingForInitialization, + WaitingForExistingOpen, + WaitingForExistingKeys, + WaitingForScriptOrComparisonResult, + WaitingForOpen, + WaitingForPut, + Finished + } mState; + + uint32_t mPendingCount; + OnFailure mOnFailure; + bool mAreScriptsEqual; +}; + +NS_IMPL_ISUPPORTS0(CompareManager) + +nsresult CompareNetwork::Initialize(nsIPrincipal* aPrincipal, + const nsAString& aURL, + Cache* const aCache) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mURL = aURL; + mURLList.AppendElement(NS_ConvertUTF16toUTF8(mURL)); + + nsCOMPtr<nsILoadGroup> loadGroup; + rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), aPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update LoadFlags for propagating to ServiceWorkerInfo. + mLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + + ServiceWorkerUpdateViaCache uvc = mRegistration->GetUpdateViaCache(); + if (uvc == ServiceWorkerUpdateViaCache::None || + (uvc == ServiceWorkerUpdateViaCache::Imports && mIsMainScript)) { + mLoadFlags |= nsIRequest::VALIDATE_ALWAYS; + } + + if (mRegistration->IsLastUpdateCheckTimeOverOneDay()) { + mLoadFlags |= nsIRequest::LOAD_BYPASS_CACHE; + } + + // Different settings are needed for fetching imported scripts, since they + // might be cross-origin scripts. + uint32_t secFlags = + mIsMainScript ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + + nsContentPolicyType contentPolicyType = + mIsMainScript ? nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER + : nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS; + + // Create a new cookieJarSettings. + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + mozilla::net::CookieJarSettings::Create(aPrincipal); + + // Populate the partitionKey by using the given prinicpal. The ServiceWorkers + // are using the foreign partitioned principal, so we can get the partitionKey + // from it and the partitionKey will only exist if it's in the third-party + // context. In first-party context, we can still use the uri to set the + // partitionKey. + if (!aPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) { + net::CookieJarSettings::Cast(cookieJarSettings) + ->SetPartitionKey(aPrincipal->OriginAttributesRef().mPartitionKey); + } else { + net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); + } + + // Note that because there is no "serviceworker" RequestContext type, we can + // use the TYPE_INTERNAL_SCRIPT content policy types when loading a service + // worker. + rv = NS_NewChannel(getter_AddRefs(mChannel), uri, aPrincipal, secFlags, + contentPolicyType, cookieJarSettings, + nullptr /* aPerformanceStorage */, loadGroup, + nullptr /* aCallbacks */, mLoadFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + // Spec says no redirects allowed for top-level SW scripts. + if (mIsMainScript) { + rv = httpChannel->SetRedirectionLimit(0); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = httpChannel->SetRequestHeader("Service-Worker"_ns, "script"_ns, + /* merge */ false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mChannel->AsyncOpen(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // If we do have an existing cache to compare with. + if (aCache) { + mCC = new CompareCache(this); + rv = mCC->Initialize(aCache, aURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + Abort(); + return rv; + } + + mState = WaitingForBothFinished; + return NS_OK; + } + + mState = WaitingForNetworkFinished; + return NS_OK; +} + +void CompareNetwork::Finish() { + if (mState == Finished) { + return; + } + + bool same = true; + nsresult rv = NS_OK; + + // mNetworkResult is prior to mCacheResult, since it's needed for reporting + // various errors to web content. + if (NS_FAILED(mNetworkResult)) { + // An imported script could become offline, since it might no longer be + // needed by the new importing script. In that case, the importing script + // must be different, and thus, it's okay to report same script found here. + rv = mIsMainScript ? mNetworkResult : NS_OK; + same = true; + } else if (mCC && NS_FAILED(mCacheResult)) { + rv = mCacheResult; + } else { // Both passed. + same = mCC && mCC->InCache() && mCC->Buffer().Equals(mBuffer); + } + + mManager->ComparisonFinished(rv, mIsMainScript, same, mMaxScope, mLoadFlags); + + // We have done with the CompareCache. + mCC = nullptr; +} + +void CompareNetwork::NetworkFinish(nsresult aRv) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForBothFinished || + mState == WaitingForNetworkFinished); + + mNetworkResult = aRv; + + if (mState == WaitingForBothFinished) { + mState = WaitingForCacheFinished; + return; + } + + if (mState == WaitingForNetworkFinished) { + Finish(); + return; + } +} + +void CompareNetwork::CacheFinish(nsresult aRv) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForBothFinished || + mState == WaitingForCacheFinished); + + mCacheResult = aRv; + + if (mState == WaitingForBothFinished) { + mState = WaitingForNetworkFinished; + return; + } + + if (mState == WaitingForCacheFinished) { + Finish(); + return; + } +} + +void CompareNetwork::Abort() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + mState = Finished; + + MOZ_ASSERT(mChannel); + mChannel->CancelWithReason(NS_BINDING_ABORTED, "CompareNetwork::Abort"_ns); + mChannel = nullptr; + + if (mCC) { + mCC->Abort(); + mCC = nullptr; + } + } +} + +NS_IMETHODIMP +CompareNetwork::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState == Finished) { + return NS_OK; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + MOZ_ASSERT_IF(mIsMainScript, channel == mChannel); + mChannel = channel; + + MOZ_ASSERT(!mChannelInfo.IsInitialized()); + mChannelInfo.InitFromChannel(mChannel); + + nsresult rv = SetPrincipalInfo(mChannel); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInternalHeaders->FillResponseHeaders(mChannel); + + nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(channel)); + if (cacheChannel) { + cacheChannel->IsFromCache(&mIsFromCache); + } + + return NS_OK; +} + +nsresult CompareNetwork::SetPrincipalInfo(nsIChannel* aChannel) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (!ssm) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIPrincipal> channelPrincipal; + nsresult rv = ssm->GetChannelResultPrincipal( + aChannel, getter_AddRefs(channelPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + UniquePtr<PrincipalInfo> principalInfo = MakeUnique<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get()); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mPrincipalInfo = std::move(principalInfo); + return NS_OK; +} + +NS_IMETHODIMP +CompareNetwork::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + // Nothing to do here! + return NS_OK; +} + +NS_IMETHODIMP +CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aLen, const uint8_t* aString) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState == Finished) { + return NS_OK; + } + + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { NetworkFinish(rv); }); + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + rv = (aStatus == NS_ERROR_REDIRECT_LOOP) ? NS_ERROR_DOM_SECURITY_ERR + : aStatus; + return NS_OK; + } + + nsCOMPtr<nsIRequest> request; + rv = aLoader->GetRequest(getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + MOZ_ASSERT(channel, "How come we don't have any channel?"); + + nsCOMPtr<nsIURI> uri; + channel->GetOriginalURI(getter_AddRefs(uri)); + bool isExtension = uri->SchemeIs("moz-extension"); + + if (isExtension && + !StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) { + // Return earlier with error is the worker script is a moz-extension url + // but the feature isn't enabled by prefs. + return NS_ERROR_FAILURE; + } + + if (isExtension) { + // NOTE: trying to register any moz-extension use that doesn't ends + // with .js/.jsm/.mjs seems to be already completing with an error + // in aStatus and they never reach this point. + + // TODO: look into avoid duplicated parts that could be shared with the HTTP + // channel scenario. + nsCOMPtr<nsIURI> channelURL; + rv = channel->GetURI(getter_AddRefs(channelURL)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString channelURLSpec; + MOZ_ALWAYS_SUCCEEDS(channelURL->GetSpec(channelURLSpec)); + + // Append the final URL (which for an extension worker script is going to + // be a file or jar url). + MOZ_DIAGNOSTIC_ASSERT(!mURLList.IsEmpty()); + if (channelURLSpec != mURLList[0]) { + mURLList.AppendElement(channelURLSpec); + } + + char16_t* buffer = nullptr; + size_t len = 0; + + rv = ScriptLoader::ConvertToUTF16(channel, aString, aLen, u"UTF-8"_ns, + nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBuffer.Adopt(buffer, len); + + rv = NS_OK; + return NS_OK; + } + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request); + + // Main scripts cannot be redirected successfully, however extensions + // may successfuly redirect imported scripts to a moz-extension url + // (if listed in the web_accessible_resources manifest property). + // + // When the service worker is initially registered the imported scripts + // will be loaded from the child process (see dom/workers/ScriptLoader.cpp) + // and in that case this method will only be called for the main script. + // + // When a registered worker is loaded again (e.g. when the webpage calls + // the ServiceWorkerRegistration's update method): + // + // - both the main and imported scripts are loaded by the + // CompareManager::FetchScript + // - the update requests for the imported scripts will also be calling this + // method and we should expect scripts redirected to an extension script + // to have a null httpChannel. + // + // The request that triggers this method is: + // + // - the one that is coming from the network (which may be intercepted by + // WebRequest listeners in extensions and redirected to a web_accessible + // moz-extension url) + // - it will then be compared with a previous response that we may have + // in the cache + // + // When the next service worker update occurs, if the request (for an imported + // script) is not redirected by an extension the cache entry is invalidated + // and a network request is triggered for the import. + if (!httpChannel) { + // Redirecting a service worker main script should fail before reaching this + // method. + // If a main script is somehow redirected, the diagnostic assert will crash + // in non-release builds. Release builds will return an explicit error. + MOZ_DIAGNOSTIC_ASSERT(!mIsMainScript, + "Unexpected ServiceWorker main script redirected"); + if (mIsMainScript) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIPrincipal> channelPrincipal; + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (!ssm) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = ssm->GetChannelResultPrincipal( + channel, getter_AddRefs(channelPrincipal)); + + // An extension did redirect a non-MainScript request to a moz-extension url + // (in that case the originalURL is the resolved jar URI and so we have to + // look to the channel principal instead). + if (channelPrincipal->SchemeIs("moz-extension")) { + char16_t* buffer = nullptr; + size_t len = 0; + + rv = ScriptLoader::ConvertToUTF16(channel, aString, aLen, u"UTF-8"_ns, + nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBuffer.Adopt(buffer, len); + + return NS_OK; + } + + // Make non-release and debug builds to crash if this happens and fail + // explicitly on release builds. + MOZ_DIAGNOSTIC_ASSERT(false, + "ServiceWorker imported script redirected to an url " + "with an unexpected scheme"); + return NS_ERROR_UNEXPECTED; + } + + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + if (NS_WARN_IF(!requestSucceeded)) { + // Get the stringified numeric status code, not statusText which could be + // something misleading like OK for a 404. + uint32_t status = 0; + Unused << httpChannel->GetResponseStatus( + &status); // don't care if this fails, use 0. + nsAutoString statusAsText; + statusAsText.AppendInt(status); + + ServiceWorkerManager::LocalizeAndReportToAllClients( + mRegistration->Scope(), "ServiceWorkerRegisterNetworkError", + nsTArray<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()), + statusAsText, mURL}); + + rv = NS_ERROR_FAILURE; + return NS_OK; + } + + // Note: we explicitly don't check for the return value here, because the + // absence of the header is not an error condition. + Unused << httpChannel->GetResponseHeader("Service-Worker-Allowed"_ns, + mMaxScope); + + // [9.2 Update]4.13, If response's cache state is not "local", + // set registration's last update check time to the current time + if (!mIsFromCache) { + mRegistration->RefreshLastUpdateCheckTime(); + } + + nsAutoCString mimeType; + rv = httpChannel->GetContentType(mimeType); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We should only end up here if !mResponseHead in the channel. If headers + // were received but no content type was specified, we'll be given + // UNKNOWN_CONTENT_TYPE "application/x-unknown-content-type" and so fall + // into the next case with its better error message. + rv = NS_ERROR_DOM_SECURITY_ERR; + return rv; + } + + if (mimeType.IsEmpty() || + !nsContentUtils::IsJavascriptMIMEType(NS_ConvertUTF8toUTF16(mimeType))) { + ServiceWorkerManager::LocalizeAndReportToAllClients( + mRegistration->Scope(), "ServiceWorkerRegisterMimeTypeError2", + nsTArray<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()), + NS_ConvertUTF8toUTF16(mimeType), mURL}); + rv = NS_ERROR_DOM_SECURITY_ERR; + return rv; + } + + nsCOMPtr<nsIURI> channelURL; + rv = httpChannel->GetURI(getter_AddRefs(channelURL)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString channelURLSpec; + MOZ_ALWAYS_SUCCEEDS(channelURL->GetSpec(channelURLSpec)); + + // Append the final URL if its different from the original + // request URL. This lets us note that a redirect occurred + // even though we don't track every redirect URL here. + MOZ_DIAGNOSTIC_ASSERT(!mURLList.IsEmpty()); + if (channelURLSpec != mURLList[0]) { + mURLList.AppendElement(channelURLSpec); + } + + char16_t* buffer = nullptr; + size_t len = 0; + + rv = ScriptLoader::ConvertToUTF16(httpChannel, aString, aLen, u"UTF-8"_ns, + nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBuffer.Adopt(buffer, len); + + rv = NS_OK; + return NS_OK; +} + +nsresult CompareCache::Initialize(Cache* const aCache, const nsAString& aURL) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCache); + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization); + + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + + RequestOrUSVString request; + request.SetAsUSVString().ShareOrDependUpon(aURL); + ErrorResult error; + CacheQueryOptions params; + RefPtr<Promise> promise = aCache->Match(jsapi.cx(), request, params, error); + if (NS_WARN_IF(error.Failed())) { + // No exception here because there are no ReadableStreams involved here. + MOZ_ASSERT(!error.IsJSException()); + mState = Finished; + return error.StealNSResult(); + } + + // Retrieve the script from aCache. + mState = WaitingForScript; + promise->AppendNativeHandler(this); + return NS_OK; +} + +void CompareCache::Finish(nsresult aStatus, bool aInCache) { + if (mState != Finished) { + mState = Finished; + mInCache = aInCache; + mCN->CacheFinish(aStatus); + } +} + +void CompareCache::Abort() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + mState = Finished; + + if (mPump) { + mPump->CancelWithReason(NS_BINDING_ABORTED, "CompareCache::Abort"_ns); + mPump = nullptr; + } + } +} + +NS_IMETHODIMP +CompareCache::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aLen, + const uint8_t* aString) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState == Finished) { + return aStatus; + } + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + Finish(aStatus, false); + return aStatus; + } + + char16_t* buffer = nullptr; + size_t len = 0; + + nsresult rv = ScriptLoader::ConvertToUTF16(nullptr, aString, aLen, + u"UTF-8"_ns, nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return rv; + } + + mBuffer.Adopt(buffer, len); + + Finish(NS_OK, true); + return NS_OK; +} + +void CompareCache::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + switch (mState) { + case Finished: + return; + case WaitingForScript: + ManageValueResult(aCx, aValue); + return; + default: + MOZ_CRASH("Unacceptable state."); + } +} + +void CompareCache::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + Finish(NS_ERROR_FAILURE, false); + return; + } +} + +void CompareCache::ManageValueResult(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + MOZ_ASSERT(NS_IsMainThread()); + + // The cache returns undefined if the object is not stored. + if (aValue.isUndefined()) { + Finish(NS_OK, false); + return; + } + + MOZ_ASSERT(aValue.isObject()); + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj)) { + Finish(NS_ERROR_FAILURE, false); + return; + } + + Response* response = nullptr; + nsresult rv = UNWRAP_OBJECT(Response, &obj, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return; + } + + MOZ_ASSERT(response->Ok()); + + nsCOMPtr<nsIInputStream> inputStream; + response->GetBody(getter_AddRefs(inputStream)); + MOZ_ASSERT(inputStream); + + MOZ_ASSERT(!mPump); + rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream.forget(), + 0, /* default segsize */ + 0, /* default segcount */ + false, /* default closeWhenDone */ + GetMainThreadSerialEventTarget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return; + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return; + } + + rv = mPump->AsyncRead(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + Finish(rv, false); + return; + } + + nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(mPump); + if (rr) { + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + rv = rr->RetargetDeliveryTo(sts); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + Finish(rv, false); + return; + } + } +} + +nsresult CompareManager::Initialize(nsIPrincipal* aPrincipal, + const nsAString& aURL, + const nsAString& aCacheName) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(mPendingCount == 0); + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization); + + // RAII Cleanup when fails. + auto guard = MakeScopeExit([&] { Cleanup(); }); + + mURL = aURL; + mPrincipal = aPrincipal; + + // Always create a CacheStorage since we want to write the network entry to + // the cache even if there isn't an existing one. + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult result; + mCacheStorage = CreateCacheStorage(jsapi.cx(), aPrincipal, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + return result.StealNSResult(); + } + + // If there is no existing cache, proceed to fetch the script directly. + if (aCacheName.IsEmpty()) { + mState = WaitingForScriptOrComparisonResult; + nsresult rv = FetchScript(aURL, true /* aIsMainScript */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + guard.release(); + return NS_OK; + } + + // Open the cache saving the old source scripts. + RefPtr<Promise> promise = mCacheStorage->Open(aCacheName, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + return result.StealNSResult(); + } + + mState = WaitingForExistingOpen; + promise->AppendNativeHandler(this); + + guard.release(); + return NS_OK; +} + +// This class manages 4 promises if needed: +// 1. Retrieve the Cache object by a given CacheName of OldCache. +// 2. Retrieve the URLs saved in OldCache. +// 3. Retrieve the Cache object of the NewCache for the newly created SW. +// 4. Put the value in the cache. +// For this reason we have mState to know what callback we are handling. +void CompareManager::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCallback); + + switch (mState) { + case Finished: + return; + case WaitingForExistingOpen: + ManageOldCache(aCx, aValue); + return; + case WaitingForExistingKeys: + ManageOldKeys(aCx, aValue); + return; + case WaitingForOpen: + ManageNewCache(aCx, aValue); + return; + case WaitingForPut: + MOZ_DIAGNOSTIC_ASSERT(mPendingCount > 0); + if (--mPendingCount == 0) { + mCallback->ComparisonResult(NS_OK, false /* aIsEqual */, mOnFailure, + mNewCacheName, mMaxScope, mLoadFlags); + Cleanup(); + } + return; + default: + MOZ_DIAGNOSTIC_ASSERT(false); + } +} + +void CompareManager::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + switch (mState) { + case Finished: + return; + case WaitingForExistingOpen: + NS_WARNING("Could not open the existing cache."); + break; + case WaitingForExistingKeys: + NS_WARNING("Could not get the existing URLs."); + break; + case WaitingForOpen: + NS_WARNING("Could not open cache."); + break; + case WaitingForPut: + NS_WARNING("Could not write to cache."); + break; + default: + MOZ_DIAGNOSTIC_ASSERT(false); + } + + Fail(NS_ERROR_FAILURE); +} + +void CompareManager::Fail(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + mCallback->ComparisonResult(aStatus, false /* aIsEqual */, mOnFailure, u""_ns, + ""_ns, mLoadFlags); + Cleanup(); +} + +void CompareManager::Cleanup() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + mState = Finished; + + MOZ_ASSERT(mCallback); + mCallback = nullptr; + + // Abort and release CompareNetworks. + for (uint32_t i = 0; i < mCNList.Length(); ++i) { + mCNList[i]->Abort(); + } + mCNList.Clear(); + } +} + +} // namespace + +nsresult PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (aCacheName.IsEmpty()) { + return NS_OK; + } + + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult rv; + RefPtr<CacheStorage> cacheStorage = + CreateCacheStorage(jsapi.cx(), aPrincipal, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + // We use the ServiceWorker scope as key for the cacheStorage. + RefPtr<Promise> promise = cacheStorage->Delete(aCacheName, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + // Set [[PromiseIsHandled]] to ensure that if this promise gets rejected, + // we don't end up reporting a rejected promise to the console. + MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled()); + + // We don't actually care about the result of the delete operation. + return NS_OK; +} + +nsresult GenerateCacheName(nsAString& aName) { + nsresult rv; + nsCOMPtr<nsIUUIDGenerator> uuidGenerator = + do_GetService("@mozilla.org/uuid-generator;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsID id; + rv = uuidGenerator->GenerateUUIDInPlace(&id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + char chars[NSID_LENGTH]; + id.ToProvidedString(chars); + + // NSID_LENGTH counts the null terminator. + aName.AssignASCII(chars, NSID_LENGTH - 1); + + return NS_OK; +} + +nsresult Compare(ServiceWorkerRegistrationInfo* aRegistration, + nsIPrincipal* aPrincipal, const nsAString& aCacheName, + const nsAString& aURL, CompareCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRegistration); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aURL.IsEmpty()); + MOZ_ASSERT(aCallback); + + RefPtr<CompareManager> cm = new CompareManager(aRegistration, aCallback); + + nsresult rv = cm->Initialize(aPrincipal, aURL, aCacheName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +} // namespace mozilla::dom::serviceWorkerScriptCache diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.h b/dom/serviceworkers/ServiceWorkerScriptCache.h new file mode 100644 index 0000000000..5d71840b46 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerScriptCache.h @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerScriptCache_h +#define mozilla_dom_ServiceWorkerScriptCache_h + +#include "nsIRequest.h" +#include "nsISupportsImpl.h" +#include "nsString.h" + +class nsILoadGroup; +class nsIPrincipal; + +namespace mozilla::dom { + +class ServiceWorkerRegistrationInfo; + +namespace serviceWorkerScriptCache { + +nsresult PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName); + +nsresult GenerateCacheName(nsAString& aName); + +enum class OnFailure : uint8_t { DoNothing, Uninstall }; + +class CompareCallback { + public: + /* + * If there is an error, ignore aInCacheAndEqual and aNewCacheName. + * On success, if the cached result and network result matched, + * aInCacheAndEqual will be true and no new cache name is passed, otherwise + * use the new cache name to load the ServiceWorker. + */ + virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual, + OnFailure aOnFailure, + const nsAString& aNewCacheName, + const nsACString& aMaxScope, + nsLoadFlags aLoadFlags) = 0; + + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING +}; + +nsresult Compare(ServiceWorkerRegistrationInfo* aRegistration, + nsIPrincipal* aPrincipal, const nsAString& aCacheName, + const nsAString& aURL, CompareCallback* aCallback); + +} // namespace serviceWorkerScriptCache + +} // namespace mozilla::dom + +#endif // mozilla_dom_ServiceWorkerScriptCache_h diff --git a/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp b/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp new file mode 100644 index 0000000000..05a2eb5c31 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp @@ -0,0 +1,291 @@ +/* -*- 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 "ServiceWorkerShutdownBlocker.h" + +#include <chrono> +#include <utility> + +#include "MainThreadUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIWritablePropertyBag2.h" +#include "nsThreadUtils.h" +#include "ServiceWorkerManager.h" + +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(ServiceWorkerShutdownBlocker, nsIAsyncShutdownBlocker, + nsITimerCallback, nsINamed) + +NS_IMETHODIMP ServiceWorkerShutdownBlocker::GetName(nsAString& aNameOut) { + aNameOut = nsLiteralString( + u"ServiceWorkerShutdownBlocker: shutting down Service Workers"); + return NS_OK; +} + +// nsINamed implementation +NS_IMETHODIMP ServiceWorkerShutdownBlocker::GetName(nsACString& aNameOut) { + aNameOut.AssignLiteral("ServiceWorkerShutdownBlocker"); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aClient) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mShutdownClient); + MOZ_ASSERT(mServiceWorkerManager); + + mShutdownClient = aClient; + + (*mServiceWorkerManager)->MaybeStartShutdown(); + mServiceWorkerManager.destroy(); + + MaybeUnblockShutdown(); + MaybeInitUnblockShutdownTimer(); + + return NS_OK; +} + +NS_IMETHODIMP ServiceWorkerShutdownBlocker::GetState(nsIPropertyBag** aBagOut) { + AssertIsOnMainThread(); + MOZ_ASSERT(aBagOut); + + nsCOMPtr<nsIWritablePropertyBag2> propertyBag = + do_CreateInstance("@mozilla.org/hash-property-bag;1"); + + if (NS_WARN_IF(!propertyBag)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsresult rv = propertyBag->SetPropertyAsBool(u"acceptingPromises"_ns, + IsAcceptingPromises()); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = propertyBag->SetPropertyAsUint32(u"pendingPromises"_ns, + GetPendingPromises()); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString shutdownStates; + + for (auto iter = mShutdownStates.iter(); !iter.done(); iter.next()) { + shutdownStates.Append(iter.get().value().GetProgressString()); + shutdownStates.Append(", "); + } + + rv = propertyBag->SetPropertyAsACString(u"shutdownStates"_ns, shutdownStates); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + propertyBag.forget(aBagOut); + + return NS_OK; +} + +/* static */ already_AddRefed<ServiceWorkerShutdownBlocker> +ServiceWorkerShutdownBlocker::CreateAndRegisterOn( + nsIAsyncShutdownClient& aShutdownBarrier, + ServiceWorkerManager& aServiceWorkerManager) { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerShutdownBlocker> blocker = + new ServiceWorkerShutdownBlocker(aServiceWorkerManager); + + nsresult rv = aShutdownBarrier.AddBlocker( + blocker.get(), NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, + u"Service Workers shutdown"_ns); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return blocker.forget(); +} + +void ServiceWorkerShutdownBlocker::WaitOnPromise( + GenericNonExclusivePromise* aPromise, uint32_t aShutdownStateId) { + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(IsAcceptingPromises()); + MOZ_ASSERT(aPromise); + MOZ_ASSERT(mShutdownStates.has(aShutdownStateId)); + + ++mState.as<AcceptingPromises>().mPendingPromises; + + RefPtr<ServiceWorkerShutdownBlocker> self = this; + + aPromise->Then(GetCurrentSerialEventTarget(), __func__, + [self = std::move(self), shutdownStateId = aShutdownStateId]( + const GenericNonExclusivePromise::ResolveOrRejectValue&) { + // Progress reporting might race with aPromise settling. + self->mShutdownStates.remove(shutdownStateId); + + if (!self->PromiseSettled()) { + self->MaybeUnblockShutdown(); + } + }); +} + +void ServiceWorkerShutdownBlocker::StopAcceptingPromises() { + AssertIsOnMainThread(); + MOZ_ASSERT(IsAcceptingPromises()); + + mState = AsVariant(NotAcceptingPromises(mState.as<AcceptingPromises>())); + + MaybeUnblockShutdown(); + MaybeInitUnblockShutdownTimer(); +} + +uint32_t ServiceWorkerShutdownBlocker::CreateShutdownState() { + AssertIsOnMainThread(); + + static uint32_t nextShutdownStateId = 1; + + MOZ_ALWAYS_TRUE(mShutdownStates.putNew(nextShutdownStateId, + ServiceWorkerShutdownState())); + + return nextShutdownStateId++; +} + +void ServiceWorkerShutdownBlocker::ReportShutdownProgress( + uint32_t aShutdownStateId, Progress aProgress) { + AssertIsOnMainThread(); + MOZ_RELEASE_ASSERT(aShutdownStateId != kInvalidShutdownStateId); + + auto lookup = mShutdownStates.lookup(aShutdownStateId); + + // Progress reporting might race with the promise that WaitOnPromise is called + // with settling. + if (!lookup) { + return; + } + + // This will check for a valid progress transition with assertions. + lookup->value().SetProgress(aProgress); + + if (aProgress == Progress::ShutdownCompleted) { + mShutdownStates.remove(lookup); + } +} + +ServiceWorkerShutdownBlocker::ServiceWorkerShutdownBlocker( + ServiceWorkerManager& aServiceWorkerManager) + : mState(VariantType<AcceptingPromises>()), + mServiceWorkerManager(WrapNotNull(&aServiceWorkerManager)) { + AssertIsOnMainThread(); +} + +ServiceWorkerShutdownBlocker::~ServiceWorkerShutdownBlocker() { + MOZ_ASSERT(!IsAcceptingPromises()); + MOZ_ASSERT(!GetPendingPromises()); + MOZ_ASSERT(!mShutdownClient); + MOZ_ASSERT(!mServiceWorkerManager); +} + +void ServiceWorkerShutdownBlocker::MaybeUnblockShutdown() { + AssertIsOnMainThread(); + + if (!mShutdownClient || IsAcceptingPromises() || GetPendingPromises()) { + return; + } + + UnblockShutdown(); +} + +void ServiceWorkerShutdownBlocker::UnblockShutdown() { + MOZ_ASSERT(mShutdownClient); + + mShutdownClient->RemoveBlocker(this); + mShutdownClient = nullptr; + + if (mTimer) { + mTimer->Cancel(); + } +} + +uint32_t ServiceWorkerShutdownBlocker::PromiseSettled() { + AssertIsOnMainThread(); + MOZ_ASSERT(GetPendingPromises()); + + if (IsAcceptingPromises()) { + return --mState.as<AcceptingPromises>().mPendingPromises; + } + + return --mState.as<NotAcceptingPromises>().mPendingPromises; +} + +bool ServiceWorkerShutdownBlocker::IsAcceptingPromises() const { + AssertIsOnMainThread(); + + return mState.is<AcceptingPromises>(); +} + +uint32_t ServiceWorkerShutdownBlocker::GetPendingPromises() const { + AssertIsOnMainThread(); + + if (IsAcceptingPromises()) { + return mState.as<AcceptingPromises>().mPendingPromises; + } + + return mState.as<NotAcceptingPromises>().mPendingPromises; +} + +ServiceWorkerShutdownBlocker::NotAcceptingPromises::NotAcceptingPromises( + AcceptingPromises aPreviousState) + : mPendingPromises(aPreviousState.mPendingPromises) { + AssertIsOnMainThread(); +} + +NS_IMETHODIMP ServiceWorkerShutdownBlocker::Notify(nsITimer*) { + // TODO: this method being called indicates that there are ServiceWorkers + // that did not complete shutdown before the timer expired - there should be + // a telemetry ping or some other way of recording the state of when this + // happens (e.g. what's returned by GetState()). + UnblockShutdown(); + return NS_OK; +} + +#ifdef RELEASE_OR_BETA +# define SW_UNBLOCK_SHUTDOWN_TIMER_DURATION 10s +#else +// In Nightly, we do want a shutdown hang to be reported so we pick a value +// notably longer than the 60s of the RunWatchDog timeout. +# define SW_UNBLOCK_SHUTDOWN_TIMER_DURATION 200s +#endif + +void ServiceWorkerShutdownBlocker::MaybeInitUnblockShutdownTimer() { + AssertIsOnMainThread(); + + if (mTimer || !mShutdownClient || IsAcceptingPromises()) { + return; + } + + MOZ_ASSERT(GetPendingPromises(), + "Shouldn't be blocking shutdown with zero pending promises."); + + using namespace std::chrono_literals; + + static constexpr auto delay = + std::chrono::duration_cast<std::chrono::milliseconds>( + SW_UNBLOCK_SHUTDOWN_TIMER_DURATION); + + mTimer = NS_NewTimer(); + + mTimer->InitWithCallback(this, delay.count(), nsITimer::TYPE_ONE_SHOT); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerShutdownBlocker.h b/dom/serviceworkers/ServiceWorkerShutdownBlocker.h new file mode 100644 index 0000000000..a200325c5b --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerShutdownBlocker.h @@ -0,0 +1,157 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkershutdownblocker_h__ +#define mozilla_dom_serviceworkershutdownblocker_h__ + +#include "nsCOMPtr.h" +#include "nsIAsyncShutdown.h" +#include "nsISupportsImpl.h" +#include "nsITimer.h" + +#include "ServiceWorkerShutdownState.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/MozPromise.h" +#include "mozilla/NotNull.h" +#include "mozilla/HashTable.h" + +namespace mozilla::dom { + +class ServiceWorkerManager; + +/** + * Main thread only. + * + * A ServiceWorkerShutdownBlocker will "accept promises", and each of these + * promises will be a "pending promise" while it hasn't settled. At some point, + * `StopAcceptingPromises()` should be called and the state will change to "not + * accepting promises" (this is a one way state transition). The shutdown phase + * of the shutdown client the blocker is created with will be blocked until + * there are no more pending promises. + * + * It doesn't matter whether the state changes to "not accepting promises" + * before or during the associated shutdown phase. + * + * In beta/release builds there will be an additional timer that starts ticking + * once both the shutdown phase has been reached and the state is "not accepting + * promises". If when the timer expire there are still pending promises, + * shutdown will be forcefully unblocked. + */ +class ServiceWorkerShutdownBlocker final : public nsIAsyncShutdownBlocker, + public nsITimerCallback, + public nsINamed { + public: + using Progress = ServiceWorkerShutdownState::Progress; + static const uint32_t kInvalidShutdownStateId = 0; + + NS_DECL_ISUPPORTS + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + /** + * Returns the registered shutdown blocker if registration succeeded and + * nullptr otherwise. + */ + static already_AddRefed<ServiceWorkerShutdownBlocker> CreateAndRegisterOn( + nsIAsyncShutdownClient& aShutdownBarrier, + ServiceWorkerManager& aServiceWorkerManager); + + /** + * Blocks shutdown until `aPromise` settles. + * + * Can be called multiple times, and shutdown will be blocked until all the + * calls' promises settle, but all of these calls must happen before + * `StopAcceptingPromises()` is called (assertions will enforce this). + * + * See `CreateShutdownState` for aShutdownStateId, which is needed to clear + * the shutdown state if the shutdown process aborts for some reason. + */ + void WaitOnPromise(GenericNonExclusivePromise* aPromise, + uint32_t aShutdownStateId); + + /** + * Once this is called, shutdown will be blocked until all promises + * passed to `WaitOnPromise()` settle, and there must be no more calls to + * `WaitOnPromise()` (assertions will enforce this). + */ + void StopAcceptingPromises(); + + /** + * Start tracking the shutdown of an individual ServiceWorker for hang + * reporting purposes. Returns a "shutdown state ID" that should be used + * in subsequent calls to ReportShutdownProgress. The shutdown of an + * individual ServiceWorker is presumed to be completed when its `Progress` + * reaches `Progress::ShutdownCompleted`. + */ + uint32_t CreateShutdownState(); + + void ReportShutdownProgress(uint32_t aShutdownStateId, Progress aProgress); + + private: + explicit ServiceWorkerShutdownBlocker( + ServiceWorkerManager& aServiceWorkerManager); + + ~ServiceWorkerShutdownBlocker(); + + /** + * No-op if any of the following are true: + * 1) `BlockShutdown()` hasn't been called yet, or + * 2) `StopAcceptingPromises()` hasn't been called yet, or + * 3) `StopAcceptingPromises()` HAS been called, but there are still pending + * promises. + */ + void MaybeUnblockShutdown(); + + /** + * Requires `BlockShutdown()` to have been called. + */ + void UnblockShutdown(); + + /** + * Returns the remaining pending promise count (i.e. excluding the promise + * that just settled). + */ + uint32_t PromiseSettled(); + + bool IsAcceptingPromises() const; + + uint32_t GetPendingPromises() const; + + /** + * Initializes a timer that will unblock shutdown unconditionally once it's + * expired (even if there are still pending promises). No-op if: + * 1) not a beta or release build, or + * 2) shutdown is not being blocked or `StopAcceptingPromises()` has not been + * called. + */ + void MaybeInitUnblockShutdownTimer(); + + struct AcceptingPromises { + uint32_t mPendingPromises = 0; + }; + + struct NotAcceptingPromises { + explicit NotAcceptingPromises(AcceptingPromises aPreviousState); + + uint32_t mPendingPromises = 0; + }; + + Variant<AcceptingPromises, NotAcceptingPromises> mState; + + nsCOMPtr<nsIAsyncShutdownClient> mShutdownClient; + + HashMap<uint32_t, ServiceWorkerShutdownState> mShutdownStates; + + nsCOMPtr<nsITimer> mTimer; + LazyInitializedOnceEarlyDestructible< + const NotNull<RefPtr<ServiceWorkerManager>>> + mServiceWorkerManager; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkershutdownblocker_h__ diff --git a/dom/serviceworkers/ServiceWorkerShutdownState.cpp b/dom/serviceworkers/ServiceWorkerShutdownState.cpp new file mode 100644 index 0000000000..40f2e09f3f --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerShutdownState.cpp @@ -0,0 +1,165 @@ +/* -*- 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 "ServiceWorkerShutdownState.h" + +#include <array> +#include <type_traits> + +#include "MainThreadUtils.h" +#include "ServiceWorkerUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/RemoteWorkerService.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsDebug.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +namespace mozilla::dom { + +using Progress = ServiceWorkerShutdownState::Progress; + +namespace { + +constexpr inline auto UnderlyingProgressValue(Progress aProgress) { + return std::underlying_type_t<Progress>(aProgress); +} + +constexpr std::array<const char*, UnderlyingProgressValue(Progress::EndGuard_)> + gProgressStrings = {{ + // clang-format off + "parent process main thread", + "parent process IPDL background thread", + "content process worker launcher thread", + "content process main thread", + "shutdown completed" + // clang-format on + }}; + +} // anonymous namespace + +ServiceWorkerShutdownState::ServiceWorkerShutdownState() + : mProgress(Progress::ParentProcessMainThread) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); +} + +ServiceWorkerShutdownState::~ServiceWorkerShutdownState() { + Unused << NS_WARN_IF(mProgress != Progress::ShutdownCompleted); +} + +const char* ServiceWorkerShutdownState::GetProgressString() const { + return gProgressStrings[UnderlyingProgressValue(mProgress)]; +} + +void ServiceWorkerShutdownState::SetProgress(Progress aProgress) { + MOZ_ASSERT(aProgress != Progress::EndGuard_); + MOZ_RELEASE_ASSERT(UnderlyingProgressValue(mProgress) + 1 == + UnderlyingProgressValue(aProgress)); + + mProgress = aProgress; +} + +namespace { + +void ReportProgressToServiceWorkerManager(uint32_t aShutdownStateId, + Progress aProgress) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + MOZ_RELEASE_ASSERT(swm, "ServiceWorkers should shutdown before SWM."); + + swm->ReportServiceWorkerShutdownProgress(aShutdownStateId, aProgress); +} + +void ReportProgressToParentProcess(uint32_t aShutdownStateId, + Progress aProgress) { + MOZ_ASSERT(XRE_IsContentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + ContentChild* contentChild = ContentChild::GetSingleton(); + MOZ_ASSERT(contentChild); + + contentChild->SendReportServiceWorkerShutdownProgress(aShutdownStateId, + aProgress); +} + +void ReportServiceWorkerShutdownProgress(uint32_t aShutdownStateId, + Progress aProgress) { + MOZ_ASSERT(UnderlyingProgressValue(Progress::ParentProcessMainThread) < + UnderlyingProgressValue(aProgress)); + MOZ_ASSERT(UnderlyingProgressValue(aProgress) < + UnderlyingProgressValue(Progress::EndGuard_)); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [shutdownStateId = aShutdownStateId, progress = aProgress] { + if (XRE_IsParentProcess()) { + ReportProgressToServiceWorkerManager(shutdownStateId, progress); + } else { + ReportProgressToParentProcess(shutdownStateId, progress); + } + }); + + if (NS_IsMainThread()) { + MOZ_ALWAYS_SUCCEEDS(r->Run()); + } else { + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + } +} + +void ReportServiceWorkerShutdownProgress(uint32_t aShutdownStateId) { + Progress progress = Progress::EndGuard_; + + if (XRE_IsParentProcess()) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + progress = Progress::ParentProcessIpdlBackgroundThread; + } else { + if (NS_IsMainThread()) { + progress = Progress::ContentProcessMainThread; + } else { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); + progress = Progress::ContentProcessWorkerLauncherThread; + } + } + + ReportServiceWorkerShutdownProgress(aShutdownStateId, progress); +} + +} // anonymous namespace + +void MaybeReportServiceWorkerShutdownProgress(const ServiceWorkerOpArgs& aArgs, + bool aShutdownCompleted) { + if (XRE_IsParentProcess() && !XRE_IsE10sParentProcess()) { + return; + } + + if (aShutdownCompleted) { + MOZ_ASSERT(aArgs.type() == + ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs); + + ReportServiceWorkerShutdownProgress( + aArgs.get_ServiceWorkerTerminateWorkerOpArgs().shutdownStateId(), + Progress::ShutdownCompleted); + + return; + } + + if (aArgs.type() == + ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs) { + ReportServiceWorkerShutdownProgress( + aArgs.get_ServiceWorkerTerminateWorkerOpArgs().shutdownStateId()); + } +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerShutdownState.h b/dom/serviceworkers/ServiceWorkerShutdownState.h new file mode 100644 index 0000000000..eec55fab8d --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerShutdownState.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#ifndef DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_ +#define DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_ + +#include <cstdint> + +#include "ipc/EnumSerializer.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" + +namespace mozilla::dom { + +class ServiceWorkerShutdownState { + public: + // Represents the "location" of the shutdown message or completion of + // shutdown. + enum class Progress { + ParentProcessMainThread, + ParentProcessIpdlBackgroundThread, + ContentProcessWorkerLauncherThread, + ContentProcessMainThread, + ShutdownCompleted, + EndGuard_, + }; + + ServiceWorkerShutdownState(); + + ~ServiceWorkerShutdownState(); + + const char* GetProgressString() const; + + void SetProgress(Progress aProgress); + + private: + Progress mProgress; +}; + +// Asynchronously reports that shutdown has progressed to the calling thread +// if aArgs is for shutdown. If aShutdownCompleted is true, aArgs must be for +// shutdown. +void MaybeReportServiceWorkerShutdownProgress(const ServiceWorkerOpArgs& aArgs, + bool aShutdownCompleted = false); + +} // namespace mozilla::dom + +namespace IPC { + +using Progress = mozilla::dom::ServiceWorkerShutdownState::Progress; + +template <> +struct ParamTraits<Progress> + : public ContiguousEnumSerializer< + Progress, Progress::ParentProcessMainThread, Progress::EndGuard_> {}; + +} // namespace IPC + +#endif // DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_ diff --git a/dom/serviceworkers/ServiceWorkerUnregisterCallback.cpp b/dom/serviceworkers/ServiceWorkerUnregisterCallback.cpp new file mode 100644 index 0000000000..36650c9b55 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUnregisterCallback.cpp @@ -0,0 +1,35 @@ +/* -*- 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 "ServiceWorkerUnregisterCallback.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(UnregisterCallback, nsIServiceWorkerUnregisterCallback) + +UnregisterCallback::UnregisterCallback() + : mPromise(new GenericPromise::Private(__func__)) {} + +UnregisterCallback::UnregisterCallback(GenericPromise::Private* aPromise) + : mPromise(aPromise) { + MOZ_DIAGNOSTIC_ASSERT(mPromise); +} + +NS_IMETHODIMP +UnregisterCallback::UnregisterSucceeded(bool aState) { + mPromise->Resolve(aState, __func__); + return NS_OK; +} + +NS_IMETHODIMP +UnregisterCallback::UnregisterFailed() { + mPromise->Reject(NS_ERROR_DOM_SECURITY_ERR, __func__); + return NS_OK; +} + +RefPtr<GenericPromise> UnregisterCallback::Promise() const { return mPromise; } + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerUnregisterCallback.h b/dom/serviceworkers/ServiceWorkerUnregisterCallback.h new file mode 100644 index 0000000000..dd1a53b37d --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUnregisterCallback.h @@ -0,0 +1,41 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ServiceWorkerUnregisterCallback_h +#define mozilla_dom_ServiceWorkerUnregisterCallback_h + +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "nsIServiceWorkerManager.h" + +namespace mozilla::dom { + +class UnregisterCallback final : public nsIServiceWorkerUnregisterCallback { + public: + NS_DECL_ISUPPORTS + + UnregisterCallback(); + + explicit UnregisterCallback(GenericPromise::Private* aPromise); + + // nsIServiceWorkerUnregisterCallback implementation + NS_IMETHOD + UnregisterSucceeded(bool aState) override; + + NS_IMETHOD + UnregisterFailed() override; + + RefPtr<GenericPromise> Promise() const; + + private: + ~UnregisterCallback() = default; + + RefPtr<GenericPromise::Private> mPromise; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ServiceWorkerUnregisterCallback_h diff --git a/dom/serviceworkers/ServiceWorkerUnregisterJob.cpp b/dom/serviceworkers/ServiceWorkerUnregisterJob.cpp new file mode 100644 index 0000000000..b40521bf52 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUnregisterJob.cpp @@ -0,0 +1,139 @@ +/* -*- 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 "ServiceWorkerUnregisterJob.h" + +#include "mozilla/Unused.h" +#include "nsIPushService.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "ServiceWorkerManager.h" + +namespace mozilla::dom { + +class ServiceWorkerUnregisterJob::PushUnsubscribeCallback final + : public nsIUnsubscribeResultCallback { + public: + NS_DECL_ISUPPORTS + + explicit PushUnsubscribeCallback(ServiceWorkerUnregisterJob* aJob) + : mJob(aJob) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD + OnUnsubscribe(nsresult aStatus, bool) override { + // Warn if unsubscribing fails, but don't prevent the worker from + // unregistering. + Unused << NS_WARN_IF(NS_FAILED(aStatus)); + mJob->Unregister(); + return NS_OK; + } + + private: + ~PushUnsubscribeCallback() = default; + + RefPtr<ServiceWorkerUnregisterJob> mJob; +}; + +NS_IMPL_ISUPPORTS(ServiceWorkerUnregisterJob::PushUnsubscribeCallback, + nsIUnsubscribeResultCallback) + +ServiceWorkerUnregisterJob::ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal, + const nsACString& aScope, + bool aSendToParent) + : ServiceWorkerJob(Type::Unregister, aPrincipal, aScope, ""_ns), + mResult(false), + mSendToParent(aSendToParent) {} + +bool ServiceWorkerUnregisterJob::GetResult() const { + MOZ_ASSERT(NS_IsMainThread()); + return mResult; +} + +ServiceWorkerUnregisterJob::~ServiceWorkerUnregisterJob() = default; + +void ServiceWorkerUnregisterJob::AsyncExecute() { + MOZ_ASSERT(NS_IsMainThread()); + + if (Canceled()) { + Finish(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Push API, section 5: "When a service worker registration is unregistered, + // any associated push subscription must be deactivated." To ensure the + // service worker registration isn't cleared as we're unregistering, we + // unsubscribe first. + nsCOMPtr<nsIPushService> pushService = + do_GetService("@mozilla.org/push/Service;1"); + if (NS_WARN_IF(!pushService)) { + Unregister(); + return; + } + nsCOMPtr<nsIUnsubscribeResultCallback> unsubscribeCallback = + new PushUnsubscribeCallback(this); + nsresult rv = pushService->Unsubscribe(NS_ConvertUTF8toUTF16(mScope), + mPrincipal, unsubscribeCallback); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unregister(); + } +} + +void ServiceWorkerUnregisterJob::Unregister() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + Finish(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Step 1 of the Unregister algorithm requires checking that the + // client origin matches the scope's origin. We perform this in + // registration->update() method directly since we don't have that + // client information available here. + + // "Let registration be the result of running [[Get Registration]] + // algorithm passing scope as the argument." + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mPrincipal, mScope); + if (!registration) { + // "If registration is null, then, resolve promise with false." + Finish(NS_OK); + return; + } + + // Note, we send the message to remove the registration from disk now. This is + // necessary to ensure the registration is removed if the controlled + // clients are closed by shutting down the browser. + if (mSendToParent) { + swm->MaybeSendUnregister(mPrincipal, mScope); + } + + swm->EvictFromBFCache(registration); + + // "Remove scope to registration map[job's scope url]." + swm->RemoveRegistration(registration); + MOZ_ASSERT(registration->IsUnregistered()); + + // "Resolve promise with true" + mResult = true; + InvokeResultCallbacks(NS_OK); + + // "Invoke Try Clear Registration with registration" + if (!registration->IsControllingClients()) { + if (registration->IsIdle()) { + registration->Clear(); + } else { + registration->ClearWhenIdle(); + } + } + + Finish(NS_OK); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerUnregisterJob.h b/dom/serviceworkers/ServiceWorkerUnregisterJob.h new file mode 100644 index 0000000000..839bbc659e --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUnregisterJob.h @@ -0,0 +1,36 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerunregisterjob_h +#define mozilla_dom_serviceworkerunregisterjob_h + +#include "ServiceWorkerJob.h" + +namespace mozilla::dom { + +class ServiceWorkerUnregisterJob final : public ServiceWorkerJob { + public: + ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal, const nsACString& aScope, + bool aSendToParent); + + bool GetResult() const; + + private: + class PushUnsubscribeCallback; + + virtual ~ServiceWorkerUnregisterJob(); + + virtual void AsyncExecute() override; + + void Unregister(); + + bool mResult; + bool mSendToParent; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerunregisterjob_h diff --git a/dom/serviceworkers/ServiceWorkerUpdateJob.cpp b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp new file mode 100644 index 0000000000..a6726fbf47 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp @@ -0,0 +1,541 @@ +/* -*- 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 "ServiceWorkerUpdateJob.h" + +#include "mozilla/Telemetry.h" +#include "nsIScriptError.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerRegistrationInfo.h" +#include "ServiceWorkerScriptCache.h" +#include "mozilla/dom/WorkerCommon.h" + +namespace mozilla::dom { + +using serviceWorkerScriptCache::OnFailure; + +namespace { + +/** + * The spec mandates slightly different behaviors for computing the scope + * prefix string in case a Service-Worker-Allowed header is specified versus + * when it's not available. + * + * With the header: + * "Set maxScopeString to "/" concatenated with the strings in maxScope's + * path (including empty strings), separated from each other by "/"." + * Without the header: + * "Set maxScopeString to "/" concatenated with the strings, except the last + * string that denotes the script's file name, in registration's registering + * script url's path (including empty strings), separated from each other by + * "/"." + * + * In simpler terms, if the header is not present, we should only use the + * "directory" part of the pathname, and otherwise the entire pathname should be + * used. ScopeStringPrefixMode allows the caller to specify the desired + * behavior. + */ +enum ScopeStringPrefixMode { eUseDirectory, eUsePath }; + +nsresult GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix, + ScopeStringPrefixMode aPrefixMode) { + nsresult rv; + if (aPrefixMode == eUseDirectory) { + nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI)); + if (NS_WARN_IF(!scriptURL)) { + return NS_ERROR_FAILURE; + } + + rv = scriptURL->GetDirectory(aPrefix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (aPrefixMode == eUsePath) { + rv = aScriptURI->GetPathQueryRef(aPrefix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode"); + } + return NS_OK; +} + +} // anonymous namespace + +class ServiceWorkerUpdateJob::CompareCallback final + : public serviceWorkerScriptCache::CompareCallback { + RefPtr<ServiceWorkerUpdateJob> mJob; + + ~CompareCallback() = default; + + public: + explicit CompareCallback(ServiceWorkerUpdateJob* aJob) : mJob(aJob) { + MOZ_ASSERT(mJob); + } + + virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual, + OnFailure aOnFailure, + const nsAString& aNewCacheName, + const nsACString& aMaxScope, + nsLoadFlags aLoadFlags) override { + mJob->ComparisonResult(aStatus, aInCacheAndEqual, aOnFailure, aNewCacheName, + aMaxScope, aLoadFlags); + } + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override) +}; + +class ServiceWorkerUpdateJob::ContinueUpdateRunnable final + : public LifeCycleEventCallback { + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; + bool mSuccess; + + public: + explicit ContinueUpdateRunnable( + const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) + : mJob(aJob), mSuccess(false) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void SetResult(bool aResult) override { mSuccess = aResult; } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + mJob->ContinueUpdateAfterScriptEval(mSuccess); + mJob = nullptr; + return NS_OK; + } +}; + +class ServiceWorkerUpdateJob::ContinueInstallRunnable final + : public LifeCycleEventCallback { + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; + bool mSuccess; + + public: + explicit ContinueInstallRunnable( + const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) + : mJob(aJob), mSuccess(false) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void SetResult(bool aResult) override { mSuccess = aResult; } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + mJob->ContinueAfterInstallEvent(mSuccess); + mJob = nullptr; + return NS_OK; + } +}; + +ServiceWorkerUpdateJob::ServiceWorkerUpdateJob( + nsIPrincipal* aPrincipal, const nsACString& aScope, nsCString aScriptSpec, + ServiceWorkerUpdateViaCache aUpdateViaCache) + : ServiceWorkerUpdateJob(Type::Update, aPrincipal, aScope, + std::move(aScriptSpec), aUpdateViaCache) {} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerUpdateJob::GetRegistration() const { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration; + return ref.forget(); +} + +ServiceWorkerUpdateJob::ServiceWorkerUpdateJob( + Type aType, nsIPrincipal* aPrincipal, const nsACString& aScope, + nsCString aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache) + : ServiceWorkerJob(aType, aPrincipal, aScope, std::move(aScriptSpec)), + mUpdateViaCache(aUpdateViaCache), + mOnFailure(serviceWorkerScriptCache::OnFailure::DoNothing) {} + +ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() = default; + +void ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRv.Failed()); + + // Cleanup after a failed installation. This essentially implements + // step 13 of the Install algorithm. + // + // https://w3c.github.io/ServiceWorker/#installation-algorithm + // + // The spec currently only runs this after an install event fails, + // but we must handle many more internal errors. So we check for + // cleanup on every non-successful exit. + if (mRegistration) { + // Some kinds of failures indicate there is something broken in the + // currently installed registration. In these cases we want to fully + // unregister. + if (mOnFailure == OnFailure::Uninstall) { + mRegistration->ClearAsCorrupt(); + } + + // Otherwise just clear the workers we may have created as part of the + // update process. + else { + mRegistration->ClearEvaluating(); + mRegistration->ClearInstalling(); + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->MaybeRemoveRegistration(mRegistration); + + // Also clear the registration on disk if we are forcing uninstall + // due to a particularly bad failure. + if (mOnFailure == OnFailure::Uninstall) { + swm->MaybeSendUnregister(mRegistration->Principal(), + mRegistration->Scope()); + } + } + } + + mRegistration = nullptr; + + Finish(aRv); +} + +void ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv) { + ErrorResult rv(aRv); + FailUpdateJob(rv); +} + +void ServiceWorkerUpdateJob::AsyncExecute() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(GetType() == Type::Update); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Invoke Update algorithm: + // https://w3c.github.io/ServiceWorker/#update-algorithm + // + // "Let registration be the result of running the Get Registration algorithm + // passing job’s scope url as the argument." + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mPrincipal, mScope); + + if (!registration) { + ErrorResult rv; + rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "uninstalled"); + FailUpdateJob(rv); + return; + } + + // "Let newestWorker be the result of running Get Newest Worker algorithm + // passing registration as the argument." + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + + // "If job’s job type is update, and newestWorker is not null and its script + // url does not equal job’s script url, then: + // 1. Invoke Reject Job Promise with job and TypeError. + // 2. Invoke Finish Job with job and abort these steps." + if (newest && !newest->ScriptSpec().Equals(mScriptSpec)) { + ErrorResult rv; + rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "changed"); + FailUpdateJob(rv); + return; + } + + SetRegistration(registration); + Update(); +} + +void ServiceWorkerUpdateJob::SetRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(!mRegistration); + MOZ_ASSERT(aRegistration); + mRegistration = aRegistration; +} + +void ServiceWorkerUpdateJob::Update() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!Canceled()); + + // SetRegistration() must be called before Update(). + MOZ_ASSERT(mRegistration); + MOZ_ASSERT(!mRegistration->GetInstalling()); + + // Begin the script download and comparison steps starting at step 5 + // of the Update algorithm. + + RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest(); + nsAutoString cacheName; + + // If the script has not changed, we need to perform a byte-for-byte + // comparison. + if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) { + cacheName = workerInfo->CacheName(); + } + + RefPtr<CompareCallback> callback = new CompareCallback(this); + + nsresult rv = serviceWorkerScriptCache::Compare( + mRegistration, mPrincipal, cacheName, NS_ConvertUTF8toUTF16(mScriptSpec), + callback); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(rv); + return; + } +} + +ServiceWorkerUpdateViaCache ServiceWorkerUpdateJob::GetUpdateViaCache() const { + return mUpdateViaCache; +} + +void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus, + bool aInCacheAndEqual, + OnFailure aOnFailure, + const nsAString& aNewCacheName, + const nsACString& aMaxScope, + nsLoadFlags aLoadFlags) { + MOZ_ASSERT(NS_IsMainThread()); + + mOnFailure = aOnFailure; + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (NS_WARN_IF(Canceled() || !swm)) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Handle failure of the download or comparison. This is part of Update + // step 5 as "If the algorithm asynchronously completes with null, then:". + if (NS_WARN_IF(NS_FAILED(aStatus))) { + FailUpdateJob(aStatus); + return; + } + + // The spec validates the response before performing the byte-for-byte check. + // Here we perform the comparison in another module and then validate the + // script URL and scope. Make sure to do this validation before accepting + // an byte-for-byte match since the service-worker-allowed header might have + // changed since the last time it was installed. + + // This is step 2 the "validate response" section of Update algorithm step 5. + // Step 1 is performed in the serviceWorkerScriptCache code. + + nsCOMPtr<nsIURI> scriptURI; + nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsCOMPtr<nsIURI> maxScopeURI; + if (!aMaxScope.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, nullptr, scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + nsAutoCString defaultAllowedPrefix; + rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix, + eUseDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsAutoCString maxPrefix(defaultAllowedPrefix); + if (maxScopeURI) { + rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + nsCOMPtr<nsIURI> scopeURI; + rv = NS_NewURI(getter_AddRefs(scopeURI), mRegistration->Scope(), nullptr, + scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_FAILURE); + return; + } + + nsAutoCString scopeString; + rv = scopeURI->GetPathQueryRef(scopeString); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_FAILURE); + return; + } + + if (!StringBeginsWith(scopeString, maxPrefix)) { + nsAutoString message; + NS_ConvertUTF8toUTF16 reportScope(mRegistration->Scope()); + NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix); + + rv = nsContentUtils::FormatLocalizedString( + message, nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerScopePathMismatch", reportScope, reportMaxPrefix); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string"); + swm->ReportToAllClients(mScope, message, u""_ns, u""_ns, 0, 0, + nsIScriptError::errorFlag); + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + // The response has been validated, so now we can consider if its a + // byte-for-byte match. This is step 6 of the Update algorithm. + if (aInCacheAndEqual) { + Finish(NS_OK); + return; + } + + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1); + + // Begin step 7 of the Update algorithm to evaluate the new script. + nsLoadFlags flags = aLoadFlags; + if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None) { + flags |= nsIRequest::VALIDATE_ALWAYS; + } + + RefPtr<ServiceWorkerInfo> sw = new ServiceWorkerInfo( + mRegistration->Principal(), mRegistration->Scope(), mRegistration->Id(), + mRegistration->Version(), mScriptSpec, aNewCacheName, flags); + + // If the registration is corrupt enough to force an uninstall if the + // upgrade fails, then we want to make sure the upgrade takes effect + // if it succeeds. Therefore force the skip-waiting flag on to replace + // the broken worker after install. + if (aOnFailure == OnFailure::Uninstall) { + sw->SetSkipWaitingFlag(); + } + + mRegistration->SetEvaluating(sw); + + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( + new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>( + "ServiceWorkerUpdateJob", this)); + RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle); + + ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate(); + MOZ_ASSERT(workerPrivate); + rv = workerPrivate->CheckScriptEvaluation(callback); + + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } +} + +void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval( + bool aScriptEvaluationResult) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Step 7.5 of the Update algorithm verifying that the script evaluated + // successfully. + + if (NS_WARN_IF(!aScriptEvaluationResult)) { + ErrorResult error; + error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(mScriptSpec, + mRegistration->Scope()); + FailUpdateJob(error); + return; + } + + Install(); +} + +void ServiceWorkerUpdateJob::Install() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(!Canceled()); + + MOZ_ASSERT(!mRegistration->GetInstalling()); + + // Begin step 2 of the Install algorithm. + // + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm + + mRegistration->TransitionEvaluatingToInstalling(); + + // Step 6 of the Install algorithm resolving the job promise. + InvokeResultCallbacks(NS_OK); + + // Queue a task to fire an event named updatefound at all the + // ServiceWorkerRegistration. + mRegistration->FireUpdateFound(); + + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( + new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>( + "ServiceWorkerUpdateJob", this)); + RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle); + + // Send the install event to the worker thread + ServiceWorkerPrivate* workerPrivate = + mRegistration->GetInstalling()->WorkerPrivate(); + nsresult rv = workerPrivate->SendLifeCycleEvent(u"install"_ns, callback); + if (NS_WARN_IF(NS_FAILED(rv))) { + ContinueAfterInstallEvent(false /* aSuccess */); + } +} + +void ServiceWorkerUpdateJob::ContinueAfterInstallEvent( + bool aInstallEventSuccess) { + if (Canceled()) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + // If we haven't been canceled we should have a registration. There appears + // to be a path where it gets cleared before we call into here. Assert + // to try to catch this condition, but don't crash in release. + MOZ_DIAGNOSTIC_ASSERT(mRegistration); + if (!mRegistration) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + // Continue executing the Install algorithm at step 12. + + // "If installFailed is true" + if (NS_WARN_IF(!aInstallEventSuccess)) { + // The installing worker is cleaned up by FailUpdateJob(). + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Abort the update Job if the installWorker is null (e.g. when an extension + // is shutting down and all its workers have been terminated). + if (!mRegistration->GetInstalling()) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + mRegistration->TransitionInstallingToWaiting(); + + Finish(NS_OK); + + // Step 20 calls for explicitly waiting for queued event tasks to fire. + // Instead, we simply queue a runnable to execute Activate. This ensures the + // events are flushed from the queue before proceeding. + + // Step 22 of the Install algorithm. Activate is executed after the + // completion of this job. The controlling client and skipWaiting checks are + // performed in TryToActivate(). + mRegistration->TryToActivateAsync(); +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerUpdateJob.h b/dom/serviceworkers/ServiceWorkerUpdateJob.h new file mode 100644 index 0000000000..536aa72bfc --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUpdateJob.h @@ -0,0 +1,97 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_serviceworkerupdatejob_h +#define mozilla_dom_serviceworkerupdatejob_h + +#include "ServiceWorkerJob.h" +#include "ServiceWorkerRegistration.h" + +#include "nsIRequest.h" + +namespace mozilla::dom { + +namespace serviceWorkerScriptCache { +enum class OnFailure : uint8_t; +} // namespace serviceWorkerScriptCache + +class ServiceWorkerManager; +class ServiceWorkerRegistrationInfo; + +// A job class that performs the Update and Install algorithms from the +// service worker spec. This class is designed to be inherited and customized +// as a different job type. This is necessary because the register job +// performs largely the same operations as the update job, but has a few +// different starting steps. +class ServiceWorkerUpdateJob : public ServiceWorkerJob { + public: + // Construct an update job to be used only for updates. + ServiceWorkerUpdateJob(nsIPrincipal* aPrincipal, const nsACString& aScope, + nsCString aScriptSpec, + ServiceWorkerUpdateViaCache aUpdateViaCache); + + already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration() const; + + protected: + // Construct an update job that is overriden as another job type. + ServiceWorkerUpdateJob(Type aType, nsIPrincipal* aPrincipal, + const nsACString& aScope, nsCString aScriptSpec, + ServiceWorkerUpdateViaCache aUpdateViaCache); + + virtual ~ServiceWorkerUpdateJob(); + + // FailUpdateJob() must be called if an update job needs Finish() with + // an error. + void FailUpdateJob(ErrorResult& aRv); + + void FailUpdateJob(nsresult aRv); + + // The entry point when the update job is being used directly. Job + // types overriding this class should override this method to + // customize behavior. + virtual void AsyncExecute() override; + + // Set the registration to be operated on by Update() or to be immediately + // returned as a result of the job. This must be called before Update(). + void SetRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + // Execute the bulk of the update job logic using the registration defined + // by a previous SetRegistration() call. This can be called by the overriden + // AsyncExecute() to complete the job. The Update() method will always call + // Finish(). This method corresponds to the spec Update algorithm. + void Update(); + + ServiceWorkerUpdateViaCache GetUpdateViaCache() const; + + private: + class CompareCallback; + class ContinueUpdateRunnable; + class ContinueInstallRunnable; + + // Utility method called after a script is loaded and compared to + // our current cached script. + void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual, + serviceWorkerScriptCache::OnFailure aOnFailure, + const nsAString& aNewCacheName, + const nsACString& aMaxScope, nsLoadFlags aLoadFlags); + + // Utility method called after evaluating the worker script. + void ContinueUpdateAfterScriptEval(bool aScriptEvaluationResult); + + // Utility method corresponding to the spec Install algorithm. + void Install(); + + // Utility method called after the install event is handled. + void ContinueAfterInstallEvent(bool aInstallEventSuccess); + + RefPtr<ServiceWorkerRegistrationInfo> mRegistration; + ServiceWorkerUpdateViaCache mUpdateViaCache; + serviceWorkerScriptCache::OnFailure mOnFailure; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_serviceworkerupdatejob_h diff --git a/dom/serviceworkers/ServiceWorkerUtils.cpp b/dom/serviceworkers/ServiceWorkerUtils.cpp new file mode 100644 index 0000000000..413b933be3 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUtils.cpp @@ -0,0 +1,146 @@ +/* -*- 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 "ServiceWorkerUtils.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/ServiceWorkerRegistrarTypes.h" +#include "nsCOMPtr.h" +#include "nsIPrincipal.h" +#include "nsIURL.h" +#include "nsPrintfCString.h" + +namespace mozilla::dom { + +bool ServiceWorkerRegistrationDataIsValid( + const ServiceWorkerRegistrationData& aData) { + return !aData.scope().IsEmpty() && !aData.currentWorkerURL().IsEmpty() && + !aData.cacheName().IsEmpty(); +} + +namespace { + +void CheckForSlashEscapedCharsInPath(nsIURI* aURI, const char* aURLDescription, + ErrorResult& aRv) { + MOZ_ASSERT(aURI); + + // A URL that can't be downcast to a standard URL is an invalid URL and should + // be treated as such and fail with SecurityError. + nsCOMPtr<nsIURL> url(do_QueryInterface(aURI)); + if (NS_WARN_IF(!url)) { + // This really should not happen, since the caller checks that we + // have an http: or https: URL! + aRv.ThrowInvalidStateError("http: or https: URL without a concept of path"); + return; + } + + nsAutoCString path; + nsresult rv = url->GetFilePath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Again, should not happen. + aRv.ThrowInvalidStateError("http: or https: URL without a concept of path"); + return; + } + + ToLowerCase(path); + if (path.Find("%2f") != kNotFound || path.Find("%5c") != kNotFound) { + nsPrintfCString err("%s contains %%2f or %%5c", aURLDescription); + aRv.ThrowTypeError(err); + } +} + +} // anonymous namespace + +void ServiceWorkerScopeAndScriptAreValid(const ClientInfo& aClientInfo, + nsIURI* aScopeURI, nsIURI* aScriptURI, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(aScopeURI); + MOZ_DIAGNOSTIC_ASSERT(aScriptURI); + + auto principalOrErr = aClientInfo.GetPrincipal(); + if (NS_WARN_IF(principalOrErr.isErr())) { + aRv.ThrowInvalidStateError("Can't make security decisions about Client"); + return; + } + + auto hasHTTPScheme = [](nsIURI* aURI) -> bool { + return aURI->SchemeIs("http") || aURI->SchemeIs("https"); + }; + auto hasMozExtScheme = [](nsIURI* aURI) -> bool { + return aURI->SchemeIs("moz-extension"); + }; + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + auto isExtension = !!BasePrincipal::Cast(principal)->AddonPolicy(); + auto hasValidURISchemes = !isExtension ? hasHTTPScheme : hasMozExtScheme; + + // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 3. + if (!hasValidURISchemes(aScriptURI)) { + auto message = !isExtension + ? "Script URL's scheme is not 'http' or 'https'"_ns + : "Script URL's scheme is not 'moz-extension'"_ns; + aRv.ThrowTypeError(message); + return; + } + + // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 4. + CheckForSlashEscapedCharsInPath(aScriptURI, "script URL", aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 8. + if (!hasValidURISchemes(aScopeURI)) { + auto message = !isExtension + ? "Scope URL's scheme is not 'http' or 'https'"_ns + : "Scope URL's scheme is not 'moz-extension'"_ns; + aRv.ThrowTypeError(message); + return; + } + + // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 9. + CheckForSlashEscapedCharsInPath(aScopeURI, "scope URL", aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // The refs should really be empty coming in here, but if someone + // injects bad data into IPC, who knows. So let's revalidate that. + nsAutoCString ref; + Unused << aScopeURI->GetRef(ref); + if (NS_WARN_IF(!ref.IsEmpty())) { + aRv.ThrowSecurityError("Non-empty fragment on scope URL"); + return; + } + + Unused << aScriptURI->GetRef(ref); + if (NS_WARN_IF(!ref.IsEmpty())) { + aRv.ThrowSecurityError("Non-empty fragment on script URL"); + return; + } + + // Unfortunately we don't seem to have an obvious window id here; in + // particular ClientInfo does not have one. + nsresult rv = principal->CheckMayLoadWithReporting( + aScopeURI, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowSecurityError("Scope URL is not same-origin with Client"); + return; + } + + rv = principal->CheckMayLoadWithReporting( + aScriptURI, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowSecurityError("Script URL is not same-origin with Client"); + return; + } +} + +} // namespace mozilla::dom diff --git a/dom/serviceworkers/ServiceWorkerUtils.h b/dom/serviceworkers/ServiceWorkerUtils.h new file mode 100644 index 0000000000..ff4b9cf3d9 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUtils.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ +#ifndef _mozilla_dom_ServiceWorkerUtils_h +#define _mozilla_dom_ServiceWorkerUtils_h + +#include "mozilla/MozPromise.h" +#include "mozilla/dom/IPCNavigationPreloadState.h" +#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h" +#include "nsTArray.h" + +class nsIURI; + +namespace mozilla { + +class CopyableErrorResult; +class ErrorResult; + +namespace dom { + +class ClientInfo; +class ServiceWorkerRegistrationData; +class ServiceWorkerRegistrationDescriptor; +struct NavigationPreloadState; + +using ServiceWorkerRegistrationPromise = + MozPromise<ServiceWorkerRegistrationDescriptor, CopyableErrorResult, false>; + +using ServiceWorkerRegistrationListPromise = + MozPromise<CopyableTArray<ServiceWorkerRegistrationDescriptor>, + CopyableErrorResult, false>; + +using NavigationPreloadStatePromise = + MozPromise<IPCNavigationPreloadState, CopyableErrorResult, false>; + +using ServiceWorkerRegistrationCallback = + std::function<void(const ServiceWorkerRegistrationDescriptor&)>; + +using ServiceWorkerRegistrationListCallback = + std::function<void(const nsTArray<ServiceWorkerRegistrationDescriptor>&)>; + +using ServiceWorkerBoolCallback = std::function<void(bool)>; + +using ServiceWorkerFailureCallback = std::function<void(ErrorResult&&)>; + +using NavigationPreloadGetStateCallback = + std::function<void(NavigationPreloadState&&)>; + +bool ServiceWorkerRegistrationDataIsValid( + const ServiceWorkerRegistrationData& aData); + +void ServiceWorkerScopeAndScriptAreValid(const ClientInfo& aClientInfo, + nsIURI* aScopeURI, nsIURI* aScriptURI, + ErrorResult& aRv); + +} // namespace dom +} // namespace mozilla + +#endif // _mozilla_dom_ServiceWorkerUtils_h diff --git a/dom/serviceworkers/docs/telemetry.md b/dom/serviceworkers/docs/telemetry.md new file mode 100644 index 0000000000..864b6bec25 --- /dev/null +++ b/dom/serviceworkers/docs/telemetry.md @@ -0,0 +1,42 @@ +1. ServiceWorkerRegistrar loading. The ability to determine whether to intercept + is based on this. (Although if not loaded, it's possible to just not intercept.) + +2. Process launching. ServiceWorkers need to be launched into a process, and + under fission this will almost certainly be a new process, and at startup we + might not be able to depend on preallocated processes. + +3. Permission transmission. + +4. Worker launching. The act of spawning the worker thread in the content process. + +5. Script loading. + Cache API opening for the given origin. + QuotaManager storage and temporary storage initialization, which has to + happen before the Cache API can start accessing its files. + +6. Fetch request serialization / deserialization to parent process. + +7. InterceptedHttpChannel creation for the fetch request. + +8. Creating FetchEvent related objects and propagting to the content process + worker thread. + +9. Handle FetchEvent by the ServiceWorker's script. + +10. Propagating the response from ServiceWorker to parent process. + +11. Synthesizing the response for the intercepted channel. + +12. Reset the interception by redirecting to a normal http channel or cancel the + interception. + +13. Push data into the intercepted channel. + +Telemetry probes cover: + +1: SERVICE_WORKER_REGISTRATION_LOADING +2-4: SERVICE_WORKER_LAUNCH_TIME_2 +2-5, 7-13: SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS_2 +7-9: SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS_2 +11: SERVICE_WORKER_FETCH_EVENT_FINISH_SYNTHESIZED_RESPONSE_MS_2 +12: SERVICE_WORKER_FETCH_EVENT_CHANNEL_RESET_MS_2 diff --git a/dom/serviceworkers/moz.build b/dom/serviceworkers/moz.build new file mode 100644 index 0000000000..c47bb8f74f --- /dev/null +++ b/dom/serviceworkers/moz.build @@ -0,0 +1,130 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Service Workers") + +# Public stuff. +EXPORTS.mozilla.dom += [ + "FetchEventOpChild.h", + "FetchEventOpParent.h", + "FetchEventOpProxyChild.h", + "FetchEventOpProxyParent.h", + "NavigationPreloadManager.h", + "ServiceWorker.h", + "ServiceWorkerActors.h", + "ServiceWorkerChild.h", + "ServiceWorkerCloneData.h", + "ServiceWorkerContainer.h", + "ServiceWorkerContainerChild.h", + "ServiceWorkerContainerParent.h", + "ServiceWorkerDescriptor.h", + "ServiceWorkerEvents.h", + "ServiceWorkerInfo.h", + "ServiceWorkerInterceptController.h", + "ServiceWorkerIPCUtils.h", + "ServiceWorkerManager.h", + "ServiceWorkerManagerChild.h", + "ServiceWorkerManagerParent.h", + "ServiceWorkerOp.h", + "ServiceWorkerOpPromise.h", + "ServiceWorkerParent.h", + "ServiceWorkerRegistrar.h", + "ServiceWorkerRegistration.h", + "ServiceWorkerRegistrationChild.h", + "ServiceWorkerRegistrationDescriptor.h", + "ServiceWorkerRegistrationInfo.h", + "ServiceWorkerRegistrationParent.h", + "ServiceWorkerShutdownState.h", + "ServiceWorkerUtils.h", +] + +UNIFIED_SOURCES += [ + "FetchEventOpChild.cpp", + "FetchEventOpParent.cpp", + "FetchEventOpProxyChild.cpp", + "FetchEventOpProxyParent.cpp", + "NavigationPreloadManager.cpp", + "ServiceWorker.cpp", + "ServiceWorkerActors.cpp", + "ServiceWorkerChild.cpp", + "ServiceWorkerCloneData.cpp", + "ServiceWorkerContainer.cpp", + "ServiceWorkerContainerChild.cpp", + "ServiceWorkerContainerParent.cpp", + "ServiceWorkerContainerProxy.cpp", + "ServiceWorkerDescriptor.cpp", + "ServiceWorkerEvents.cpp", + "ServiceWorkerInfo.cpp", + "ServiceWorkerInterceptController.cpp", + "ServiceWorkerJob.cpp", + "ServiceWorkerJobQueue.cpp", + "ServiceWorkerManager.cpp", + "ServiceWorkerManagerParent.cpp", + "ServiceWorkerOp.cpp", + "ServiceWorkerParent.cpp", + "ServiceWorkerPrivate.cpp", + "ServiceWorkerProxy.cpp", + "ServiceWorkerQuotaUtils.cpp", + "ServiceWorkerRegisterJob.cpp", + "ServiceWorkerRegistrar.cpp", + "ServiceWorkerRegistration.cpp", + "ServiceWorkerRegistrationChild.cpp", + "ServiceWorkerRegistrationDescriptor.cpp", + "ServiceWorkerRegistrationInfo.cpp", + "ServiceWorkerRegistrationParent.cpp", + "ServiceWorkerRegistrationProxy.cpp", + "ServiceWorkerScriptCache.cpp", + "ServiceWorkerShutdownBlocker.cpp", + "ServiceWorkerShutdownState.cpp", + "ServiceWorkerUnregisterCallback.cpp", + "ServiceWorkerUnregisterJob.cpp", + "ServiceWorkerUpdateJob.cpp", + "ServiceWorkerUtils.cpp", +] + +IPDL_SOURCES += [ + "IPCNavigationPreloadState.ipdlh", + "IPCServiceWorkerDescriptor.ipdlh", + "IPCServiceWorkerRegistrationDescriptor.ipdlh", + "PFetchEventOp.ipdl", + "PFetchEventOpProxy.ipdl", + "PServiceWorker.ipdl", + "PServiceWorkerContainer.ipdl", + "PServiceWorkerManager.ipdl", + "PServiceWorkerRegistration.ipdl", + "ServiceWorkerOpArgs.ipdlh", + "ServiceWorkerRegistrarTypes.ipdlh", +] + +LOCAL_INCLUDES += [ + # For HttpBaseChannel.h dependencies + "/netwerk/base", + # For HttpBaseChannel.h + "/netwerk/protocol/http", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +MOCHITEST_MANIFESTS += [ + "test/mochitest-dFPI.ini", + "test/mochitest.ini", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "test/chrome-dFPI.ini", + "test/chrome.ini", +] + +BROWSER_CHROME_MANIFESTS += [ + "test/browser-dFPI.ini", + "test/browser.ini", + "test/isolated/multi-e10s-update/browser.ini", +] + +TEST_DIRS += ["test/gtest"] diff --git a/dom/serviceworkers/test/ForceRefreshChild.sys.mjs b/dom/serviceworkers/test/ForceRefreshChild.sys.mjs new file mode 100644 index 0000000000..b2b965be9e --- /dev/null +++ b/dom/serviceworkers/test/ForceRefreshChild.sys.mjs @@ -0,0 +1,12 @@ +export class ForceRefreshChild extends JSWindowActorChild { + constructor() { + super(); + } + + handleEvent(evt) { + this.sendAsyncMessage("test:event", { + type: evt.type, + detail: evt.details, + }); + } +} diff --git a/dom/serviceworkers/test/ForceRefreshParent.sys.mjs b/dom/serviceworkers/test/ForceRefreshParent.sys.mjs new file mode 100644 index 0000000000..cb2d4809e9 --- /dev/null +++ b/dom/serviceworkers/test/ForceRefreshParent.sys.mjs @@ -0,0 +1,77 @@ +var maxCacheLoadCount = 3; +var cachedLoadCount = 0; +var baseLoadCount = 0; +var done = false; + +export class ForceRefreshParent extends JSWindowActorParent { + constructor() { + super(); + } + + receiveMessage(msg) { + // if done is called, ignore the msg. + if (done) { + return; + } + if (msg.data.type === "base-load") { + baseLoadCount += 1; + if (cachedLoadCount === maxCacheLoadCount) { + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 2, + "cached load should occur before second base load" + ); + done = true; + return ForceRefreshParent.done(); + } + if (baseLoadCount !== 1) { + ForceRefreshParent.SimpleTest.ok( + false, + "base load without cached load should only occur once" + ); + done = true; + return ForceRefreshParent.done(); + } + } else if (msg.data.type === "base-register") { + ForceRefreshParent.SimpleTest.ok( + !cachedLoadCount, + "cached load should not occur before base register" + ); + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 1, + "register should occur after first base load" + ); + } else if (msg.data.type === "base-sw-ready") { + ForceRefreshParent.SimpleTest.ok( + !cachedLoadCount, + "cached load should not occur before base ready" + ); + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 1, + "ready should occur after first base load" + ); + ForceRefreshParent.refresh(); + } else if (msg.data.type === "cached-load") { + ForceRefreshParent.SimpleTest.ok( + cachedLoadCount < maxCacheLoadCount, + "cached load should not occur too many times" + ); + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 1, + "cache load occur after first base load" + ); + cachedLoadCount += 1; + if (cachedLoadCount < maxCacheLoadCount) { + return ForceRefreshParent.refresh(); + } + ForceRefreshParent.forceRefresh(); + } else if (msg.data.type === "cached-failure") { + ForceRefreshParent.SimpleTest.ok(false, "failure: " + msg.data.detail); + done = true; + ForceRefreshParent.done(); + } + } +} diff --git a/dom/serviceworkers/test/abrupt_completion_worker.js b/dom/serviceworkers/test/abrupt_completion_worker.js new file mode 100644 index 0000000000..7afebc6d45 --- /dev/null +++ b/dom/serviceworkers/test/abrupt_completion_worker.js @@ -0,0 +1,18 @@ +function setMessageHandler(response) { + onmessage = e => { + e.source.postMessage(response); + }; +} + +setMessageHandler("handler-before-throw"); + +// importScripts will throw when the ServiceWorker is past the "intalling" state. +importScripts(`empty.js?${Date.now()}`); + +// When importScripts throws an uncaught exception, these calls should never be +// made and the message handler should remain responding "handler-before-throw". +setMessageHandler("handler-after-throw"); + +// There needs to be a fetch handler to avoid the no-fetch optimizaiton, +// which will skip starting up this worker. +onfetch = e => e.respondWith(new Response("handler-after-throw")); diff --git a/dom/serviceworkers/test/activate_event_error_worker.js b/dom/serviceworkers/test/activate_event_error_worker.js new file mode 100644 index 0000000000..9f09cc5225 --- /dev/null +++ b/dom/serviceworkers/test/activate_event_error_worker.js @@ -0,0 +1,4 @@ +// Worker that errors on receiving an activate event. +onactivate = function(e) { + undefined.doSomething; +}; diff --git a/dom/serviceworkers/test/async_waituntil_worker.js b/dom/serviceworkers/test/async_waituntil_worker.js new file mode 100644 index 0000000000..94219e4879 --- /dev/null +++ b/dom/serviceworkers/test/async_waituntil_worker.js @@ -0,0 +1,53 @@ +var keepAlivePromise; +var resolvePromise; +var result = "Failed"; + +onactivate = function(event) { + event.waitUntil(clients.claim()); +}; + +onmessage = function(event) { + if (event.data === "Start") { + event.waitUntil(Promise.reject()); + + keepAlivePromise = new Promise(function(resolve, reject) { + resolvePromise = resolve; + }); + + result = "Success"; + event.waitUntil(keepAlivePromise); + event.source.postMessage("Started"); + } else if (event.data === "Result") { + event.source.postMessage(result); + if (resolvePromise !== undefined) { + resolvePromise(); + } + } +}; + +addEventListener("fetch", e => { + let respondWithPromise = new Promise(function(res, rej) { + setTimeout(() => { + res(new Response("ok")); + }, 0); + }); + e.respondWith(respondWithPromise); + // Test that waitUntil can be called in the promise handler of the existing + // lifetime extension promise. + respondWithPromise.then(() => { + e.waitUntil( + clients.matchAll().then(cls => { + dump(`matchAll returned ${cls.length} client(s) with URLs:\n`); + cls.forEach(cl => { + dump(`${cl.url}\n`); + }); + + if (cls.length != 1) { + dump("ERROR: no controlled clients.\n"); + } + client = cls[0]; + client.postMessage("Done"); + }) + ); + }); +}); diff --git a/dom/serviceworkers/test/blocking_install_event_worker.js b/dom/serviceworkers/test/blocking_install_event_worker.js new file mode 100644 index 0000000000..5cb9270b04 --- /dev/null +++ b/dom/serviceworkers/test/blocking_install_event_worker.js @@ -0,0 +1,22 @@ +function postMessageToTest(msg) { + return clients.matchAll({ includeUncontrolled: true }).then(list => { + for (var client of list) { + if (client.url.endsWith("test_install_event_gc.html")) { + client.postMessage(msg); + break; + } + } + }); +} + +addEventListener("install", evt => { + // This must be a simple promise to trigger the CC failure. + evt.waitUntil(new Promise(function() {})); + postMessageToTest({ type: "INSTALL_EVENT" }); +}); + +addEventListener("message", evt => { + if (evt.data.type === "ping") { + postMessageToTest({ type: "pong" }); + } +}); diff --git a/dom/serviceworkers/test/browser-common.ini b/dom/serviceworkers/test/browser-common.ini new file mode 100644 index 0000000000..e1c8cfbc48 --- /dev/null +++ b/dom/serviceworkers/test/browser-common.ini @@ -0,0 +1,52 @@ +[DEFAULT] +support-files = + browser_base_force_refresh.html + browser_cached_force_refresh.html + browser_head.js + download/window.html + download/worker.js + download_canceled/page_download_canceled.html + download_canceled/server-stream-download.sjs + download_canceled/sw_download_canceled.js + fetch.js + file_userContextId_openWindow.js + force_refresh_browser_worker.js + ForceRefreshChild.sys.mjs + ForceRefreshParent.sys.mjs + empty.html + empty_with_utils.html + empty.js + intercepted_channel_process_swap_worker.js + navigationPreload_page.html + network_with_utils.html + page_post_controlled.html + redirect.sjs + simple_fetch_worker.js + storage_recovery_worker.sjs + sw_respondwith_serviceworker.js + sw_with_navigationPreload.js + utils.js + +[browser_antitracking.js] +[browser_antitracking_subiframes.js] +[browser_devtools_serviceworker_interception.js] +skip-if = serviceworker_e10s +[browser_force_refresh.js] +skip-if = verify # Bug 1603340 +[browser_download.js] +[browser_download_canceled.js] +skip-if = verify +[browser_intercepted_channel_process_swap.js] +skip-if = !fission +[browser_intercepted_worker_script.js] +[browser_navigation_fetch_fault_handling.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_navigationPreload_read_after_respondWith.js] +[browser_remote_type_process_swap.js] +[browser_storage_permission.js] +skip-if = true # Crashes: @ mozilla::dom::ServiceWorkerManagerService::PropagateUnregister(unsigned long, mozilla::ipc::PrincipalInfo const&, nsTSubstring<char16_t> const&), #Bug 1578337 +[browser_storage_recovery.js] +[browser_unregister_with_containers.js] +[browser_userContextId_openWindow.js] +skip-if = true # See bug 1769437. diff --git a/dom/serviceworkers/test/browser-dFPI.ini b/dom/serviceworkers/test/browser-dFPI.ini new file mode 100644 index 0000000000..c327062f16 --- /dev/null +++ b/dom/serviceworkers/test/browser-dFPI.ini @@ -0,0 +1,7 @@ +[DEFAULT] +# Enable dFPI(cookieBehavior 5) for service worker tests. +prefs = + network.cookie.cookieBehavior=5 +dupe-manifest = true + +[include:browser-common.ini] diff --git a/dom/serviceworkers/test/browser.ini b/dom/serviceworkers/test/browser.ini new file mode 100644 index 0000000000..2a7162e9ed --- /dev/null +++ b/dom/serviceworkers/test/browser.ini @@ -0,0 +1,4 @@ +[DEFAULT] +dupe-manifest = true + +[include:browser-common.ini] diff --git a/dom/serviceworkers/test/browser_antitracking.js b/dom/serviceworkers/test/browser_antitracking.js new file mode 100644 index 0000000000..cb72f1c7e8 --- /dev/null +++ b/dom/serviceworkers/test/browser_antitracking.js @@ -0,0 +1,104 @@ +const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT; +const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + +let { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); + +const TOP_DOMAIN = "http://mochi.test:8888/"; +const SW_DOMAIN = "https://tracking.example.org/"; + +const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + TOP_DOMAIN +); +const SW_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + SW_DOMAIN +); + +const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`; +const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`; +const SW_IFRAME_PAGE = `${SW_TEST_ROOT}page_post_controlled.html`; +// An empty script suffices for our SW needs; it's by definition no-fetch. +const SW_REL_SW_SCRIPT = "empty.js"; + +/** + * Set up a no-fetch-optimized ServiceWorker on a domain that will be covered by + * tracking protection (but is not yet). Once the SW is installed, activate TP + * and create a tab that embeds that tracking-site in an iframe. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ], + }); + + // Open the top-level page. + info("Opening a new tab: " + SW_REGISTER_PAGE); + let topTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE, + }); + + // ## Install SW + info("Installing SW"); + await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ sw: SW_REL_SW_SCRIPT }], + async function({ sw }) { + // Waive the xray to use the content utils.js script functions. + await content.wrappedJSObject.registerAndWaitForActive(sw); + } + ); + + // Enable Anti-tracking. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ], + }); + await UrlClassifierTestUtils.addTestTrackers(); + + // Open the top-level URL. + info("Loading a new top-level URL: " + TOP_EMPTY_PAGE); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + topTab.linkedBrowser + ); + BrowserTestUtils.loadURI(topTab.linkedBrowser, TOP_EMPTY_PAGE); + await browserLoadedPromise; + + // Create Iframe in the top-level page and verify its state. + let { controlled } = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + const payload = await content.wrappedJSObject.createIframeAndWaitForMessage( + url + ); + return payload; + } + ); + + ok(!controlled, "Should not be controlled!"); + + // ## Cleanup + info("Loading the SW unregister page: " + SW_REGISTER_PAGE); + browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() { + await content.wrappedJSObject.unregisterAll(); + }); + + // Close the testing tab. + BrowserTestUtils.removeTab(topTab); +}); diff --git a/dom/serviceworkers/test/browser_antitracking_subiframes.js b/dom/serviceworkers/test/browser_antitracking_subiframes.js new file mode 100644 index 0000000000..d2b7a97c96 --- /dev/null +++ b/dom/serviceworkers/test/browser_antitracking_subiframes.js @@ -0,0 +1,105 @@ +const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + +const TOP_DOMAIN = "http://mochi.test:8888/"; +const SW_DOMAIN = "https://example.org/"; + +const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + TOP_DOMAIN +); +const SW_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + SW_DOMAIN +); + +const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`; +const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`; +const SW_IFRAME_PAGE = `${SW_TEST_ROOT}page_post_controlled.html`; +const SW_REL_SW_SCRIPT = "empty.js"; + +/** + * Set up a ServiceWorker on a domain that will be used as 3rd party iframe. + * That 3rd party frame should be controlled by the ServiceWorker. + * After that, we open a second iframe into the first one. That should not be + * controlled. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ], + }); + + // Open the top-level page. + info("Opening a new tab: " + SW_REGISTER_PAGE); + let topTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE, + }); + + // Install SW + info("Registering a SW: " + SW_REL_SW_SCRIPT); + await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ sw: SW_REL_SW_SCRIPT }], + async function({ sw }) { + // Waive the xray to use the content utils.js script functions. + await content.wrappedJSObject.registerAndWaitForActive(sw); + // User interaction + content.document.userInteractionForTesting(); + } + ); + + info("Loading a new top-level URL: " + TOP_EMPTY_PAGE); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + topTab.linkedBrowser + ); + BrowserTestUtils.loadURI(topTab.linkedBrowser, TOP_EMPTY_PAGE); + await browserLoadedPromise; + + // Create Iframe in the top-level page and verify its state. + info("Creating iframe and checking if controlled"); + let { controlled } = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + content.document.userInteractionForTesting(); + const payload = await content.wrappedJSObject.createIframeAndWaitForMessage( + url + ); + return payload; + } + ); + + ok(controlled, "Should be controlled!"); + + // Create a nested Iframe. + info("Creating nested-iframe and checking if controlled"); + let { nested_controlled } = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + const payload = await content.wrappedJSObject.createNestedIframeAndWaitForMessage( + url + ); + return payload; + } + ); + + ok(!nested_controlled, "Should not be controlled!"); + + info("Loading the SW unregister page: " + SW_REGISTER_PAGE); + browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() { + await content.wrappedJSObject.unregisterAll(); + }); + + // Close the testing tab. + BrowserTestUtils.removeTab(topTab); +}); diff --git a/dom/serviceworkers/test/browser_base_force_refresh.html b/dom/serviceworkers/test/browser_base_force_refresh.html new file mode 100644 index 0000000000..1c3d02d42f --- /dev/null +++ b/dom/serviceworkers/test/browser_base_force_refresh.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> +addEventListener('load', function(event) { + navigator.serviceWorker.register('force_refresh_browser_worker.js').then(function(swr) { + if (!swr) { + return; + } + window.dispatchEvent(new Event("base-register", { bubbles: true })); + }); + + navigator.serviceWorker.ready.then(function() { + window.dispatchEvent(new Event("base-sw-ready", { bubbles: true })); + }); + + window.dispatchEvent(new Event("base-load", { bubbles: true })); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/browser_cached_force_refresh.html b/dom/serviceworkers/test/browser_cached_force_refresh.html new file mode 100644 index 0000000000..faf2ee7a83 --- /dev/null +++ b/dom/serviceworkers/test/browser_cached_force_refresh.html @@ -0,0 +1,59 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> +function ok(exp, msg) { + if (!exp) { + throw(msg); + } +} + +function is(actual, expected, msg) { + if (actual !== expected) { + throw('got "' + actual + '", but expected "' + expected + '" - ' + msg); + } +} + +function fail(err) { + window.dispatchEvent(new Event("cached-failure", { bubbles: true, detail: err })); +} + +function getUncontrolledClients(sw) { + return new Promise(function(resolve, reject) { + navigator.serviceWorker.addEventListener('message', function onMsg(evt) { + if (evt.data.type === 'CLIENTS') { + navigator.serviceWorker.removeEventListener('message', onMsg); + resolve(evt.data.detail); + } + }); + sw.postMessage({ type: 'GET_UNCONTROLLED_CLIENTS' }) + }); +} + +addEventListener('load', function(event) { + if (!navigator.serviceWorker.controller) { + return fail(window.location.href + ' is not controlled!'); + } + + getUncontrolledClients(navigator.serviceWorker.controller) + .then(function(clientList) { + is(clientList.length, 1, 'should only have one client'); + is(clientList[0].url, window.location.href, + 'client url should match current window'); + is(clientList[0].frameType, 'top-level', + 'client should be a top-level window'); + window.dispatchEvent(new Event('cached-load', { bubbles: true })); + }) + .catch(function(err) { + fail(err); + }); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js b/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js new file mode 100644 index 0000000000..fc31159116 --- /dev/null +++ b/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js @@ -0,0 +1,264 @@ +"use strict"; + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const emptyDoc = BASE_URI + "empty.html"; +const fakeDoc = BASE_URI + "fake.html"; +const helloDoc = BASE_URI + "hello.html"; + +const CROSS_URI = "http://example.com/browser/dom/serviceworkers/test/"; +const crossRedirect = CROSS_URI + "redirect"; +const crossHelloDoc = CROSS_URI + "hello.html"; + +const sw = BASE_URI + "fetch.js"; + +async function checkObserver(aInput) { + let interceptedChannel = null; + + // We always get two channels which receive the "http-on-stop-request" + // notification if the service worker hijacks the request and respondWith an + // another fetch. One is for the "outer" window request when the other one is + // for the "inner" service worker request. Therefore, distinguish them by the + // order. + let waitForSecondOnStopRequest = aInput.intercepted; + + let promiseResolve; + + function observer(aSubject) { + let channel = aSubject.QueryInterface(Ci.nsIChannel); + // Since we cannot make sure that the network event triggered by the fetch() + // in this testcase is the very next event processed by ObserverService, we + // have to wait until we catch the one we want. + if (!channel.URI.spec.includes(aInput.expectedURL)) { + return; + } + + if (waitForSecondOnStopRequest) { + waitForSecondOnStopRequest = false; + return; + } + + // Wait for the service worker to intercept the request if it's expected to + // be intercepted + if (aInput.intercepted && interceptedChannel === null) { + return; + } else if (interceptedChannel) { + ok( + aInput.intercepted, + "Service worker intercepted the channel as expected" + ); + } else { + ok(!aInput.intercepted, "The channel doesn't be intercepted"); + } + + var tc = interceptedChannel + ? interceptedChannel.QueryInterface(Ci.nsITimedChannel) + : aSubject.QueryInterface(Ci.nsITimedChannel); + + // Check service worker related timings. + var serviceWorkerTimings = [ + { + start: tc.launchServiceWorkerStartTime, + end: tc.launchServiceWorkerEndTime, + }, + { + start: tc.dispatchFetchEventStartTime, + end: tc.dispatchFetchEventEndTime, + }, + { start: tc.handleFetchEventStartTime, end: tc.handleFetchEventEndTime }, + ]; + if (!aInput.swPresent) { + serviceWorkerTimings.forEach(aTimings => { + is(aTimings.start, 0, "SW timings should be 0."); + is(aTimings.end, 0, "SW timings should be 0."); + }); + } + + // Check network related timings. + var networkTimings = [ + tc.domainLookupStartTime, + tc.domainLookupEndTime, + tc.connectStartTime, + tc.connectEndTime, + tc.requestStartTime, + tc.responseStartTime, + tc.responseEndTime, + ]; + if (aInput.fetch) { + networkTimings.reduce((aPreviousTiming, aCurrentTiming) => { + ok(aPreviousTiming <= aCurrentTiming, "Checking network timings"); + return aCurrentTiming; + }); + } else { + networkTimings.forEach(aTiming => + is(aTiming, 0, "Network timings should be 0.") + ); + } + + interceptedChannel = null; + Services.obs.removeObserver(observer, topic); + promiseResolve(); + } + + function addInterceptedChannel(aSubject) { + let channel = aSubject.QueryInterface(Ci.nsIChannel); + if (!channel.URI.spec.includes(aInput.url)) { + return; + } + + // Hold the interceptedChannel until checking timing information. + // Note: It's a interceptedChannel in the type of httpChannel + interceptedChannel = channel; + Services.obs.removeObserver(addInterceptedChannel, topic_SW); + } + + const topic = "http-on-stop-request"; + const topic_SW = "service-worker-synthesized-response"; + + Services.obs.addObserver(observer, topic); + if (aInput.intercepted) { + Services.obs.addObserver(addInterceptedChannel, topic_SW); + } + + await new Promise(resolve => { + promiseResolve = resolve; + }); +} + +async function contentFetch(aURL) { + if (aURL.includes("redirect")) { + await content.window.fetch(aURL, { mode: "no-cors" }); + return; + } + await content.window.fetch(aURL); +} + +// The observer topics are fired in the parent process in parent-intercept +// and the content process in child-intercept. This function will handle running +// the check in the correct process. Note that it will block until the observers +// are notified. +async function fetchAndCheckObservers( + aFetchBrowser, + aObserverBrowser, + aTestCase +) { + let promise = null; + + promise = checkObserver(aTestCase); + + await SpecialPowers.spawn(aFetchBrowser, [aTestCase.url], contentFetch); + await promise; +} + +async function registerSWAndWaitForActive(aServiceWorker) { + let swr = await content.navigator.serviceWorker.register(aServiceWorker, { + scope: "empty.html", + }); + await new Promise(resolve => { + let worker = swr.installing || swr.waiting || swr.active; + if (worker.state === "activated") { + return resolve(); + } + + worker.addEventListener("statechange", () => { + if (worker.state === "activated") { + return resolve(); + } + }); + }); + + await new Promise(resolve => { + if (content.navigator.serviceWorker.controller) { + return resolve(); + } + + content.navigator.serviceWorker.addEventListener( + "controllerchange", + resolve, + { once: true } + ); + }); +} + +async function unregisterSW() { + let swr = await content.navigator.serviceWorker.getRegistration(); + swr.unregister(); +} + +add_task(async function test_serivce_worker_interception() { + info("Setting the prefs to having e10s enabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + // Make sure observer and testing function run in the same process + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + waitForExplicitFinish(); + + info("Open the tab"); + let tab = BrowserTestUtils.addTab(gBrowser, emptyDoc); + let tabBrowser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(tabBrowser); + + info("Open the tab for observing"); + let tab_observer = BrowserTestUtils.addTab(gBrowser, emptyDoc); + let tabBrowser_observer = gBrowser.getBrowserForTab(tab_observer); + await BrowserTestUtils.browserLoaded(tabBrowser_observer); + + let testcases = [ + { + url: helloDoc, + expectedURL: helloDoc, + swPresent: false, + intercepted: false, + fetch: true, + }, + { + url: fakeDoc, + expectedURL: helloDoc, + swPresent: true, + intercepted: true, + fetch: false, // should use HTTP cache + }, + { + // Bypass http cache + url: helloDoc + "?ForBypassingHttpCache=" + Date.now(), + expectedURL: helloDoc, + swPresent: true, + intercepted: false, + fetch: true, + }, + { + // no-cors mode redirect to no-cors mode (trigger internal redirect) + url: crossRedirect + "?url=" + crossHelloDoc + "&mode=no-cors", + expectedURL: crossHelloDoc, + swPresent: true, + redirect: "hello.html", + intercepted: true, + fetch: true, + }, + ]; + + info("Test 1: Verify simple fetch"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[0]); + + info("Register a service worker"); + await SpecialPowers.spawn(tabBrowser, [sw], registerSWAndWaitForActive); + + info("Test 2: Verify simple hijack"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[1]); + + info("Test 3: Verify fetch without using http cache"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[2]); + + info("Test 4: make a internal redirect"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[3]); + + info("Clean up"); + await SpecialPowers.spawn(tabBrowser, [undefined], unregisterSW); + + gBrowser.removeTab(tab); + gBrowser.removeTab(tab_observer); +}); diff --git a/dom/serviceworkers/test/browser_download.js b/dom/serviceworkers/test/browser_download.js new file mode 100644 index 0000000000..271f217012 --- /dev/null +++ b/dom/serviceworkers/test/browser_download.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" +); + +var gTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); + +function getFile(aFilename) { + if (aFilename.startsWith("file:")) { + var url = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL); + return url.file.clone(); + } + + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(aFilename); + return file; +} + +function windowObserver(win, topic) { + if (topic !== "domwindowopened") { + return; + } + + win.addEventListener( + "load", + function() { + if ( + win.document.documentURI === + "chrome://mozapps/content/downloads/unknownContentType.xhtml" + ) { + executeSoon(function() { + let dialog = win.document.getElementById("unknownContentType"); + let button = dialog.getButton("accept"); + button.disabled = false; + dialog.acceptDialog(); + }); + } + }, + { once: true } + ); +} + +function test() { + waitForExplicitFinish(); + + Services.ww.registerNotification(windowObserver); + + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }, + function() { + var url = gTestRoot + "download/window.html"; + var tab = BrowserTestUtils.addTab(gBrowser); + gBrowser.selectedTab = tab; + + Downloads.getList(Downloads.ALL) + .then(function(downloadList) { + var downloadListener; + + function downloadVerifier(aDownload) { + if (aDownload.succeeded) { + var file = getFile(aDownload.target.path); + ok(file.exists(), "download completed"); + is(file.fileSize, 33, "downloaded file has correct size"); + file.remove(false); + downloadList.remove(aDownload).catch(console.error); + downloadList.removeView(downloadListener).catch(console.error); + gBrowser.removeTab(tab); + Services.ww.unregisterNotification(windowObserver); + + executeSoon(finish); + } + } + + downloadListener = { + onDownloadAdded: downloadVerifier, + onDownloadChanged: downloadVerifier, + }; + + return downloadList.addView(downloadListener); + }) + .then(function() { + BrowserTestUtils.loadURI(gBrowser, url); + }); + } + ); +} diff --git a/dom/serviceworkers/test/browser_download_canceled.js b/dom/serviceworkers/test/browser_download_canceled.js new file mode 100644 index 0000000000..836a581acf --- /dev/null +++ b/dom/serviceworkers/test/browser_download_canceled.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test cancellation of a download in order to test edge-cases related to + * channel diversion. Channel diversion occurs in cases of file (and PSM cert) + * downloads where we realize in the child that we really want to consume the + * channel data in the parent. For data "sourced" by the parent, like network + * data, data streaming to the child is suspended and the parent waits for the + * child to send back the data it already received, then the channel is resumed. + * For data generated by the child, such as (the current, to be mooted by + * parent-intercept) child-side intercept, the data (currently) stream is + * continually pumped up to the parent. + * + * In particular, we want to reproduce the circumstances of Bug 1418795 where + * the child-side input-stream pump attempts to send data to the parent process + * but the parent has canceled the channel and so the IPC Actor has been torn + * down. Diversion begins once the nsURILoader receives the OnStartRequest + * notification with the headers, so there are two ways to produce + */ + +const { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" +); + +/** + * Clear the downloads list so other tests don't see our byproducts. + */ +async function clearDownloads() { + const downloads = await Downloads.getList(Downloads.ALL); + downloads.removeFinished(); +} + +/** + * Returns a Promise that will be resolved once the download dialog shows up and + * we have clicked the given button. + */ +function promiseClickDownloadDialogButton(buttonAction) { + const uri = "chrome://mozapps/content/downloads/unknownContentType.xhtml"; + return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, uri, { + async callback(win) { + // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to invoke + // its postShowCallback that results in a misleading error to the console + // if we close the dialog before it gets a chance to run. Just a + // setTimeout is not sufficient because it appears we get our "load" + // listener before the document's, so we use TestUtils.waitForTick() to + // defer until after its load handler runs, then use setTimeout(0) to end + // up after its eval. + await TestUtils.waitForTick(); + + await new Promise(resolve => setTimeout(resolve, 0)); + + const button = win.document + .getElementById("unknownContentType") + .getButton(buttonAction); + button.disabled = false; + info(`clicking ${buttonAction} button`); + button.click(); + }, + }); +} + +async function performCanceledDownload(tab, path) { + // If we're going to show a modal dialog for this download, then we should + // use it to cancel the download. If not, then we have to let the download + // start and then call into the downloads API ourselves to cancel it. + // We use this promise to signal the cancel being complete in either case. + let cancelledDownload; + + if ( + Services.prefs.getBoolPref( + "browser.download.always_ask_before_handling_new_types", + false + ) + ) { + // Start waiting for the download dialog before triggering the download. + cancelledDownload = promiseClickDownloadDialogButton("cancel"); + // Wait for the cancelation to have been triggered. + info("waiting for download popup"); + } else { + let downloadView; + cancelledDownload = new Promise(resolve => { + downloadView = { + onDownloadAdded(aDownload) { + aDownload.cancel(); + resolve(); + }, + }; + }); + const downloadList = await Downloads.getList(Downloads.ALL); + await downloadList.addView(downloadView); + } + + // Trigger the download. + info(`triggering download of "${path}"`); + /* eslint-disable no-shadow */ + await SpecialPowers.spawn(tab.linkedBrowser, [path], function(path) { + // Put a Promise in place that we can wait on for stream closure. + content.wrappedJSObject.trackStreamClosure(path); + // Create the link and trigger the download. + const link = content.document.createElement("a"); + link.href = path; + link.download = path; + content.document.body.appendChild(link); + link.click(); + }); + /* eslint-enable no-shadow */ + + // Wait for the download to cancel. + await cancelledDownload; + info("cancelled download"); + + // Wait for confirmation that the stream stopped. + info(`wait for the ${path} stream to close.`); + /* eslint-disable no-shadow */ + const why = await SpecialPowers.spawn(tab.linkedBrowser, [path], function( + path + ) { + return content.wrappedJSObject.streamClosed[path].promise; + }); + /* eslint-enable no-shadow */ + is(why.why, "canceled", "Ensure the stream canceled instead of timing out."); + // Note that for the "sw-stream-download" case, we end up with a bogus + // reason of "'close' may only be called on a stream in the 'readable' state." + // Since we aren't actually invoking close(), I'm assuming this is an + // implementation bug that will be corrected in the web platform tests. + info(`Cancellation reason: ${why.message} after ${why.ticks} ticks`); +} + +const gTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); + +const PAGE_URL = `${gTestRoot}download_canceled/page_download_canceled.html`; + +add_task(async function interruptedDownloads() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + // Open the tab + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: PAGE_URL, + }); + + // Wait for it to become controlled. Check that it was a promise that + // resolved as expected rather than undefined by checking the return value. + const controlled = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + function() { + // This is a promise set up by the page during load, and we are post-load. + return content.wrappedJSObject.controlled; + } + ); + is(controlled, "controlled", "page became controlled"); + + // Download a pass-through fetch stream. + await performCanceledDownload(tab, "sw-passthrough-download"); + + // Download a SW-generated stream + await performCanceledDownload(tab, "sw-stream-download"); + + // Cleanup + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + return content.wrappedJSObject.registration.unregister(); + }); + BrowserTestUtils.removeTab(tab); + await clearDownloads(); +}); diff --git a/dom/serviceworkers/test/browser_force_refresh.js b/dom/serviceworkers/test/browser_force_refresh.js new file mode 100644 index 0000000000..11c3c3f1f7 --- /dev/null +++ b/dom/serviceworkers/test/browser_force_refresh.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); + +async function refresh() { + EventUtils.synthesizeKey("R", { accelKey: true }); +} + +async function forceRefresh() { + EventUtils.synthesizeKey("R", { accelKey: true, shiftKey: true }); +} + +async function done() { + // unregister window actors + ChromeUtils.unregisterWindowActor("ForceRefresh"); + let tab = gBrowser.selectedTab; + let tabBrowser = gBrowser.getBrowserForTab(tab); + await ContentTask.spawn(tabBrowser, null, async function() { + const swr = await content.navigator.serviceWorker.getRegistration(); + await swr.unregister(); + }); + + BrowserTestUtils.removeTab(tab); + executeSoon(finish); +} + +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ["browser.cache.disk.enable", false], + ["browser.cache.memory.enable", false], + ], + }, + async function() { + // create ForceRefreseh window actor + const { ForceRefreshParent } = ChromeUtils.importESModule( + getRootDirectory(gTestPath) + "ForceRefreshParent.sys.mjs" + ); + + // setup helper functions for ForceRefreshParent + ForceRefreshParent.SimpleTest = SimpleTest; + ForceRefreshParent.refresh = refresh; + ForceRefreshParent.forceRefresh = forceRefresh; + ForceRefreshParent.done = done; + + // setup window actor options + let windowActorOptions = { + parent: { + esModuleURI: + getRootDirectory(gTestPath) + "ForceRefreshParent.sys.mjs", + }, + child: { + esModuleURI: + getRootDirectory(gTestPath) + "ForceRefreshChild.sys.mjs", + events: { + "base-register": { capture: true, wantUntrusted: true }, + "base-sw-ready": { capture: true, wantUntrusted: true }, + "base-load": { capture: true, wantUntrusted: true }, + "cached-load": { capture: true, wantUntrusted: true }, + "cached-failure": { capture: true, wantUntrusted: true }, + }, + }, + allFrames: true, + }; + + // register ForceRefresh window actors + ChromeUtils.registerWindowActor("ForceRefresh", windowActorOptions); + + // create a new tab and load test url + var url = gTestRoot + "browser_base_force_refresh.html"; + var tab = BrowserTestUtils.addTab(gBrowser); + var tabBrowser = gBrowser.getBrowserForTab(tab); + gBrowser.selectedTab = tab; + BrowserTestUtils.loadURI(gBrowser, url); + } + ); +} diff --git a/dom/serviceworkers/test/browser_head.js b/dom/serviceworkers/test/browser_head.js new file mode 100644 index 0000000000..e1d8049308 --- /dev/null +++ b/dom/serviceworkers/test/browser_head.js @@ -0,0 +1,319 @@ +/** + * This file contains common functionality for ServiceWorker browser tests. + * + * Note that the normal auto-import mechanics for browser mochitests only + * handles "head.js", but we currently store all of our different varieties of + * mochitest in a single directory, which potentially results in a collision + * for similar heuristics for xpcshell. + * + * Many of the storage-related helpers in this file come from: + * https://searchfox.org/mozilla-central/source/dom/localstorage/test/unit/head.js + **/ + +// To use this file, explicitly import it via (including the eslint comment): +// +// /* import-globals-from browser_head.js */ +// Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", this); + +// Find the current parent directory of the test context we're being loaded into +// such that one can do `${originNoTrailingSlash}/${DIR_PATH}/file_in_dir.foo`. +const DIR_PATH = getRootDirectory(gTestPath) + .replace("chrome://mochitests/content/", "") + .slice(0, -1); + +const SWM = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +// The expected minimum usage for an origin that has any Cache API storage in +// use. Currently, the DB uses a page size of 4k and a minimum growth size of +// 32k and has enough tables/indices for this to round up to 64k. +const kMinimumOriginUsageBytes = 65536; + +function getPrincipal(url, attrs) { + const uri = Services.io.newURI(url); + if (!attrs) { + attrs = {}; + } + return Services.scriptSecurityManager.createContentPrincipal(uri, attrs); +} + +async function _qm_requestFinished(request) { + await new Promise(function(resolve) { + request.callback = function() { + resolve(); + }; + }); + + if (request.resultCode !== Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; +} + +async function qm_reset_storage() { + return new Promise(resolve => { + let request = Services.qms.reset(); + request.callback = resolve; + }); +} + +async function get_qm_origin_usage(origin) { + return new Promise(resolve => { + const principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + Services.qms.getUsageForPrincipal(principal, request => + resolve(request.result.usage) + ); + }); +} + +/** + * Clear the group associated with the given origin via nsIClearDataService. We + * are using nsIClearDataService here because nsIQuotaManagerService doesn't + * (directly) provide a means of clearing a group. + */ +async function clear_qm_origin_group_via_clearData(origin) { + const uri = Services.io.newURI(origin); + const baseDomain = Services.eTLD.getBaseDomain(uri); + info(`Clearing storage on domain ${baseDomain} (from origin ${origin})`); + + // Initiate group clearing and wait for it. + await new Promise((resolve, reject) => { + Services.clearData.deleteDataFromBaseDomain( + baseDomain, + false, + Services.clearData.CLEAR_DOM_QUOTA, + failedFlags => { + if (failedFlags) { + reject(failedFlags); + } else { + resolve(); + } + } + ); + }); +} + +/** + * Look up the nsIServiceWorkerRegistrationInfo for a given SW descriptor. + */ +function swm_lookup_reg(swDesc) { + // Scopes always include the full origin. + const fullScope = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`; + const principal = getPrincipal(fullScope); + + const reg = SWM.getRegistrationByPrincipal(principal, fullScope); + + return reg; +} + +/** + * Install a ServiceWorker according to the provided descriptor by opening a + * fresh tab that will be closed when we are done. Returns the + * `nsIServiceWorkerRegistrationInfo` corresponding to the registration. + * + * The descriptor may have the following properties: + * - scope: Optional. + * - script: The script, which usually just wants to be a relative path. + * - origin: Requred, the origin (which should not include a trailing slash). + */ +async function install_sw(swDesc) { + info( + `Installing ServiceWorker ${swDesc.script} at ${swDesc.scope} on origin ${swDesc.origin}` + ); + const pageUrlStr = `${swDesc.origin}/${DIR_PATH}/empty_with_utils.html`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageUrlStr, + }, + async browser => { + await SpecialPowers.spawn( + browser, + [{ swScript: swDesc.script, swScope: swDesc.scope }], + async function({ swScript, swScope }) { + await content.wrappedJSObject.registerAndWaitForActive( + swScript, + swScope + ); + } + ); + } + ); + info(`ServiceWorker installed`); + + return swm_lookup_reg(swDesc); +} + +/** + * Consume storage in the given origin by storing randomly generated Blobs into + * Cache API storage and IndexedDB storage. We use both APIs in order to + * ensure that data clearing wipes both QM clients. + * + * Randomly generated Blobs means Blobs with literally random content. This is + * done to compensate for the Cache API using snappy for compression. + */ +async function consume_storage(origin, storageDesc) { + info(`Consuming storage on origin ${origin}`); + const pageUrlStr = `${origin}/${DIR_PATH}/empty_with_utils.html`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageUrlStr, + }, + async browser => { + await SpecialPowers.spawn(browser, [storageDesc], async function({ + cacheBytes, + idbBytes, + }) { + await content.wrappedJSObject.fillStorage(cacheBytes, idbBytes); + }); + } + ); +} + +// Check if the origin is effectively empty, but allowing for the minimum size +// Cache API database to be present. +function is_minimum_origin_usage(originUsageBytes) { + return originUsageBytes <= kMinimumOriginUsageBytes; +} + +/** + * Perform a navigation, waiting until the navigation stops, then returning + * the `textContent` of the body node. The expectation is this will be used + * with ServiceWorkers that return a body that indicates the ServiceWorker + * provided the result (possibly derived from the request) versus if + * interception didn't happen. + */ +async function navigate_and_get_body(swDesc, debugTag) { + let pageUrlStr = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`; + if (debugTag) { + pageUrlStr += "?" + debugTag; + } + info(`Navigating to ${pageUrlStr}`); + + const tabResult = await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageUrlStr, + // In the event of an aborted navigation, the load event will never + // happen... + waitForLoad: false, + // ...but the stop will. + waitForStateStop: true, + }, + async browser => { + info(` Tab opened, querying body content.`); + const spawnResult = await SpecialPowers.spawn(browser, [], function() { + const controlled = !!content.navigator.serviceWorker.controller; + // Special-case about: URL's. + let loc = content.document.documentURI; + if (loc.startsWith("about:")) { + // about:neterror is parameterized by query string, so truncate that + // off because our tests just care if we're seeing the neterror page. + const idxQuestion = loc.indexOf("?"); + if (idxQuestion !== -1) { + loc = loc.substring(0, idxQuestion); + } + return { controlled, body: loc }; + } + return { + controlled, + body: content.document?.body?.textContent?.trim(), + }; + }); + + return spawnResult; + } + ); + + return tabResult; +} + +function waitForIframeLoad(iframe) { + return new Promise(function(resolve) { + iframe.onload = resolve; + }); +} + +function waitForRegister(scope, callback) { + return new Promise(function(resolve) { + let listener = { + onRegister(registration) { + if (registration.scope !== scope) { + return; + } + SWM.removeListener(listener); + resolve(callback ? callback(registration) : registration); + }, + }; + SWM.addListener(listener); + }); +} + +function waitForUnregister(scope) { + return new Promise(function(resolve) { + let listener = { + onUnregister(registration) { + if (registration.scope !== scope) { + return; + } + SWM.removeListener(listener); + resolve(registration); + }, + }; + SWM.addListener(listener); + }); +} + +// Be careful using this helper function, please make sure QuotaUsageCheck must +// happen, otherwise test would be stucked in this function. +function waitForQuotaUsageCheckFinish(scope) { + return new Promise(function(resolve) { + let listener = { + onQuotaUsageCheckFinish(registration) { + if (registration.scope !== scope) { + return; + } + SWM.removeListener(listener); + resolve(registration); + }, + }; + SWM.addListener(listener); + }); +} + +function waitForServiceWorkerRegistrationChange(registration, callback) { + return new Promise(function(resolve) { + let listener = { + onChange() { + registration.removeListener(listener); + if (callback) { + callback(); + } + resolve(callback ? callback() : undefined); + }, + }; + registration.addListener(listener); + }); +} + +function waitForServiceWorkerShutdown() { + return new Promise(function(resolve) { + let observer = { + observe(subject, topic, data) { + if (topic !== "service-worker-shutdown") { + return; + } + SpecialPowers.removeObserver(observer, "service-worker-shutdown"); + resolve(); + }, + }; + SpecialPowers.addObserver(observer, "service-worker-shutdown"); + }); +} diff --git a/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js b/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js new file mode 100644 index 0000000000..f4722e91c0 --- /dev/null +++ b/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that navigation loads through intercepted channels result in the +// appropriate process swaps. This appears to only be possible when navigating +// to a cross-origin URL, where that navigation is controlled by a ServiceWorker. + +"use strict"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://example.org"; + +const SAME_ORIGIN_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + SAME_ORIGIN +); +const CROSS_ORIGIN_ROOT = SAME_ORIGIN_ROOT.replace(SAME_ORIGIN, CROSS_ORIGIN); + +const SW_REGISTER_URL = `${CROSS_ORIGIN_ROOT}empty_with_utils.html`; +const SW_SCRIPT_URL = `${CROSS_ORIGIN_ROOT}intercepted_channel_process_swap_worker.js`; +const URL_BEFORE_NAVIGATION = `${SAME_ORIGIN_ROOT}empty.html`; +const CROSS_ORIGIN_URL = `${CROSS_ORIGIN_ROOT}empty.html`; + +const TESTCASES = [ + { + url: CROSS_ORIGIN_URL, + description: + "Controlled cross-origin navigation with network-provided response", + }, + { + url: `${CROSS_ORIGIN_ROOT}this-path-does-not-exist?respondWith=${CROSS_ORIGIN_URL}`, + description: + "Controlled cross-origin navigation with ServiceWorker-provided response", + }, +]; + +async function navigateTab(aTab, aUrl) { + BrowserTestUtils.loadURI(aTab.linkedBrowser, aUrl); + + await BrowserTestUtils.waitForLocationChange(gBrowser, aUrl).then(() => + BrowserTestUtils.browserStopped(aTab.linkedBrowser) + ); +} + +async function runTestcase(aTab, aTestcase) { + info(`Testing ${aTestcase.description}`); + + await navigateTab(aTab, URL_BEFORE_NAVIGATION); + + const [initialPid] = E10SUtils.getBrowserPids(aTab.linkedBrowser); + + await navigateTab(aTab, aTestcase.url); + + const [finalPid] = E10SUtils.getBrowserPids(aTab.linkedBrowser); + + await SpecialPowers.spawn(aTab.linkedBrowser, [], () => { + Assert.ok( + content.navigator.serviceWorker.controller, + `${content.location} should be controlled.` + ); + }); + + Assert.notEqual( + initialPid, + finalPid, + `Navigating from ${URL_BEFORE_NAVIGATION} to ${aTab.linkedBrowser.currentURI.spec} should have resulted in a different PID.` + ); +} + +add_task(async function setupPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +add_task(async function setupBrowser() { + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_URL, + }); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [SW_SCRIPT_URL], + async scriptUrl => { + await content.wrappedJSObject.registerAndWaitForActive(scriptUrl); + } + ); +}); + +add_task(async function runTestcases() { + for (const testcase of TESTCASES) { + await runTestcase(gBrowser.selectedTab, testcase); + } +}); + +add_task(async function cleanup() { + const tab = gBrowser.selectedTab; + + await navigateTab(tab, SW_REGISTER_URL); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.unregisterAll(); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/browser_intercepted_worker_script.js b/dom/serviceworkers/test/browser_intercepted_worker_script.js new file mode 100644 index 0000000000..2e0542df07 --- /dev/null +++ b/dom/serviceworkers/test/browser_intercepted_worker_script.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test tests if the service worker is able to intercept the script loading + * channel of a dedicated worker. + * + * On success, the test will not crash. + */ + +const SAME_ORIGIN = "https://example.com"; + +const SAME_ORIGIN_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + SAME_ORIGIN +); + +const SW_REGISTER_URL = `${SAME_ORIGIN_ROOT}empty_with_utils.html`; +const SW_SCRIPT_URL = `${SAME_ORIGIN_ROOT}simple_fetch_worker.js`; +const SCRIPT_URL = `${SAME_ORIGIN_ROOT}empty.js`; + +async function navigateTab(aTab, aUrl) { + BrowserTestUtils.loadURI(aTab.linkedBrowser, aUrl); + + await BrowserTestUtils.waitForLocationChange(gBrowser, aUrl).then(() => + BrowserTestUtils.browserStopped(aTab.linkedBrowser) + ); +} + +async function runTest(aTestSharedWorker) { + const tab = gBrowser.selectedTab; + + await navigateTab(tab, SW_REGISTER_URL); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [SCRIPT_URL, aTestSharedWorker], + async (scriptUrl, testSharedWorker) => { + await new Promise(resolve => { + content.navigator.serviceWorker.onmessage = e => { + if (e.data == scriptUrl) { + resolve(); + } + }; + + if (testSharedWorker) { + let worker = new content.Worker(scriptUrl); + } else { + let worker = new content.SharedWorker(scriptUrl); + } + }); + } + ); + + ok(true, "The service worker has intercepted the script loading."); +} + +add_task(async function setupPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ], + }); +}); + +add_task(async function setupBrowser() { + // The tab will be used by subsequent test steps via 'gBrowser.selectedTab'. + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_URL, + }); + + registerCleanupFunction(async _ => { + await navigateTab(tab, SW_REGISTER_URL); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.unregisterAll(); + }); + + BrowserTestUtils.removeTab(tab); + }); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [SW_SCRIPT_URL], + async scriptUrl => { + await content.wrappedJSObject.registerAndWaitForActive(scriptUrl); + } + ); +}); + +add_task(async function runTests() { + await runTest(false); + await runTest(true); +}); diff --git a/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js b/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js new file mode 100644 index 0000000000..0e8a9eee20 --- /dev/null +++ b/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js @@ -0,0 +1,115 @@ +const TOP_DOMAIN = "http://mochi.test:8888/"; +const SW_DOMAIN = "https://example.org/"; + +const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + TOP_DOMAIN +); +const SW_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + SW_DOMAIN +); + +const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`; +const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`; +const SW_IFRAME_PAGE = `${SW_TEST_ROOT}navigationPreload_page.html`; +// An empty script suffices for our SW needs; it's by definition no-fetch. +const SW_REL_SW_SCRIPT = "sw_with_navigationPreload.js"; + +/** + * Test the FetchEvent.preloadResponse can be read after FetchEvent.respondWith() + * + * Step 1. register a ServiceWorker which only handles FetchEvent when request + * url includes navigationPreload_page.html. Otherwise, it alwasy + * fallbacks the fetch to the network. + * If the request url includes navigationPreload_page.html, it call + * FetchEvent.respondWith() with a new Resposne, and then call + * FetchEvent.waitUtil() to wait FetchEvent.preloadResponse and post the + * preloadResponse's text to clients. + * Step 2. Open a controlled page and register message event handler to receive + * the postMessage from ServiceWorker. + * Step 3. Create a iframe which url is navigationPreload_page.html, such that + * ServiceWorker can fake the response and then send preloadResponse's + * result. + * Step 4. Unregister the ServiceWorker and cleanup the environment. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + // Step 1. + info("Opening a new tab: " + SW_REGISTER_PAGE); + let topTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE, + }); + + // ## Install SW + await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ sw: SW_REL_SW_SCRIPT }], + async function({ sw }) { + // Waive the xray to use the content utils.js script functions. + dump(`register serviceworker...\n`); + await content.wrappedJSObject.registerAndWaitForActive(sw); + } + ); + + // Step 2. + info("Loading a controlled page: " + SW_REGISTER_PAGE); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + topTab.linkedBrowser + ); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + info("Create a target iframe: " + SW_IFRAME_PAGE); + let result = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + async function waitForNavigationPreload() { + return new Promise(resolve => { + content.wrappedJSObject.navigator.serviceWorker.addEventListener( + `message`, + event => { + resolve(event.data); + } + ); + }); + } + + let promise = waitForNavigationPreload(); + + // Step 3. + const iframe = content.wrappedJSObject.document.createElement("iframe"); + iframe.src = url; + content.wrappedJSObject.document.body.appendChild(iframe); + await new Promise(r => { + iframe.onload = r; + }); + + let result = await promise; + return result; + } + ); + + is(result, "NavigationPreload\n", "Should get NavigationPreload result"); + + // Step 4. + info("Loading the SW unregister page: " + SW_REGISTER_PAGE); + browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() { + await content.wrappedJSObject.unregisterAll(); + }); + + // Close the testing tab. + BrowserTestUtils.removeTab(topTab); +}); diff --git a/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js new file mode 100644 index 0000000000..6655c52a62 --- /dev/null +++ b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js @@ -0,0 +1,272 @@ +/** + * This test file tests our automatic recovery and any related mitigating + * heuristics that occur during intercepted navigation fetch request. + * Specifically, we should be resetting interception so that we go to the + * network in these cases and then potentially taking actions like unregistering + * the ServiceWorker and/or clearing QuotaManager-managed storage for the + * origin. + * + * See specific test permutations for specific details inline in the test. + * + * NOTE THAT CURRENTLY THIS TEST IS DISCUSSING MITIGATIONS THAT ARE NOT YET + * IMPLEMENTED, JUST PLANNED. These will be iterated on and added to the rest + * of the stack of patches on Bug 1503072. + * + * ## Test Mechanics + * + * ### Fetch Fault Injection + * + * We expose: + * - On nsIServiceWorkerInfo, the per-ServiceWorker XPCOM interface: + * - A mechanism for creating synthetic faults by setting the + * `nsIServiceWorkerInfo::testingInjectCancellation` attribute to a failing + * nsresult. The fault is applied at the beginning of the steps to dispatch + * the fetch event on the global. + * - A count of the number of times we experienced these navigation faults + * that had to be reset as `nsIServiceWorkerInfo::navigationFaultCount`. + * (This would also include real faults, but we only expect to see synthetic + * faults in this test.) + * - On nsIServiceWorkerRegistrationInfo, the per-registration XPCOM interface: + * - A readonly attribute that indicates how many times an origin storage + * usage check has been initiated. + * + * We also use: + * - `nsIServiceWorkerManager::addListener(nsIServiceWorkerManagerListener)` + * allows our test to listen for the unregistration of registrations. This + * allows us to be notified when unregistering or origin-clearing actions have + * been taken as a mitigation. + * + * ### General Test Approach + * + * For each test we: + * - Ensure/confirm the testing origin has no QuotaManager storage in use. + * - Install the ServiceWorker. + * - If we are testing the situation where we want to simulate the origin being + * near its quota limit, we also generate Cache API and IDB storage usage + * sufficient to put our origin over the threshold. + * - We run a quota check on the origin after doing this in order to make sure + * that we did this correctly and that we properly constrained the limit for + * the origin. We fail the test for test implementation reasons if we + * didn't accomplish this. + * - Verify a fetch navigation to the SW works without any fault injection, + * producing a result produced by the ServiceWorker. + * - Begin fault permutations in a loop, where for each pass of the loop: + * - We trigger a navigation which will result in an intercepted fetch + * which will fault. We wait until the navigation completes. + * - We verify that we got the request from the network. + * - We verify that the ServiceWorker's navigationFaultCount increased. + * - If this the count at which we expect a mitigation to take place, we wait + * for the registration to become unregistered AND: + * - We check whether the storage for the origin was cleared or not, which + * indicates which mitigation of the following happened: + * - Unregister the registration directly. + * - Clear the origin's data which will also unregister the registration + * as a side effect. + * - We check whether the registration indicates an origin quota check + * happened or not. + * + * ### Disk Usage Limits + * + * In order to avoid gratuitous disk I/O and related overheads, we limit QM + * ("temporary") storage to 10 MiB which ends up limiting group usage to 10 MiB. + * This lets us set a threshold situation where we claim that a SW needs at + * least 4 MiB of storage for installation/operation, meaning that any usage + * beyond 6 MiB in the group will constitute a need to clear the group or + * origin. We fill with the storage with 8 MiB of artificial usage to this end, + * storing 4 MiB in Cache API and 4 MiB in IDB. + **/ + +// Because of the amount of I/O involved in this test, pernosco reproductions +// may experience timeouts without a timeout multiplier. +requestLongerTimeout(2); + +/* import-globals-from browser_head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", + this +); + +// The origin we run the tests on. +const TEST_ORIGIN = "https://test1.example.org"; +// An origin in the same group that impacts the usage of the TEST_ORIGIN. Used +// to verify heuristics related to group-clearing (where clearing the +// TEST_ORIGIN itself would not be sufficient for us to mitigate quota limits +// being reached.) +const SAME_GROUP_ORIGIN = "https://test2.example.org"; + +const TEST_SW_SETUP = { + origin: TEST_ORIGIN, + // Page with a body textContent of "NETWORK" and has utils.js loaded. + scope: "network_with_utils.html", + // SW that serves a body with a textContent of "SERVICEWORKER" and + // has utils.js loaded. + script: "sw_respondwith_serviceworker.js", +}; + +const TEST_STORAGE_SETUP = { + cacheBytes: 4 * 1024 * 1024, // 4 MiB + idbBytes: 4 * 1024 * 1024, // 4 MiB +}; + +const FAULTS_BEFORE_MITIGATION = 3; + +/** + * Core test iteration logic. + * + * Parameters: + * - name: Human readable name of the fault we're injecting. + * - useError: The nsresult failure code to inject into fetch. + * - errorPage: The "about" page that we expect errors to leave us on. + * - consumeQuotaOrigin: If truthy, the origin to place the storage usage in. + * If falsey, we won't fill storage. + */ +async function do_fault_injection_test({ + name, + useError, + errorPage, + consumeQuotaOrigin, +}) { + info( + `### testing: error: ${name} (${useError}) consumeQuotaOrigin: ${consumeQuotaOrigin}` + ); + + // ## Ensure/confirm the testing origins have no QuotaManager storage in use. + await clear_qm_origin_group_via_clearData(TEST_ORIGIN); + + // ## Install the ServiceWorker + const reg = await install_sw(TEST_SW_SETUP); + const sw = reg.activeWorker; + + // ## Generate quota usage if appropriate + if (consumeQuotaOrigin) { + await consume_storage(consumeQuotaOrigin, TEST_STORAGE_SETUP); + } + + // ## Verify normal navigation is served by the SW. + info(`## Checking normal operation.`); + { + const debugTag = `err=${name}&fault=0`; + const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); + is( + docInfo.body, + "SERVICEWORKER", + "navigation without injected fault originates from ServiceWorker" + ); + + is( + docInfo.controlled, + true, + "successfully intercepted navigation should be controlled" + ); + } + + // Make sure the test is listening on the ServiceWorker unregistration, since + // we expect it happens after navigation fault threshold reached. + const unregisteredPromise = waitForUnregister(reg.scope); + + // Make sure the test is listening on the finish of quota checking, since we + // expect it happens after navigation fault threshold reached. + const quotaUsageCheckFinishPromise = waitForQuotaUsageCheckFinish(reg.scope); + + // ## Inject faults in a loop until expected mitigation. + sw.testingInjectCancellation = useError; + for (let iFault = 0; iFault < FAULTS_BEFORE_MITIGATION; iFault++) { + info(`## Testing with injected fault number ${iFault + 1}`); + // We should never have triggered an origin quota usage check before the + // final fault injection. + is(reg.quotaUsageCheckCount, 0, "No quota usage check yet"); + + // Make sure our loads encode the specific + const debugTag = `err=${name}&fault=${iFault + 1}`; + + const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); + // We should always be receiving network fallback. + is( + docInfo.body, + "NETWORK", + "navigation with injected fault originates from network" + ); + + is(docInfo.controlled, false, "bypassed pages shouldn't be controlled"); + + // The fault count should have increased + is( + sw.navigationFaultCount, + iFault + 1, + "navigation fault increased (to expected value)" + ); + } + + await unregisteredPromise; + is(reg.unregistered, true, "registration should be unregistered"); + + //is(reg.quotaUsageCheckCount, 1, "Quota usage check must be started"); + await quotaUsageCheckFinishPromise; + + if (consumeQuotaOrigin) { + // Check that there is no longer any storage usaged by the origin in this + // case. + const originUsage = await get_qm_origin_usage(TEST_ORIGIN); + ok( + is_minimum_origin_usage(originUsage), + "origin usage should be mitigated" + ); + + if (consumeQuotaOrigin === SAME_GROUP_ORIGIN) { + const sameGroupUsage = await get_qm_origin_usage(SAME_GROUP_ORIGIN); + ok(sameGroupUsage === 0, "same group usage should be mitigated"); + } + } +} + +add_task(async function test_navigation_fetch_fault_handling() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.mitigations.bypass_on_fault", true], + ["dom.serviceWorkers.mitigations.group_usage_headroom_kb", 5 * 1024], + ["dom.quotaManager.testing", true], + // We want the temporary global limit to be 10 MiB (the pref is in KiB). + // This will result in the group limit also being 10 MiB because on small + // disks we provide a group limit value of min(10 MiB, global limit). + ["dom.quotaManager.temporaryStorage.fixedLimit", 10 * 1024], + ], + }); + + // Need to reset the storages to make dom.quotaManager.temporaryStorage.fixedLimit + // works. + await qm_reset_storage(); + + const quotaOriginVariations = [ + // Don't put us near the storage limit. + undefined, + // Put us near the storage limit in the SW origin itself. + TEST_ORIGIN, + // Put us near the storage limit in the SW origin's group but not the origin + // itself. + SAME_GROUP_ORIGIN, + ]; + + for (const consumeQuotaOrigin of quotaOriginVariations) { + await do_fault_injection_test({ + name: "NS_ERROR_DOM_ABORT_ERR", + useError: 0x80530014, // Not in `Cr`. + // Abort errors manifest as about:blank pages. + errorPage: "about:blank", + consumeQuotaOrigin, + }); + + await do_fault_injection_test({ + name: "NS_ERROR_INTERCEPTION_FAILED", + useError: 0x804b0064, // Not in `Cr`. + // Interception failures manifest as corrupt content pages. + errorPage: "about:neterror", + consumeQuotaOrigin, + }); + } + + // Cleanup: wipe the origin and group so all the ServiceWorkers go away. + await clear_qm_origin_group_via_clearData(TEST_ORIGIN); +}); diff --git a/dom/serviceworkers/test/browser_remote_type_process_swap.js b/dom/serviceworkers/test/browser_remote_type_process_swap.js new file mode 100644 index 0000000000..60c8078e0d --- /dev/null +++ b/dom/serviceworkers/test/browser_remote_type_process_swap.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test tests a navigation request to a Service Worker-controlled origin & + * scope that results in a cross-origin redirect to a + * non-Service Worker-controlled scope which additionally participates in + * cross-process redirect. + * + * On success, the test will not crash. + */ + +const ORIGIN = "http://mochi.test:8888"; +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + ORIGIN +); + +const SW_REGISTER_PAGE_URL = `${TEST_ROOT}empty_with_utils.html`; +const SW_SCRIPT_URL = `${TEST_ROOT}empty.js`; + +const FILE_URL = (() => { + // Get the file as an nsIFile. + const file = getChromeDir(getResolvedURI(gTestPath)); + file.append("empty.html"); + + // Convert the nsIFile to an nsIURI to access the path. + return Services.io.newFileURI(file).spec; +})(); + +const CROSS_ORIGIN = "https://example.com"; +const CROSS_ORIGIN_URL = SW_REGISTER_PAGE_URL.replace(ORIGIN, CROSS_ORIGIN); +const CROSS_ORIGIN_REDIRECT_URL = `${TEST_ROOT}redirect.sjs?${CROSS_ORIGIN_URL}`; + +async function loadURI(aXULBrowser, aURI) { + const browserLoadedPromise = BrowserTestUtils.browserLoaded(aXULBrowser); + BrowserTestUtils.loadURI(aXULBrowser, aURI); + + return browserLoadedPromise; +} + +async function runTest() { + // Step 1: register a Service Worker under `ORIGIN` so that all subsequent + // requests to `ORIGIN` will be marked as controlled. + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["devtools.console.stdout.content", true], + ], + }); + + info(`Loading tab with page ${SW_REGISTER_PAGE_URL}`); + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE_URL, + }); + info(`Loaded page ${SW_REGISTER_PAGE_URL}`); + + info(`Registering Service Worker ${SW_SCRIPT_URL}`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ scriptURL: SW_SCRIPT_URL }], + async ({ scriptURL }) => { + await content.wrappedJSObject.registerAndWaitForActive(scriptURL); + } + ); + info(`Registered and activated Service Worker ${SW_SCRIPT_URL}`); + + // Step 2: open a page over file:// and navigate to trigger a process swap + // for the response. + info(`Loading ${FILE_URL}`); + await loadURI(tab.linkedBrowser, FILE_URL); + + Assert.equal( + tab.linkedBrowser.remoteType, + E10SUtils.FILE_REMOTE_TYPE, + `${FILE_URL} should load in a file process` + ); + + info(`Dynamically creating ${FILE_URL}'s link`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ href: CROSS_ORIGIN_REDIRECT_URL }], + ({ href }) => { + const { document } = content; + const link = document.createElement("a"); + link.href = href; + link.id = "link"; + link.appendChild(document.createTextNode(href)); + document.body.appendChild(link); + } + ); + + const redirectPromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + CROSS_ORIGIN_URL + ); + + info("Starting navigation"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#link", + {}, + tab.linkedBrowser + ); + + info(`Waiting for location to change to ${CROSS_ORIGIN_URL}`); + await redirectPromise; + + info("Waiting for the browser to stop"); + await BrowserTestUtils.browserStopped(tab.linkedBrowser); + + if (SpecialPowers.useRemoteSubframes) { + Assert.ok( + E10SUtils.isWebRemoteType(tab.linkedBrowser.remoteType), + `${CROSS_ORIGIN_URL} should load in a web-content process` + ); + } + + // Step 3: cleanup. + info("Loading initial page to unregister all Service Workers"); + await loadURI(tab.linkedBrowser, SW_REGISTER_PAGE_URL); + + info("Unregistering all Service Workers"); + await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async () => await content.wrappedJSObject.unregisterAll() + ); + + info("Closing tab"); + BrowserTestUtils.removeTab(tab); +} + +add_task(runTest); diff --git a/dom/serviceworkers/test/browser_storage_permission.js b/dom/serviceworkers/test/browser_storage_permission.js new file mode 100644 index 0000000000..68c2c18bad --- /dev/null +++ b/dom/serviceworkers/test/browser_storage_permission.js @@ -0,0 +1,297 @@ +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const PAGE_URI = BASE_URI + "empty.html"; +const SCOPE = PAGE_URI + "?storage_permission"; +const SW_SCRIPT = BASE_URI + "empty.js"; + +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Until the e10s refactor is complete, use a single process to avoid + // service worker propagation race. + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ script: SW_SCRIPT, scope: SCOPE }], + async function(opts) { + let reg = await content.navigator.serviceWorker.register(opts.script, { + scope: opts.scope, + }); + let worker = reg.installing || reg.waiting || reg.active; + await new Promise(resolve => { + if (worker.state === "activated") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "activated") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_allow_permission() { + PermissionTestUtils.add( + PAGE_URI, + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_deny_permission() { + PermissionTestUtils.add( + PAGE_URI, + "cookie", + Ci.nsICookiePermission.ACCESS_DENY + ); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + is(controller, null, "page should be not controlled with storage denied"); + + BrowserTestUtils.removeTab(tab); + PermissionTestUtils.remove(PAGE_URI, "cookie"); +}); + +add_task(async function test_session_permission() { + PermissionTestUtils.add( + PAGE_URI, + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION + ); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + is(controller, null, "page should be not controlled with session storage"); + + BrowserTestUtils.removeTab(tab); + PermissionTestUtils.remove(PAGE_URI, "cookie"); +}); + +// Test to verify an about:blank iframe successfully inherits the +// parent's controller when storage is blocked between opening the +// parent page and creating the iframe. +add_task(async function test_block_storage_before_blank_iframe() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + let controller2 = await SpecialPowers.spawn(browser, [], async function() { + let f = content.document.createElement("iframe"); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller2, "page should be controlled with storage allowed"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + ], + }); + + let controller3 = await SpecialPowers.spawn(browser, [], async function() { + let f = content.document.createElement("iframe"); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller3, "page should be controlled with storage allowed"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +// Test to verify a blob URL iframe successfully inherits the +// parent's controller when storage is blocked between opening the +// parent page and creating the iframe. +add_task(async function test_block_storage_before_blob_iframe() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + let controller2 = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob(["<!DOCTYPE html><html></html>"], { + type: "text/html", + }); + let f = content.document.createElement("iframe"); + // No need to call revokeObjectURL() since the window will be closed shortly. + f.src = content.URL.createObjectURL(b); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller2, "page should be controlled with storage allowed"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + ], + }); + + let controller3 = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob(["<!DOCTYPE html><html></html>"], { + type: "text/html", + }); + let f = content.document.createElement("iframe"); + // No need to call revokeObjectURL() since the window will be closed shortly. + f.src = content.URL.createObjectURL(b); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller3, "page should be controlled with storage allowed"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +// Test to verify a blob worker script does not hit our service +// worker storage assertions when storage is blocked between opening +// the parent page and creating the worker. Note, we cannot +// explicitly check if the worker is controlled since we don't expose +// WorkerNavigator.serviceWorkers.controller yet. +add_task(async function test_block_storage_before_blob_worker() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + let scriptURL = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob( + ["self.postMessage(self.location.href);self.close()"], + { type: "application/javascript" } + ); + // No need to call revokeObjectURL() since the window will be closed shortly. + let u = content.URL.createObjectURL(b); + let w = new content.Worker(u); + return await new Promise(resolve => { + w.onmessage = e => resolve(e.data); + }); + }); + + ok(scriptURL.startsWith("blob:"), "blob URL worker should run"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + ], + }); + + let scriptURL2 = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob( + ["self.postMessage(self.location.href);self.close()"], + { type: "application/javascript" } + ); + // No need to call revokeObjectURL() since the window will be closed shortly. + let u = content.URL.createObjectURL(b); + let w = new content.Worker(u); + return await new Promise(resolve => { + w.onmessage = e => resolve(e.data); + }); + }); + + ok(scriptURL2.startsWith("blob:"), "blob URL worker should run"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function cleanup() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + let worker = reg.active; + await reg.unregister(); + await new Promise(resolve => { + if (worker.state === "redundant") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "redundant") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/browser_storage_recovery.js b/dom/serviceworkers/test/browser_storage_recovery.js new file mode 100644 index 0000000000..5ecc135021 --- /dev/null +++ b/dom/serviceworkers/test/browser_storage_recovery.js @@ -0,0 +1,152 @@ +"use strict"; + +// This test registers a SW for a scope that will never control a document +// and therefore never trigger a "fetch" functional event that would +// automatically attempt to update the registration. The overlap of the +// PAGE_URI and SCOPE is incidental. checkForUpdate is the only thing that +// will trigger an update of the registration and so there is no need to +// worry about Schedule Job races to coalesce an update job. + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const PAGE_URI = BASE_URI + "empty.html"; +const SCOPE = PAGE_URI + "?storage_recovery"; +const SW_SCRIPT = BASE_URI + "storage_recovery_worker.sjs"; + +async function checkForUpdate(browser) { + return SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + await reg.update(); + return !!reg.installing; + }); +} + +// Delete all of our chrome-namespace Caches for this origin, leaving any +// content-owned caches in place. This is exclusively for simulating loss +// of the origin's storage without loss of the registration and without +// having to worry that future enhancements to QuotaClients/ServiceWorkerRegistrar +// will break this test. If you want to wipe storage for an origin, use +// QuotaManager APIs +async function wipeStorage(u) { + let uri = Services.io.newURI(u); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + let caches = new CacheStorage("chrome", principal); + let list = await caches.keys(); + return Promise.all(list.map(c => caches.delete(c))); +} + +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.idle_timeout", 0], + ], + }); + + // Configure the server script to not redirect. + await fetch(SW_SCRIPT + "?clear-redirect"); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ script: SW_SCRIPT, scope: SCOPE }], + async function(opts) { + let reg = await content.navigator.serviceWorker.register(opts.script, { + scope: opts.scope, + }); + let worker = reg.installing || reg.waiting || reg.active; + await new Promise(resolve => { + if (worker.state === "activated") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "activated") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Verify that our service worker doesn't update normally. +add_task(async function normal_update_check() { + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let updated = await checkForUpdate(browser); + ok(!updated, "normal update check should not trigger an update"); + + BrowserTestUtils.removeTab(tab); +}); + +// Test what happens when we wipe the service worker scripts +// out from under the site before triggering the update. This +// should cause an update to occur. +add_task(async function wiped_update_check() { + // Wipe the backing cache storage, but leave the SW registered. + await wipeStorage(PAGE_URI); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let updated = await checkForUpdate(browser); + ok(updated, "wiping the service worker scripts should trigger an update"); + + BrowserTestUtils.removeTab(tab); +}); + +// Test what happens when we wipe the service worker scripts +// out from under the site before triggering the update. This +// should cause an update to occur. +add_task(async function wiped_and_failed_update_check() { + // Wipe the backing cache storage, but leave the SW registered. + await wipeStorage(PAGE_URI); + + // Configure the service worker script to redirect. This will + // prevent the update from completing successfully. + await fetch(SW_SCRIPT + "?set-redirect"); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Attempt to update the service worker. This should throw + // an error because the script is now redirecting. + let updateFailed = false; + try { + await checkForUpdate(browser); + } catch (e) { + updateFailed = true; + } + ok(updateFailed, "redirecting service worker script should fail to update"); + + // Also, since the existing service worker's scripts are broken + // we should also remove the registration completely when the + // update fails. + let exists = await SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + return !!reg; + }); + ok( + !exists, + "registration should be removed after scripts are wiped and update fails" + ); + + // Note, we don't have to clean up the service worker registration + // since its effectively been force-removed here. + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/browser_unregister_with_containers.js b/dom/serviceworkers/test/browser_unregister_with_containers.js new file mode 100644 index 0000000000..0304f70c1d --- /dev/null +++ b/dom/serviceworkers/test/browser_unregister_with_containers.js @@ -0,0 +1,153 @@ +"use strict"; + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const PAGE_URI = BASE_URI + "empty.html"; +const SCOPE = PAGE_URI + "?unregister_with_containers"; +const SW_SCRIPT = BASE_URI + "empty.js"; + +function doRegister(browser) { + return SpecialPowers.spawn( + browser, + [{ script: SW_SCRIPT, scope: SCOPE }], + async function(opts) { + let reg = await content.navigator.serviceWorker.register(opts.script, { + scope: opts.scope, + }); + let worker = reg.installing || reg.waiting || reg.active; + await new Promise(resolve => { + if (worker.state === "activated") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "activated") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + ); +} + +function doUnregister(browser) { + return SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + let worker = reg.active; + await reg.unregister(); + await new Promise(resolve => { + if (worker.state === "redundant") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "redundant") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + }); +} + +function isControlled(browser) { + return SpecialPowers.spawn(browser, [], function() { + return !!content.navigator.serviceWorker.controller; + }); +} + +async function checkControlled(browser) { + let controlled = await isControlled(browser); + ok(controlled, "window should be controlled"); +} + +async function checkUncontrolled(browser) { + let controlled = await isControlled(browser); + ok(!controlled, "window should not be controlled"); +} + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Avoid service worker propagation races by disabling multi-e10s for now. + // This can be removed after the e10s refactor is complete. + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + // Setup service workers in two different contexts with the same scope. + let containerTab1 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 1, + }); + let containerBrowser1 = gBrowser.getBrowserForTab(containerTab1); + await BrowserTestUtils.browserLoaded(containerBrowser1); + + let containerTab2 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 2, + }); + let containerBrowser2 = gBrowser.getBrowserForTab(containerTab2); + await BrowserTestUtils.browserLoaded(containerBrowser2); + + await doRegister(containerBrowser1); + await doRegister(containerBrowser2); + + await checkUncontrolled(containerBrowser1); + await checkUncontrolled(containerBrowser2); + + // Close the tabs we used to register the service workers. These are not + // controlled. + BrowserTestUtils.removeTab(containerTab1); + BrowserTestUtils.removeTab(containerTab2); + + // Open a controlled tab in each container. + containerTab1 = BrowserTestUtils.addTab(gBrowser, SCOPE, { + userContextId: 1, + }); + containerBrowser1 = gBrowser.getBrowserForTab(containerTab1); + await BrowserTestUtils.browserLoaded(containerBrowser1); + + containerTab2 = BrowserTestUtils.addTab(gBrowser, SCOPE, { + userContextId: 2, + }); + containerBrowser2 = gBrowser.getBrowserForTab(containerTab2); + await BrowserTestUtils.browserLoaded(containerBrowser2); + + await checkControlled(containerBrowser1); + await checkControlled(containerBrowser2); + + // Remove the first container's controlled tab + BrowserTestUtils.removeTab(containerTab1); + + // Create a new uncontrolled tab for the first container and use it to + // unregister the service worker. + containerTab1 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 1, + }); + containerBrowser1 = gBrowser.getBrowserForTab(containerTab1); + await BrowserTestUtils.browserLoaded(containerBrowser1); + await doUnregister(containerBrowser1); + + await checkUncontrolled(containerBrowser1); + await checkControlled(containerBrowser2); + + // Remove the second container's controlled tab + BrowserTestUtils.removeTab(containerTab2); + + // Create a new uncontrolled tab for the second container and use it to + // unregister the service worker. + containerTab2 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 2, + }); + containerBrowser2 = gBrowser.getBrowserForTab(containerTab2); + await BrowserTestUtils.browserLoaded(containerBrowser2); + await doUnregister(containerBrowser2); + + await checkUncontrolled(containerBrowser1); + await checkUncontrolled(containerBrowser2); + + // Close the two tabs we used to unregister the service worker. + BrowserTestUtils.removeTab(containerTab1); + BrowserTestUtils.removeTab(containerTab2); +}); diff --git a/dom/serviceworkers/test/browser_userContextId_openWindow.js b/dom/serviceworkers/test/browser_userContextId_openWindow.js new file mode 100644 index 0000000000..8b07a532fb --- /dev/null +++ b/dom/serviceworkers/test/browser_userContextId_openWindow.js @@ -0,0 +1,163 @@ +let Cm = Components.manager; + +let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +const URI = "https://example.com/browser/dom/serviceworkers/test/empty.html"; +const MOCK_CID = Components.ID("{2a0f83c4-8818-4914-a184-f1172b4eaaa7}"); +const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1"; +const USER_CONTEXT_ID = 3; + +let mockAlertsService = { + showAlert(alert, alertListener) { + ok(true, "Showing alert"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(function() { + alertListener.observe(null, "alertshow", alert.cookie); + }, 100); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(function() { + alertListener.observe(null, "alertclickcallback", alert.cookie); + }, 100); + }, + + showAlertNotification( + imageUrl, + title, + text, + textClickable, + cookie, + alertListener, + name, + dir, + lang, + data + ) { + this.showAlert(); + }, + + QueryInterface(aIID) { + if (aIID.equals(Ci.nsISupports) || aIID.equals(Ci.nsIAlertsService)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + createInstance(aIID) { + return this.QueryInterface(aIID); + }, +}; + +registerCleanupFunction(() => { + Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory( + MOCK_CID, + mockAlertsService + ); +}); + +add_setup(async function() { + // make sure userContext, SW and notifications are enabled. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userContext.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999], + ["browser.link.open_newwindow", 3], + ], + }); +}); + +add_task(async function test() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory( + MOCK_CID, + "alerts service", + ALERTS_SERVICE_CONTRACT_ID, + mockAlertsService + ); + + // open the tab in the correct userContextId + let tab = BrowserTestUtils.addTab(gBrowser, URI, { + userContextId: USER_CONTEXT_ID, + }); + let browser = gBrowser.getBrowserForTab(tab); + + // select tab and make sure its browser is focused + gBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + // wait for tab load + await BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab)); + + // Waiting for new tab. + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + + // here the test. + /* eslint-disable no-shadow */ + let uci = await SpecialPowers.spawn(browser, [URI], uri => { + let uci = content.document.nodePrincipal.userContextId; + + // Registration of the SW + return ( + content.navigator.serviceWorker + .register("file_userContextId_openWindow.js") + + // Activation + .then(swr => { + return new content.window.Promise(resolve => { + let worker = swr.installing; + worker.addEventListener("statechange", () => { + if (worker.state === "activated") { + resolve(swr); + } + }); + }); + }) + + // Ask for an openWindow. + .then(swr => { + swr.showNotification("testPopup"); + return uci; + }) + ); + }); + /* eslint-enable no-shadow */ + + is(uci, USER_CONTEXT_ID, "Tab runs with UCI " + USER_CONTEXT_ID); + + let newTab = await newTabPromise; + + is( + newTab.getAttribute("usercontextid"), + USER_CONTEXT_ID, + "New tab has UCI equal " + USER_CONTEXT_ID + ); + + // wait for SW unregistration + /* eslint-disable no-shadow */ + uci = await SpecialPowers.spawn(browser, [], () => { + let uci = content.document.nodePrincipal.userContextId; + + return content.navigator.serviceWorker + .getRegistration(".") + .then(registration => { + return registration.unregister(); + }) + .then(() => { + return uci; + }); + }); + /* eslint-enable no-shadow */ + + is(uci, USER_CONTEXT_ID, "Tab runs with UCI " + USER_CONTEXT_ID); + + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/bug1151916_driver.html b/dom/serviceworkers/test/bug1151916_driver.html new file mode 100644 index 0000000000..08e7d9414f --- /dev/null +++ b/dom/serviceworkers/test/bug1151916_driver.html @@ -0,0 +1,53 @@ +<html> + <body> + <script language="javascript"> + function fail(msg) { + window.parent.postMessage({ status: "failed", message: msg }, "*"); + } + + function success(msg) { + window.parent.postMessage({ status: "success", message: msg }, "*"); + } + + if (!window.parent) { + dump("This file must be embedded in an iframe!"); + } + + navigator.serviceWorker.getRegistration() + .then(function(reg) { + if (!reg) { + navigator.serviceWorker.ready.then(function(registration) { + if (registration.active.state == "activating") { + registration.active.onstatechange = function(e) { + registration.active.onstatechange = null; + if (registration.active.state == "activated") { + success("Registered and activated"); + } + } + } else { + success("Registered and activated"); + } + }); + navigator.serviceWorker.register("bug1151916_worker.js", + { scope: "." }); + } else { + // Simply force the sw to load a resource and touch self.caches. + if (!reg.active) { + fail("no-active-worker"); + return; + } + + fetch("madeup.txt").then(function(res) { + res.text().then(function(v) { + if (v == "Hi there") { + success("Loaded from cache"); + } else { + fail("Response text did not match"); + } + }, fail); + }, fail); + } + }, fail); + </script> + </body> +</html> diff --git a/dom/serviceworkers/test/bug1151916_worker.js b/dom/serviceworkers/test/bug1151916_worker.js new file mode 100644 index 0000000000..2688b78668 --- /dev/null +++ b/dom/serviceworkers/test/bug1151916_worker.js @@ -0,0 +1,15 @@ +onactivate = function(e) { + e.waitUntil( + self.caches.open("default-cache").then(function(cache) { + var response = new Response("Hi there"); + return cache.put("madeup.txt", response); + }) + ); +}; + +onfetch = function(e) { + if (e.request.url.match(/madeup.txt$/)) { + var p = self.caches.match("madeup.txt", { cacheName: "default-cache" }); + e.respondWith(p); + } +}; diff --git a/dom/serviceworkers/test/bug1240436_worker.js b/dom/serviceworkers/test/bug1240436_worker.js new file mode 100644 index 0000000000..c21f60b60f --- /dev/null +++ b/dom/serviceworkers/test/bug1240436_worker.js @@ -0,0 +1,2 @@ +// a contains a ZERO WIDTH JOINER (0x200D) +var a = ""; diff --git a/dom/serviceworkers/test/chrome-common.ini b/dom/serviceworkers/test/chrome-common.ini new file mode 100644 index 0000000000..b5cae318e1 --- /dev/null +++ b/dom/serviceworkers/test/chrome-common.ini @@ -0,0 +1,21 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + chrome_helpers.js + empty.js + fetch.js + hello.html + serviceworker.html + serviceworkerinfo_iframe.html + serviceworkermanager_iframe.html + serviceworkerregistrationinfo_iframe.html + utils.js + worker.js + worker2.js + +[test_devtools_track_serviceworker_time.html] +[test_privateBrowsing.html] +[test_serviceworkerinfo.xhtml] +skip-if = serviceworker_e10s # nsIWorkerDebugger attribute not implemented +[test_serviceworkermanager.xhtml] +[test_serviceworkerregistrationinfo.xhtml] diff --git a/dom/serviceworkers/test/chrome-dFPI.ini b/dom/serviceworkers/test/chrome-dFPI.ini new file mode 100644 index 0000000000..b07337f753 --- /dev/null +++ b/dom/serviceworkers/test/chrome-dFPI.ini @@ -0,0 +1,7 @@ +[DEFAULT] +# Enable dFPI(cookieBehavior 5) for service worker tests. +prefs = + network.cookie.cookieBehavior=5 +dupe-manifest = true + +[include:chrome-common.ini] diff --git a/dom/serviceworkers/test/chrome.ini b/dom/serviceworkers/test/chrome.ini new file mode 100644 index 0000000000..e6f7e3206d --- /dev/null +++ b/dom/serviceworkers/test/chrome.ini @@ -0,0 +1,4 @@ +[DEFAULT] +dupe-manifest = true + +[include:chrome-common.ini] diff --git a/dom/serviceworkers/test/chrome_helpers.js b/dom/serviceworkers/test/chrome_helpers.js new file mode 100644 index 0000000000..566ba52eca --- /dev/null +++ b/dom/serviceworkers/test/chrome_helpers.js @@ -0,0 +1,71 @@ +let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +let EXAMPLE_URL = "https://example.com/chrome/dom/serviceworkers/test/"; + +function waitForIframeLoad(iframe) { + return new Promise(function(resolve) { + iframe.onload = resolve; + }); +} + +function waitForRegister(scope, callback) { + return new Promise(function(resolve) { + let listener = { + onRegister(registration) { + if (registration.scope !== scope) { + return; + } + swm.removeListener(listener); + resolve(callback ? callback(registration) : registration); + }, + }; + swm.addListener(listener); + }); +} + +function waitForUnregister(scope) { + return new Promise(function(resolve) { + let listener = { + onUnregister(registration) { + if (registration.scope !== scope) { + return; + } + swm.removeListener(listener); + resolve(registration); + }, + }; + swm.addListener(listener); + }); +} + +function waitForServiceWorkerRegistrationChange(registration, callback) { + return new Promise(function(resolve) { + let listener = { + onChange() { + registration.removeListener(listener); + if (callback) { + callback(); + } + resolve(callback ? callback() : undefined); + }, + }; + registration.addListener(listener); + }); +} + +function waitForServiceWorkerShutdown() { + return new Promise(function(resolve) { + let observer = { + observe(subject, topic, data) { + if (topic !== "service-worker-shutdown") { + return; + } + SpecialPowers.removeObserver(observer, "service-worker-shutdown"); + resolve(); + }, + }; + SpecialPowers.addObserver(observer, "service-worker-shutdown"); + }); +} diff --git a/dom/serviceworkers/test/claim_clients/client.html b/dom/serviceworkers/test/claim_clients/client.html new file mode 100644 index 0000000000..969a6dbf10 --- /dev/null +++ b/dom/serviceworkers/test/claim_clients/client.html @@ -0,0 +1,43 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - claim client </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("This page shouldn't be launched directly!"); + } + + window.onload = function() { + parent.postMessage("READY", "*"); + } + + navigator.serviceWorker.oncontrollerchange = function() { + parent.postMessage({ + event: "controllerchange", + controller: (navigator.serviceWorker.controller !== null) + }, "*"); + } + + navigator.serviceWorker.onmessage = function(e) { + parent.postMessage({ + event: "message", + data: e.data + }, "*"); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/claim_oninstall_worker.js b/dom/serviceworkers/test/claim_oninstall_worker.js new file mode 100644 index 0000000000..6d79c919b0 --- /dev/null +++ b/dom/serviceworkers/test/claim_oninstall_worker.js @@ -0,0 +1,7 @@ +oninstall = function(e) { + var claimFailedPromise = new Promise(function(resolve, reject) { + clients.claim().then(reject, () => resolve()); + }); + + e.waitUntil(claimFailedPromise); +}; diff --git a/dom/serviceworkers/test/claim_worker_1.js b/dom/serviceworkers/test/claim_worker_1.js new file mode 100644 index 0000000000..493798a88b --- /dev/null +++ b/dom/serviceworkers/test/claim_worker_1.js @@ -0,0 +1,32 @@ +onactivate = function(e) { + var result = { + resolve_value: false, + match_count_before: -1, + match_count_after: -1, + message: "claim_worker_1", + }; + + self.clients + .matchAll() + .then(function(matched) { + // should be 0 + result.match_count_before = matched.length; + }) + .then(function() { + return self.clients.claim(); + }) + .then(function(ret) { + result.resolve_value = ret; + return self.clients.matchAll(); + }) + .then(function(matched) { + // should be 2 + result.match_count_after = matched.length; + for (i = 0; i < matched.length; i++) { + matched[i].postMessage(result); + } + if (result.match_count_after !== 2) { + dump("ERROR: claim_worker_1 failed to capture clients.\n"); + } + }); +}; diff --git a/dom/serviceworkers/test/claim_worker_2.js b/dom/serviceworkers/test/claim_worker_2.js new file mode 100644 index 0000000000..b244495ecd --- /dev/null +++ b/dom/serviceworkers/test/claim_worker_2.js @@ -0,0 +1,34 @@ +onactivate = function(e) { + var result = { + resolve_value: false, + match_count_before: -1, + match_count_after: -1, + message: "claim_worker_2", + }; + + self.clients + .matchAll() + .then(function(matched) { + // should be 0 + result.match_count_before = matched.length; + }) + .then(function() { + return clients.claim(); + }) + .then(function(ret) { + result.resolve_value = ret; + return clients.matchAll(); + }) + .then(function(matched) { + // should be 1 + result.match_count_after = matched.length; + if (result.match_count_after === 1) { + matched[0].postMessage(result); + } else { + dump("ERROR: claim_worker_2 failed to capture clients.\n"); + for (let i = 0; i < matched.length; ++i) { + dump("### ### matched[" + i + "]: " + matched[i].url + "\n"); + } + } + }); +}; diff --git a/dom/serviceworkers/test/close_test.js b/dom/serviceworkers/test/close_test.js new file mode 100644 index 0000000000..cc0ec4d335 --- /dev/null +++ b/dom/serviceworkers/test/close_test.js @@ -0,0 +1,22 @@ +function ok(v, msg) { + client.postMessage({ status: "ok", result: !!v, message: msg }); +} + +var client; +onmessage = function(e) { + if (e.data.message == "start") { + self.clients.matchAll().then(function(clients) { + client = clients[0]; + try { + close(); + ok(false, "close() should throw"); + } catch (ex) { + ok( + ex.name === "InvalidAccessError", + "close() should throw InvalidAccessError" + ); + } + client.postMessage({ status: "done" }); + }); + } +}; diff --git a/dom/serviceworkers/test/console_monitor.js b/dom/serviceworkers/test/console_monitor.js new file mode 100644 index 0000000000..e3c347b153 --- /dev/null +++ b/dom/serviceworkers/test/console_monitor.js @@ -0,0 +1,44 @@ +/* eslint-env mozilla/chrome-script */ + +let consoleListener; + +function ConsoleListener() { + Services.console.registerListener(this); +} + +ConsoleListener.prototype = { + callbacks: [], + + observe: aMsg => { + if (!(aMsg instanceof Ci.nsIScriptError)) { + return; + } + + let msg = { + cssSelectors: aMsg.cssSelectors, + errorMessage: aMsg.errorMessage, + sourceName: aMsg.sourceName, + sourceLine: aMsg.sourceLine, + lineNumber: aMsg.lineNumber, + columnNumber: aMsg.columnNumber, + category: aMsg.category, + windowID: aMsg.outerWindowID, + innerWindowID: aMsg.innerWindowID, + isScriptError: true, + isWarning: (aMsg.flags & Ci.nsIScriptError.warningFlag) === 1, + }; + + sendAsyncMessage("monitor", msg); + }, +}; + +addMessageListener("load", function(e) { + consoleListener = new ConsoleListener(); + sendAsyncMessage("ready", {}); +}); + +addMessageListener("unload", function(e) { + Services.console.unregisterListener(consoleListener); + consoleListener = null; + sendAsyncMessage("unloaded", {}); +}); diff --git a/dom/serviceworkers/test/controller/index.html b/dom/serviceworkers/test/controller/index.html new file mode 100644 index 0000000000..2a68e3f4bb --- /dev/null +++ b/dom/serviceworkers/test/controller/index.html @@ -0,0 +1,72 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + // Make sure to use good, unique messages, since the actual expression will not show up in test results. + function my_ok(result, msg) { + parent.postMessage({status: "ok", result, message: msg}, "*"); + } + + function finish() { + parent.postMessage({status: "done"}, "*"); + } + + navigator.serviceWorker.ready.then(function(swr) { + my_ok(swr.scope.match(/serviceworkers\/test\/control$/), + "This page should be controlled by upper level registration"); + my_ok(swr.installing == undefined, + "Upper level registration should not have a installing worker."); + if (navigator.serviceWorker.controller) { + // We are controlled. + // Register a new worker for this sub-scope. After that, controller should still be for upper level, but active should change to be this scope's. + navigator.serviceWorker.register("../worker2.js", { scope: "./" }).then(function(e) { + my_ok("installing" in e, "ServiceWorkerRegistration.installing exists."); + my_ok(e.installing instanceof ServiceWorker, "ServiceWorkerRegistration.installing is a ServiceWorker."); + + my_ok("waiting" in e, "ServiceWorkerRegistration.waiting exists."); + my_ok("active" in e, "ServiceWorkerRegistration.active exists."); + + my_ok(e.installing && + e.installing.scriptURL.match(/worker2.js$/), + "Installing is serviceworker/controller"); + + my_ok("scope" in e, "ServiceWorkerRegistration.scope exists."); + my_ok(e.scope.match(/serviceworkers\/test\/controller\/$/), "Scope is serviceworker/test/controller " + e.scope); + + my_ok("unregister" in e, "ServiceWorkerRegistration.unregister exists."); + + my_ok(navigator.serviceWorker.controller.scriptURL.match(/worker\.js$/), + "Controller is still worker.js"); + + e.unregister().then(function(result) { + my_ok(result, "Unregistering the SW should succeed"); + finish(); + }, function(error) { + dump("Error unregistering the SW: " + error + "\n"); + }); + }); + } else { + my_ok(false, "Should've been controlled!"); + finish(); + } + }).catch(function(e) { + my_ok(false, "Some test threw an error " + e); + finish(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/create_another_sharedWorker.html b/dom/serviceworkers/test/create_another_sharedWorker.html new file mode 100644 index 0000000000..f49194fa50 --- /dev/null +++ b/dom/serviceworkers/test/create_another_sharedWorker.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<title>Shared workers: create antoehr sharedworekr client</title> +<pre id=log>Hello World</pre> +<script> + var worker = new SharedWorker('sharedWorker_fetch.js'); +</script> diff --git a/dom/serviceworkers/test/download/window.html b/dom/serviceworkers/test/download/window.html new file mode 100644 index 0000000000..7d7893e0e6 --- /dev/null +++ b/dom/serviceworkers/test/download/window.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> + +function wait_until_controlled() { + return new Promise(function(resolve) { + if (navigator.serviceWorker.controller) { + return resolve(); + } + navigator.serviceWorker.addEventListener('controllerchange', function onController() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.removeEventListener('controllerchange', onController); + return resolve(); + } + }); + }); +} +addEventListener('load', function(event) { + var registration; + navigator.serviceWorker.register('worker.js').then(function(swr) { + registration = swr; + + // While the iframe below is a navigation, we still wait until we are + // controlled here. We want an active client to hold the service worker + // alive since it calls unregister() on itself. + return wait_until_controlled(); + + }).then(function() { + var frame = document.createElement('iframe'); + document.body.appendChild(frame); + frame.src = 'fake_download'; + + // The service worker is unregistered in the fetch event. The window and + // frame are cleaned up from the browser chrome script. + }); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/download/worker.js b/dom/serviceworkers/test/download/worker.js new file mode 100644 index 0000000000..71fb502f1c --- /dev/null +++ b/dom/serviceworkers/test/download/worker.js @@ -0,0 +1,34 @@ +addEventListener("install", function(evt) { + evt.waitUntil(self.skipWaiting()); +}); + +addEventListener("activate", function(evt) { + // We claim the current clients in order to ensure that we have an + // active client when we call unregister in the fetch handler. Otherwise + // the unregister() can kill the current worker before returning a + // response. + evt.waitUntil(clients.claim()); +}); + +addEventListener("fetch", function(evt) { + // This worker may live long enough to receive a fetch event from the next + // test. Just pass such requests through to the network. + if (!evt.request.url.includes("fake_download")) { + return; + } + + // We should only get a single download fetch event. Automatically unregister. + evt.respondWith( + registration.unregister().then(function() { + return new Response("service worker generated download", { + headers: { + "Content-Disposition": 'attachment; filename="fake_download.bin"', + // Prevent the default text editor from being launched + "Content-Type": "application/octet-stream", + // fake encoding header that should have no effect + "Content-Encoding": "gzip", + }, + }); + }) + ); +}); diff --git a/dom/serviceworkers/test/download_canceled/page_download_canceled.html b/dom/serviceworkers/test/download_canceled/page_download_canceled.html new file mode 100644 index 0000000000..e3904c4967 --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/page_download_canceled.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + +<script src="../utils.js"></script> +<script type="text/javascript"> +function wait_until_controlled() { + return new Promise(function(resolve) { + if (navigator.serviceWorker.controller) { + return resolve('controlled'); + } + navigator.serviceWorker.addEventListener('controllerchange', function onController() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.removeEventListener('controllerchange', onController); + return resolve('controlled'); + } + }); + }); +} +addEventListener('load', async function(event) { + window.controlled = wait_until_controlled(); + window.registration = + await navigator.serviceWorker.register('sw_download_canceled.js'); + let sw = registration.installing || registration.waiting || + registration.active; + await waitForState(sw, 'activated'); + sw.postMessage('claim'); +}); + +// Place to hold promises for stream closures reported by the SW. +window.streamClosed = {}; + +// The ServiceWorker will postMessage to this BroadcastChannel when the streams +// are closed. (Alternately, the SW could have used the clients API to post at +// us, but the mechanism by which that operates would be different when this +// test is uplifted, and it's desirable to avoid timing changes.) +// +// The browser test will use this promise to wait on stream shutdown. +window.swStreamChannel = new BroadcastChannel("stream-closed"); +function trackStreamClosure(path) { + let resolve; + const promise = new Promise(r => { resolve = r }); + window.streamClosed[path] = { promise, resolve }; +} +window.swStreamChannel.onmessage = ({ data }) => { + window.streamClosed[data.what].resolve(data); +}; +</script> + +</body> +</html> diff --git a/dom/serviceworkers/test/download_canceled/server-stream-download.sjs b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs new file mode 100644 index 0000000000..e6ebd68126 --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { setInterval, clearInterval } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) { + x = { + data, + QueryInterface(iid) { + return this; + }, + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function(x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +/* + * We want to let the sw_download_canceled.js service worker know when the + * stream was canceled. To this end, we let it issue a monitor request which we + * fulfill when the stream has been canceled. In order to coordinate between + * multiple requests, we use the getObjectState/setObjectState mechanism that + * httpd.js exposes to let data be shared and/or persist between requests. We + * handle both possible orderings of the requests because we currently don't + * try and impose an ordering between the two requests as issued by the SW, and + * file_blocked_script.sjs encourages us to do this, but we probably could order + * them. + */ +const MONITOR_KEY = "stream-monitor"; +function completeMonitorResponse(response, data) { + response.write(JSON.stringify(data)); + response.finish(); +} +function handleMonitorRequest(request, response) { + response.setHeader("Content-Type", "application/json"); + response.setStatusLine(request.httpVersion, 200, "Found"); + + response.processAsync(); + // Necessary to cause the headers to be flushed; that or touching the + // bodyOutputStream getter. + response.write(""); + dump("server-stream-download.js: monitor headers issued\n"); + + const alreadyCompleted = getGlobalState(MONITOR_KEY); + if (alreadyCompleted) { + completeMonitorResponse(response, alreadyCompleted); + setGlobalState(null, MONITOR_KEY); + } else { + setGlobalState(response, MONITOR_KEY); + } +} + +const MAX_TICK_COUNT = 3000; +const TICK_INTERVAL = 2; +function handleStreamRequest(request, response) { + const name = "server-stream-download"; + + // Create some payload to send. + let strChunk = + "Static routes are the future of ServiceWorkers! So say we all!\n"; + while (strChunk.length < 1024) { + strChunk += strChunk; + } + + response.setHeader("Content-Disposition", `attachment; filename="${name}"`); + response.setHeader( + "Content-Type", + `application/octet-stream; name="${name}"` + ); + response.setHeader("Content-Length", `${strChunk.length * MAX_TICK_COUNT}`); + response.setStatusLine(request.httpVersion, 200, "Found"); + + response.processAsync(); + response.write(strChunk); + dump("server-stream-download.js: stream headers + first payload issued\n"); + + let count = 0; + let intervalId; + function closeStream(why, message) { + dump("server-stream-download.js: closing stream: " + why + "\n"); + clearInterval(intervalId); + response.finish(); + + const data = { why, message }; + const monitorResponse = getGlobalState(MONITOR_KEY); + if (monitorResponse) { + completeMonitorResponse(monitorResponse, data); + setGlobalState(null, MONITOR_KEY); + } else { + setGlobalState(data, MONITOR_KEY); + } + } + function tick() { + try { + // bound worst-case behavior. + if (count++ > MAX_TICK_COUNT) { + closeStream("timeout", "timeout"); + return; + } + response.write(strChunk); + } catch (e) { + closeStream("canceled", e.message); + } + } + intervalId = setInterval(tick, TICK_INTERVAL); +} + +Components.utils.importGlobalProperties(["URLSearchParams"]); +function handleRequest(request, response) { + dump( + "server-stream-download.js: processing request for " + + request.path + + "?" + + request.queryString + + "\n" + ); + const query = new URLSearchParams(request.queryString); + if (query.has("monitor")) { + handleMonitorRequest(request, response); + } else { + handleStreamRequest(request, response); + } +} diff --git a/dom/serviceworkers/test/download_canceled/sw_download_canceled.js b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js new file mode 100644 index 0000000000..5d9d5f9bfd --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file is derived from :bkelly's https://glitch.com/edit/#!/html-sw-stream + +addEventListener("install", evt => { + evt.waitUntil(self.skipWaiting()); +}); + +// Create a BroadcastChannel to notify when we have closed our streams. +const channel = new BroadcastChannel("stream-closed"); + +const MAX_TICK_COUNT = 3000; +const TICK_INTERVAL = 4; +/** + * Generate a continuous stream of data at a sufficiently high frequency that a + * there"s a good chance of racing channel cancellation. + */ +function handleStream(evt, filename) { + // Create some payload to send. + const encoder = new TextEncoder(); + let strChunk = + "Static routes are the future of ServiceWorkers! So say we all!\n"; + while (strChunk.length < 1024) { + strChunk += strChunk; + } + const dataChunk = encoder.encode(strChunk); + + evt.waitUntil( + new Promise(resolve => { + let body = new ReadableStream({ + start: controller => { + const closeStream = why => { + console.log("closing stream: " + JSON.stringify(why) + "\n"); + clearInterval(intervalId); + resolve(); + // In event of error, the controller will automatically have closed. + if (why.why != "canceled") { + try { + controller.close(); + } catch (ex) { + // If we thought we should cancel but experienced a problem, + // that's a different kind of failure and we need to report it. + // (If we didn't catch the exception here, we'd end up erroneously + // in the tick() method's canceled handler.) + channel.postMessage({ + what: filename, + why: "close-failure", + message: ex.message, + ticks: why.ticks, + }); + return; + } + } + // Post prior to performing any attempt to close... + channel.postMessage(why); + }; + + controller.enqueue(dataChunk); + let count = 0; + let intervalId; + function tick() { + try { + // bound worst-case behavior. + if (count++ > MAX_TICK_COUNT) { + closeStream({ + what: filename, + why: "timeout", + message: "timeout", + ticks: count, + }); + return; + } + controller.enqueue(dataChunk); + } catch (e) { + closeStream({ + what: filename, + why: "canceled", + message: e.message, + ticks: count, + }); + } + } + // Alternately, streams' pull mechanism could be used here, but this + // test doesn't so much want to saturate the stream as to make sure the + // data is at least flowing a little bit. (Also, the author had some + // concern about slowing down the test by overwhelming the event loop + // and concern that we might not have sufficent back-pressure plumbed + // through and an infinite pipe might make bad things happen.) + intervalId = setInterval(tick, TICK_INTERVAL); + tick(); + }, + }); + evt.respondWith( + new Response(body, { + headers: { + "Content-Disposition": `attachment; filename="${filename}"`, + "Content-Type": "application/octet-stream", + }, + }) + ); + }) + ); +} + +/** + * Use an .sjs to generate a similar stream of data to the above, passing the + * response through directly. Because we're handing off the response but also + * want to be able to report when cancellation occurs, we create a second, + * overlapping long-poll style fetch that will not finish resolving until the + * .sjs experiences closure of its socket and terminates the payload stream. + */ +function handlePassThrough(evt, filename) { + evt.waitUntil( + (async () => { + console.log("issuing monitor fetch request"); + const response = await fetch("server-stream-download.sjs?monitor"); + console.log("monitor headers received, awaiting body"); + const data = await response.json(); + console.log("passthrough monitor fetch completed, notifying."); + channel.postMessage({ + what: filename, + why: data.why, + message: data.message, + }); + })() + ); + evt.respondWith( + fetch("server-stream-download.sjs").then(response => { + console.log("server-stream-download.sjs Response received, propagating"); + return response; + }) + ); +} + +addEventListener("fetch", evt => { + console.log(`SW processing fetch of ${evt.request.url}`); + if (evt.request.url.includes("sw-stream-download")) { + return handleStream(evt, "sw-stream-download"); + } + if (evt.request.url.includes("sw-passthrough-download")) { + return handlePassThrough(evt, "sw-passthrough-download"); + } +}); + +addEventListener("message", evt => { + if (evt.data === "claim") { + evt.waitUntil(clients.claim()); + } +}); diff --git a/dom/serviceworkers/test/empty.html b/dom/serviceworkers/test/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/empty.html diff --git a/dom/serviceworkers/test/empty.js b/dom/serviceworkers/test/empty.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/empty.js diff --git a/dom/serviceworkers/test/empty_with_utils.html b/dom/serviceworkers/test/empty_with_utils.html new file mode 100644 index 0000000000..75f0aa8872 --- /dev/null +++ b/dom/serviceworkers/test/empty_with_utils.html @@ -0,0 +1,13 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="utils.js" type="text/javascript"></script> +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/error_reporting_helpers.js b/dom/serviceworkers/test/error_reporting_helpers.js new file mode 100644 index 0000000000..42ddbe42a2 --- /dev/null +++ b/dom/serviceworkers/test/error_reporting_helpers.js @@ -0,0 +1,73 @@ +"use strict"; + +/** + * Helpers for use in tests that want to verify that localized error messages + * are logged during the test. Because most of our errors (ex: + * ServiceWorkerManager) generate nsIScriptError instances with flattened + * strings (the interpolated arguments aren't kept around), we load the string + * bundle and use it to derive the exact string message we expect for the given + * payload. + **/ + +let stringBundleService = SpecialPowers.Cc[ + "@mozilla.org/intl/stringbundle;1" +].getService(SpecialPowers.Ci.nsIStringBundleService); +let localizer = stringBundleService.createBundle( + "chrome://global/locale/dom/dom.properties" +); + +/** + * Start monitoring the console for the given localized error message string(s) + * with the given arguments to be logged. Call before running code that will + * generate the console message. Pair with a call to + * `wait_for_expected_message` invoked after the message should have been + * generated. + * + * Multiple error messages can be expected, just repeat the msgId and args + * argument pair as needed. + * + * @param {String} msgId + * The localization message identifier used in the properties file. + * @param {String[]} args + * The list of formatting arguments we expect the error to be generated with. + * @return {Object} Promise/handle to pass to wait_for_expected_message. + */ +function expect_console_message(/* msgId, args, ... */) { + let expectations = []; + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 2) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + if (args.length === 0) { + expectations.push({ errorMessage: localizer.GetStringFromName(msgId) }); + } else { + expectations.push({ + errorMessage: localizer.formatStringFromName(msgId, args), + }); + } + } + return new Promise(resolve => { + SimpleTest.monitorConsole(resolve, expectations); + }); +} +let expect_console_messages = expect_console_message; + +/** + * Stop monitoring the console, returning a Promise that will be resolved when + * the sentinel console message sent through the async data path has been + * received. The Promise will not reject on failure; instead a mochitest + * failure will have been generated by ok(false)/equivalent by the time it is + * resolved. + */ +function wait_for_expected_message(expectedPromise) { + SimpleTest.endMonitorConsole(); + return expectedPromise; +} + +/** + * Derive an absolute URL string from a relative URL to simplify error message + * argument generation. + */ +function make_absolute_url(relUrl) { + return new URL(relUrl, window.location).href; +} diff --git a/dom/serviceworkers/test/eval_worker.js b/dom/serviceworkers/test/eval_worker.js new file mode 100644 index 0000000000..79b4808e66 --- /dev/null +++ b/dom/serviceworkers/test/eval_worker.js @@ -0,0 +1 @@ +eval("1+1"); diff --git a/dom/serviceworkers/test/eventsource/eventsource.resource b/dom/serviceworkers/test/eventsource/eventsource.resource new file mode 100644 index 0000000000..eb62cbd4c5 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource.resource @@ -0,0 +1,22 @@ +:this file must be enconded in utf8 +:and its Content-Type must be equal to text/event-stream + +retry:500 +data: 2 +unknow: unknow + +event: other_event_name +retry:500 +data: 2 +unknow: unknow + +event: click +retry:500 + +event: blur +retry:500 + +event:keypress +retry:500 + + diff --git a/dom/serviceworkers/test/eventsource/eventsource.resource^headers^ b/dom/serviceworkers/test/eventsource/eventsource.resource^headers^ new file mode 100644 index 0000000000..5b88be7c32 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource.resource^headers^ @@ -0,0 +1,3 @@ +Content-Type: text/event-stream +Cache-Control: no-cache, must-revalidate +Access-Control-Allow-Origin: * diff --git a/dom/serviceworkers/test/eventsource/eventsource_cors_response.html b/dom/serviceworkers/test/eventsource/eventsource_cors_response.html new file mode 100644 index 0000000000..115a0f5c65 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_cors_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(true, "EventSource should work with cors responses"); + doUnregister(); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(false, "Something went wrong"); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js new file mode 100644 index 0000000000..0b9535d08e --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js @@ -0,0 +1,30 @@ +// Cross origin request +var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/"; + +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + ok( + request.cache === "no-store", + "EventSource should make a no-store request" + ); + var fetchRequest = new Request(prefix + "eventsource.resource", { + mode: "cors", + }); + event.respondWith( + fetch(fetchRequest).then(fetchResponse => { + return fetchResponse; + }) + ); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html new file mode 100644 index 0000000000..970cae517f --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "https://example.com/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(true, "EventSource should not work with mixed content cors responses"); + doUnregister(); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js new file mode 100644 index 0000000000..754e461a01 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js @@ -0,0 +1,29 @@ +var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/"; + +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + ok( + request.cache === "no-store", + "EventSource should make a no-store request" + ); + var fetchRequest = new Request(prefix + "eventsource.resource", { + mode: "cors", + }); + event.respondWith( + fetch(fetchRequest).then(fetchResponse => { + return fetchResponse; + }) + ); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html b/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html new file mode 100644 index 0000000000..bce12259cc --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(true, "EventSource should not work with opaque responses"); + doUnregister(); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_opaque_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js new file mode 100644 index 0000000000..f663e7e39c --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js @@ -0,0 +1,30 @@ +// Cross origin request +var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/"; + +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + ok( + request.cache === "no-store", + "EventSource should make a no-store request" + ); + var fetchRequest = new Request(prefix + "eventsource.resource", { + mode: "no-cors", + }); + event.respondWith( + fetch(fetchRequest).then(fetchResponse => { + return fetchResponse; + }) + ); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_register_worker.html b/dom/serviceworkers/test/eventsource/eventsource_register_worker.html new file mode 100644 index 0000000000..59e8e92ab6 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_register_worker.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + function getURLParam (aTarget, aValue) { + return decodeURI(aTarget.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURI(aValue).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1")); + } + + function onLoad() { + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "done"}, "*"); + }); + + navigator.serviceWorker.register(getURLParam(document.location, "script"), {scope: "."}); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html new file mode 100644 index 0000000000..7f6228c91e --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(true, "EventSource should work with synthetic responses"); + doUnregister(); + }; + source.onerror = function(error) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_synthetic_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js new file mode 100644 index 0000000000..32aeb91b81 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js @@ -0,0 +1,27 @@ +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + var headerList = { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, must-revalidate", + }; + var headers = new Headers(headerList); + var init = { + headers, + mode: "cors", + }; + var body = "data: data0\r\r"; + var response = new Response(body, init); + event.respondWith(response); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js b/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js new file mode 100644 index 0000000000..b110ae5f58 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js @@ -0,0 +1,17 @@ +function ok(aCondition, aMessage) { + return new Promise(function(resolve, reject) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + reject(); + return; + } + res[0].postMessage({ + status: "callback", + data: "ok", + condition: aCondition, + message: aMessage, + }); + resolve(); + }); + }); +} diff --git a/dom/serviceworkers/test/fetch.js b/dom/serviceworkers/test/fetch.js new file mode 100644 index 0000000000..ca723821ea --- /dev/null +++ b/dom/serviceworkers/test/fetch.js @@ -0,0 +1,33 @@ +function get_query_params(url) { + var search = new URL(url).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split("&"); + params.forEach(function(param) { + var element = param.split("="); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +addEventListener("fetch", function(event) { + if (event.request.url.includes("fail.html")) { + event.respondWith(fetch("hello.html", { integrity: "abc" })); + } else if (event.request.url.includes("fake.html")) { + event.respondWith(fetch("hello.html")); + } else if (event.request.url.includes("file_js_cache")) { + event.respondWith(fetch(event.request)); + } else if (event.request.url.includes("redirect")) { + let param = get_query_params(event.request.url); + let url = param.url; + let mode = param.mode; + + event.respondWith(fetch(url, { mode })); + } +}); + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/fetch/cookie/cookie_test.js b/dom/serviceworkers/test/fetch/cookie/cookie_test.js new file mode 100644 index 0000000000..cdfee8949d --- /dev/null +++ b/dom/serviceworkers/test/fetch/cookie/cookie_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("synth.html")) { + var body = + "<script>" + + 'window.parent.postMessage({status: "done", cookie: document.cookie}, "*");' + + "</script>"; + event.respondWith( + new Response(body, { headers: { "Content-Type": "text/html" } }) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/cookie/register.html b/dom/serviceworkers/test/fetch/cookie/register.html new file mode 100644 index 0000000000..99eabaf0a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/cookie/register.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script src="../../utils.js"></script> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + document.cookie = "foo=bar"; + + navigator.serviceWorker.register("cookie_test.js", {scope: "."}) + .then(reg => { + return waitForState(reg.installing, "activated", reg); + }).then(done); +</script> diff --git a/dom/serviceworkers/test/fetch/cookie/unregister.html b/dom/serviceworkers/test/fetch/cookie/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/cookie/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/deliver-gzip.sjs b/dom/serviceworkers/test/fetch/deliver-gzip.sjs new file mode 100644 index 0000000000..31d2a71f7a --- /dev/null +++ b/dom/serviceworkers/test/fetch/deliver-gzip.sjs @@ -0,0 +1,23 @@ +"use strict"; + +function handleRequest(request, response) { + // The string "hello" repeated 10 times followed by newline. Compressed using gzip. + /* eslint-disable prettier/prettier */ + let bytes = [0x1f, 0x8b, 0x08, 0x08, 0x4d, 0xe2, 0xf9, 0x54, 0x00, 0x03, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xcb, 0x48, 0xcd, 0xc9, 0xc9, 0xcf, + 0x20, 0x85, 0xe0, 0x02, 0x00, 0xf5, 0x4b, 0x38, 0xcf, 0x33, 0x00, + 0x00, 0x00]; + + /* eslint-enable prettier/prettier */ + + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Length", "" + bytes.length, false); + response.setHeader("Content-Type", "text/plain", false); + + let bos = Components.classes[ + "@mozilla.org/binaryoutputstream;1" + ].createInstance(Components.interfaces.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + + bos.writeByteArray(bytes); +} diff --git a/dom/serviceworkers/test/fetch/fetch_tests.js b/dom/serviceworkers/test/fetch/fetch_tests.js new file mode 100644 index 0000000000..bbe55c3a35 --- /dev/null +++ b/dom/serviceworkers/test/fetch/fetch_tests.js @@ -0,0 +1,716 @@ +var origin = "http://mochi.test:8888"; + +function fetchXHRWithMethod(name, method, onload, onerror, headers) { + expectAsyncResult(); + + onload = + onload || + function() { + my_ok(false, "XHR load should not complete successfully"); + finish(); + }; + onerror = + onerror || + function() { + my_ok( + false, + "XHR load for " + name + " should be intercepted successfully" + ); + finish(); + }; + + var x = new XMLHttpRequest(); + x.open(method, name, true); + x.onload = function() { + onload(x); + }; + x.onerror = function() { + onerror(x); + }; + headers = headers || []; + headers.forEach(function(header) { + x.setRequestHeader(header[0], header[1]); + }); + x.send(); +} + +var corsServerPath = + "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs"; +var corsServerURL = "http://example.com" + corsServerPath; + +function redirectURL(hops) { + return ( + hops[0].server + + corsServerPath + + "?hop=1&hops=" + + encodeURIComponent(JSON.stringify(hops)) + ); +} + +function fetchXHR(name, onload, onerror, headers) { + return fetchXHRWithMethod(name, "GET", onload, onerror, headers); +} + +fetchXHR("bare-synthesized.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "synthesized response body", + "load should have synthesized response" + ); + finish(); +}); + +fetchXHR("test-respondwith-response.txt", function(xhr) { + my_ok( + xhr.status == 200, + "test-respondwith-response load should be successful" + ); + my_ok( + xhr.responseText == "test-respondwith-response response body", + "load should have response" + ); + finish(); +}); + +fetchXHR("synthesized-404.txt", function(xhr) { + my_ok(xhr.status == 404, "load should 404"); + my_ok( + xhr.responseText == "synthesized response body", + "404 load should have synthesized response" + ); + finish(); +}); + +fetchXHR("synthesized-headers.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.getResponseHeader("X-Custom-Greeting") === "Hello", + "custom header should be set" + ); + my_ok( + xhr.responseText == "synthesized response body", + "custom header load should have synthesized response" + ); + finish(); +}); + +fetchXHR("synthesized-redirect-real-file.txt", function(xhr) { + dump("Got status AARRGH " + xhr.status + " " + xhr.responseText + "\n"); + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "This is a real file.\n", + "Redirect to real file should complete." + ); + finish(); +}); + +fetchXHR("synthesized-redirect-twice-real-file.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "This is a real file.\n", + "Redirect to real file (twice) should complete." + ); + finish(); +}); + +fetchXHR("synthesized-redirect-synthesized.txt", function(xhr) { + my_ok(xhr.status == 200, "synth+redirect+synth load should be successful"); + my_ok( + xhr.responseText == "synthesized response body", + "load should have redirected+synthesized response" + ); + finish(); +}); + +fetchXHR("synthesized-redirect-twice-synthesized.txt", function(xhr) { + my_ok( + xhr.status == 200, + "synth+redirect+synth (twice) load should be successful" + ); + my_ok( + xhr.responseText == "synthesized response body", + "load should have redirected+synthesized (twice) response" + ); + finish(); +}); + +fetchXHR("redirect.sjs", function(xhr) { + my_ok(xhr.status == 404, "redirected load should be uninterrupted"); + finish(); +}); + +fetchXHR("ignored.txt", function(xhr) { + my_ok(xhr.status == 404, "load should be uninterrupted"); + finish(); +}); + +fetchXHR("rejected.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR("nonresponse.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR("nonresponse2.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR("nonpromise.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR( + "headers.txt", + function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "1", "request header checks should have passed"); + finish(); + }, + null, + [ + ["X-Test1", "header1"], + ["X-Test2", "header2"], + ] +); + +fetchXHR("http://user:pass@mochi.test:8888/user-pass", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "http://user:pass@mochi.test:8888/user-pass", + "The username and password should be preserved" + ); + finish(); +}); + +fetchXHR("readable-stream.txt", function(xhr) { + my_ok(xhr.status == 200, "loading completed"); + my_ok(xhr.responseText == "Hello!", "The message is correct!"); + finish(); +}); + +fetchXHR( + "readable-stream-locked.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +fetchXHR( + "readable-stream-with-exception.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +fetchXHR( + "readable-stream-with-exception2.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +fetchXHR( + "readable-stream-already-consumed.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +var expectedUncompressedResponse = ""; +for (let i = 0; i < 10; ++i) { + expectedUncompressedResponse += "hello"; +} +expectedUncompressedResponse += "\n"; + +// ServiceWorker does not intercept, at which point the network request should +// be correctly decoded. +fetchXHR("deliver-gzip.sjs", function(xhr) { + my_ok(xhr.status == 200, "network gzip load should be successful"); + my_ok( + xhr.responseText == expectedUncompressedResponse, + "network gzip load should have synthesized response." + ); + my_ok( + xhr.getResponseHeader("Content-Encoding") == "gzip", + "network Content-Encoding should be gzip." + ); + my_ok( + xhr.getResponseHeader("Content-Length") == "35", + "network Content-Length should be of original gzipped file." + ); + finish(); +}); + +fetchXHR("hello.gz", function(xhr) { + my_ok(xhr.status == 200, "gzip load should be successful"); + my_ok( + xhr.responseText == expectedUncompressedResponse, + "gzip load should have synthesized response." + ); + my_ok( + xhr.getResponseHeader("Content-Encoding") == "gzip", + "Content-Encoding should be gzip." + ); + my_ok( + xhr.getResponseHeader("Content-Length") == "35", + "Content-Length should be of original gzipped file." + ); + finish(); +}); + +fetchXHR("hello-after-extracting.gz", function(xhr) { + my_ok(xhr.status == 200, "gzip load after extracting should be successful"); + my_ok( + xhr.responseText == expectedUncompressedResponse, + "gzip load after extracting should have synthesized response." + ); + my_ok( + xhr.getResponseHeader("Content-Encoding") == "gzip", + "Content-Encoding after extracting should be gzip." + ); + my_ok( + xhr.getResponseHeader("Content-Length") == "35", + "Content-Length after extracting should be of original gzipped file." + ); + finish(); +}); + +fetchXHR(corsServerURL + "?status=200&allowOrigin=*", function(xhr) { + my_ok( + xhr.status == 200, + "cross origin load with correct headers should be successful" + ); + my_ok( + xhr.getResponseHeader("access-control-allow-origin") == null, + "cors headers should be filtered out" + ); + finish(); +}); + +// Verify origin header is sent properly even when we have a no-intercept SW. +var uriOrigin = encodeURIComponent(origin); +fetchXHR( + "http://example.org" + + corsServerPath + + "?ignore&status=200&origin=" + + uriOrigin + + "&allowOrigin=" + + uriOrigin, + function(xhr) { + my_ok( + xhr.status == 200, + "cross origin load with correct headers should be successful" + ); + my_ok( + xhr.getResponseHeader("access-control-allow-origin") == null, + "cors headers should be filtered out" + ); + finish(); + } +); + +// Verify that XHR is considered CORS tainted even when original URL is same-origin +// redirected to cross-origin. +fetchXHR( + redirectURL([ + { server: origin }, + { server: "http://example.org", allowOrigin: origin }, + ]), + function(xhr) { + my_ok( + xhr.status == 200, + "cross origin load with correct headers should be successful" + ); + my_ok( + xhr.getResponseHeader("access-control-allow-origin") == null, + "cors headers should be filtered out" + ); + finish(); + } +); + +// Test that CORS preflight requests cannot be intercepted. Performs a +// cross-origin XHR that the SW chooses not to intercept. This requires a +// preflight request, which the SW must not be allowed to intercept. +fetchXHR( + corsServerURL + "?status=200&allowOrigin=*", + null, + function(xhr) { + my_ok( + xhr.status == 0, + "cross origin load with incorrect headers should be a failure" + ); + finish(); + }, + [["X-Unsafe", "unsafe"]] +); + +// Test that CORS preflight requests cannot be intercepted. Performs a +// cross-origin XHR that the SW chooses to intercept and respond with a +// cross-origin fetch. This requires a preflight request, which the SW must not +// be allowed to intercept. +fetchXHR( + "http://example.org" + corsServerPath + "?status=200&allowOrigin=*", + null, + function(xhr) { + my_ok( + xhr.status == 0, + "cross origin load with incorrect headers should be a failure" + ); + finish(); + }, + [["X-Unsafe", "unsafe"]] +); + +// Test that when the page fetches a url the controlling SW forces a redirect to +// another location. This other location fetch should also be intercepted by +// the SW. +fetchXHR("something.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "something else response body", + "load should have something else" + ); + finish(); +}); + +// Test fetch will internally get it's SkipServiceWorker flag set. The request is +// made from the SW through fetch(). fetch() fetches a server-side JavaScript +// file that force a redirect. The redirect location fetch does not go through +// the SW. +fetchXHR("redirect_serviceworker.sjs", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "// empty worker, always succeed!\n", + "load should have redirection content" + ); + finish(); +}); + +fetchXHR( + "empty-header", + function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "emptyheader", + "load should have the expected content" + ); + finish(); + }, + null, + [["emptyheader", ""]] +); + +expectAsyncResult(); +fetch( + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*" +).then( + function(res) { + my_ok(res.ok, "Valid CORS request should receive valid response"); + my_ok(res.type == "cors", "Response type should be CORS"); + res.text().then(function(body) { + my_ok( + body === "<res>hello pass</res>\n", + "cors response body should match" + ); + finish(); + }); + }, + function(e) { + my_ok(false, "CORS Fetch failed"); + finish(); + } +); + +expectAsyncResult(); +fetch( + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200", + { mode: "no-cors" } +).then( + function(res) { + my_ok(res.type == "opaque", "Response type should be opaque"); + my_ok(res.status == 0, "Status should be 0"); + res.text().then(function(body) { + my_ok(body === "", "opaque response body should be empty"); + finish(); + }); + }, + function(e) { + my_ok(false, "no-cors Fetch failed"); + finish(); + } +); + +expectAsyncResult(); +fetch("opaque-on-same-origin").then( + function(res) { + my_ok( + false, + "intercepted opaque response for non no-cors request should fail." + ); + finish(); + }, + function(e) { + my_ok( + true, + "intercepted opaque response for non no-cors request should fail." + ); + finish(); + } +); + +expectAsyncResult(); +fetch("http://example.com/opaque-no-cors", { mode: "no-cors" }).then( + function(res) { + my_ok( + res.type == "opaque", + "intercepted opaque response for no-cors request should have type opaque." + ); + finish(); + }, + function(e) { + my_ok( + false, + "intercepted opaque response for no-cors request should pass." + ); + finish(); + } +); + +expectAsyncResult(); +fetch("http://example.com/cors-for-no-cors", { mode: "no-cors" }).then( + function(res) { + my_ok( + res.type == "cors", + "synthesize CORS response should result in outer CORS response" + ); + finish(); + }, + function(e) { + my_ok(false, "cors-for-no-cors request should not reject"); + finish(); + } +); + +function arrayBufferFromString(str) { + var arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; ++i) { + arr[i] = str.charCodeAt(i); + } + return arr; +} + +expectAsyncResult(); +fetch(new Request("body-simple", { method: "POST", body: "my body" })) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch( + new Request("body-arraybufferview", { + method: "POST", + body: arrayBufferFromString("my body"), + }) +) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the ArrayBufferView body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch( + new Request("body-arraybuffer", { + method: "POST", + body: arrayBufferFromString("my body").buffer, + }) +) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the ArrayBuffer body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +var usp = new URLSearchParams(); +usp.set("foo", "bar"); +usp.set("baz", "qux"); +fetch(new Request("body-urlsearchparams", { method: "POST", body: usp })) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "foo=bar&baz=quxfoo=bar&baz=qux", + "the URLSearchParams body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +var fd = new FormData(); +fd.set("foo", "bar"); +fd.set("baz", "qux"); +fetch(new Request("body-formdata", { method: "POST", body: fd })) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body.indexOf('Content-Disposition: form-data; name="foo"\r\n\r\nbar') < + body.indexOf('Content-Disposition: form-data; name="baz"\r\n\r\nqux'), + "the FormData body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch( + new Request("body-blob", { + method: "POST", + body: new Blob(new String("my body")), + }) +) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the Blob body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch("interrupt.sjs").then( + function(res) { + my_ok(true, "interrupted fetch succeeded"); + res.text().then( + function(body) { + my_ok(false, "interrupted fetch shouldn't have complete body"); + finish(); + }, + function() { + my_ok(true, "interrupted fetch shouldn't have complete body"); + finish(); + } + ); + }, + function(e) { + my_ok(false, "interrupted fetch failed"); + finish(); + } +); + +["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"].forEach(function(method) { + fetchXHRWithMethod("xhr-method-test.txt", method, function(xhr) { + my_ok(xhr.status == 200, method + " load should be successful"); + if (method === "HEAD") { + my_ok( + xhr.responseText == "", + method + "load should not have synthesized response" + ); + } else { + my_ok( + xhr.responseText == "intercepted " + method, + method + " load should have synthesized response" + ); + } + finish(); + }); +}); + +expectAsyncResult(); +fetch(new Request("empty-header", { headers: { emptyheader: "" } })) + .then(function(res) { + return res.text(); + }) + .then( + function(body) { + my_ok( + body == "emptyheader", + "The empty header was observed in the fetch event" + ); + finish(); + }, + function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); + } + ); + +expectAsyncResult(); +fetch("fetchevent-extendable") + .then(function(res) { + return res.text(); + }) + .then( + function(body) { + my_ok(body == "extendable", "FetchEvent inherits from ExtendableEvent"); + finish(); + }, + function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); + } + ); + +expectAsyncResult(); +fetch("fetchevent-request") + .then(function(res) { + return res.text(); + }) + .then( + function(body) { + my_ok(body == "non-nullable", "FetchEvent.request must be non-nullable"); + finish(); + }, + function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); + } + ); diff --git a/dom/serviceworkers/test/fetch/fetch_worker_script.js b/dom/serviceworkers/test/fetch/fetch_worker_script.js new file mode 100644 index 0000000000..6eb0b18a77 --- /dev/null +++ b/dom/serviceworkers/test/fetch/fetch_worker_script.js @@ -0,0 +1,28 @@ +function my_ok(v, msg) { + postMessage({ type: "ok", value: v, msg }); +} + +function finish() { + postMessage("finish"); +} + +function expectAsyncResult() { + postMessage("expect"); +} + +expectAsyncResult(); +try { + var success = false; + importScripts("nonexistent_imported_script.js"); +} catch (x) {} + +my_ok(success, "worker imported script should be intercepted"); +finish(); + +function check_intercepted_script() { + success = true; +} + +importScripts("fetch_tests.js"); + +finish(); //corresponds to the gExpected increment before creating this worker diff --git a/dom/serviceworkers/test/fetch/hsts/embedder.html b/dom/serviceworkers/test/fetch/hsts/embedder.html new file mode 100644 index 0000000000..ad44809042 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/embedder.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + window.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="http://example.com/tests/dom/serviceworkers/test/fetch/hsts/index.html"></iframe> diff --git a/dom/serviceworkers/test/fetch/hsts/hsts_test.js b/dom/serviceworkers/test/fetch/hsts/hsts_test.js new file mode 100644 index 0000000000..f79307f4e4 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/hsts_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(fetch("realindex.html")); + } else if (event.request.url.includes("image-20px.png")) { + if (event.request.url.indexOf("https://") == 0) { + event.respondWith(fetch("image-40px.png")); + } else { + event.respondWith(Response.error()); + } + } +}); diff --git a/dom/serviceworkers/test/fetch/hsts/image-20px.png b/dom/serviceworkers/test/fetch/hsts/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/image-20px.png diff --git a/dom/serviceworkers/test/fetch/hsts/image-40px.png b/dom/serviceworkers/test/fetch/hsts/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/image-40px.png diff --git a/dom/serviceworkers/test/fetch/hsts/image.html b/dom/serviceworkers/test/fetch/hsts/image.html new file mode 100644 index 0000000000..7036ea954e --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/image.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script> +onload=function(){ + var img = new Image(); + img.src = "http://example.com/tests/dom/serviceworkers/test/fetch/hsts/image-20px.png"; + img.onload = function() { + window.parent.postMessage({status: "image", data: img.width}, "*"); + }; + img.onerror = function() { + window.parent.postMessage({status: "image", data: "error"}, "*"); + }; +}; +</script> diff --git a/dom/serviceworkers/test/fetch/hsts/realindex.html b/dom/serviceworkers/test/fetch/hsts/realindex.html new file mode 100644 index 0000000000..e7d282fe83 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/realindex.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<script> + var securityInfoPresent = !!SpecialPowers.wrap(window).docShell.currentDocumentChannel.securityInfo; + window.parent.postMessage({status: "protocol", + data: location.protocol, + securityInfoPresent}, + "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/hsts/register.html b/dom/serviceworkers/test/fetch/hsts/register.html new file mode 100644 index 0000000000..bcdc146aec --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("hsts_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/hsts/register.html^headers^ b/dom/serviceworkers/test/fetch/hsts/register.html^headers^ new file mode 100644 index 0000000000..a46bf65bd9 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/register.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Strict-Transport-Security: max-age=60 diff --git a/dom/serviceworkers/test/fetch/hsts/unregister.html b/dom/serviceworkers/test/fetch/hsts/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js b/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js new file mode 100644 index 0000000000..0490017332 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js @@ -0,0 +1,19 @@ +self.addEventListener("install", function(event) { + event.waitUntil( + caches.open("cache").then(function(cache) { + return cache.add("index.html"); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith( + new Promise(function(resolve, reject) { + caches.match(event.request).then(function(response) { + resolve(response.clone()); + }); + }) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/index.html b/dom/serviceworkers/test/fetch/https/clonedresponse/index.html new file mode 100644 index 0000000000..a435548443 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/index.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/register.html b/dom/serviceworkers/test/fetch/https/clonedresponse/register.html new file mode 100644 index 0000000000..41774f70d1 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html b/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/https/https_test.js b/dom/serviceworkers/test/fetch/https/https_test.js new file mode 100644 index 0000000000..ad144d58ab --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/https_test.js @@ -0,0 +1,31 @@ +self.addEventListener("install", function(event) { + event.waitUntil( + caches.open("cache").then(function(cache) { + var synth = new Response( + '<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-sw"}, "*");</script>', + { headers: { "Content-Type": "text/html" } } + ); + return Promise.all([ + cache.add("index.html"), + cache.put("synth-sw.html", synth), + ]); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.includes("synth-sw.html")) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.includes("synth-window.html")) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.includes("synth.html")) { + event.respondWith( + new Response( + '<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth"}, "*");</script>', + { headers: { "Content-Type": "text/html" } } + ) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/https/index.html b/dom/serviceworkers/test/fetch/https/index.html new file mode 100644 index 0000000000..a435548443 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/index.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/https/register.html b/dom/serviceworkers/test/fetch/https/register.html new file mode 100644 index 0000000000..fa666fe957 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/register.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(reg => { + return window.caches.open("cache").then(function(cache) { + var synth = new Response('<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-window"}, "*");</scri' + 'pt>', + {headers:{"Content-Type": "text/html"}}); + return cache.put('synth-window.html', synth).then(_ => done(reg)); + }); + }); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/https/unregister.html b/dom/serviceworkers/test/fetch/https/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png b/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png b/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/index.html b/dom/serviceworkers/test/fetch/imagecache-maxage/index.html new file mode 100644 index 0000000000..0d4c52eedd --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/index.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script> +var width, url, width2, url2; +function maybeReport() { + if (width !== undefined && url !== undefined && + width2 !== undefined && url2 !== undefined) { + window.parent.postMessage({status: "result", + width, + width2, + url, + url2}, "*"); + } +} +onload = function() { + width = document.querySelector("img").width; + width2 = document.querySelector("img").width; + maybeReport(); +}; +navigator.serviceWorker.onmessage = function(event) { + if (event.data.suffix == "2") { + url2 = event.data.url; + } else { + url = event.data.url; + } + maybeReport(); +}; +</script> +<img src="image.png"> +<img src="image2.png"> diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js b/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js new file mode 100644 index 0000000000..62707566ea --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js @@ -0,0 +1,45 @@ +function synthesizeImage(suffix) { + // Serve image-20px for the first page, and image-40px for the second page. + return clients + .matchAll() + .then(clients => { + var url = "image-20px.png"; + clients.forEach(client => { + if (client.url.indexOf("?new") > 0) { + url = "image-40px.png"; + } + client.postMessage({ suffix, url }); + }); + return fetch(url); + }) + .then(response => { + return response.arrayBuffer(); + }) + .then(ab => { + var headers; + if (suffix == "") { + headers = { + "Content-Type": "image/png", + Date: "Tue, 1 Jan 1990 01:02:03 GMT", + "Cache-Control": "max-age=1", + }; + } else { + headers = { + "Content-Type": "image/png", + "Cache-Control": "no-cache", + }; + } + return new Response(ab, { + status: 200, + headers, + }); + }); +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("image.png")) { + event.respondWith(synthesizeImage("")); + } else if (event.request.url.includes("image2.png")) { + event.respondWith(synthesizeImage("2")); + } +}); diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/register.html b/dom/serviceworkers/test/fetch/imagecache-maxage/register.html new file mode 100644 index 0000000000..af4dde2e29 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("maxage_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html b/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache/image-20px.png b/dom/serviceworkers/test/fetch/imagecache/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/image-20px.png diff --git a/dom/serviceworkers/test/fetch/imagecache/image-40px.png b/dom/serviceworkers/test/fetch/imagecache/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/image-40px.png diff --git a/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js b/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js new file mode 100644 index 0000000000..e1f2af736b --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js @@ -0,0 +1,15 @@ +function synthesizeImage() { + return clients.matchAll().then(clients => { + var url = "image-40px.png"; + clients.forEach(client => { + client.postMessage(url); + }); + return fetch(url); + }); +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("image-20px.png")) { + event.respondWith(synthesizeImage()); + } +}); diff --git a/dom/serviceworkers/test/fetch/imagecache/index.html b/dom/serviceworkers/test/fetch/imagecache/index.html new file mode 100644 index 0000000000..f634f68bb7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/index.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script> +var width, url; +function maybeReport() { + if (width !== undefined && url !== undefined) { + window.parent.postMessage({status: "result", + width, + url}, "*"); + } +} +onload = function() { + width = document.querySelector("img").width; + maybeReport(); +}; +navigator.serviceWorker.onmessage = function(event) { + url = event.data; + maybeReport(); +}; +</script> +<img src="image-20px.png"> diff --git a/dom/serviceworkers/test/fetch/imagecache/postmortem.html b/dom/serviceworkers/test/fetch/imagecache/postmortem.html new file mode 100644 index 0000000000..53356cd02c --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/postmortem.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script> +onload = function() { + var width = document.querySelector("img").width; + window.parent.postMessage({status: "postmortem", + width}, "*"); +}; +</script> +<img src="image-20px.png"> diff --git a/dom/serviceworkers/test/fetch/imagecache/register.html b/dom/serviceworkers/test/fetch/imagecache/register.html new file mode 100644 index 0000000000..f6d1eb382f --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/register.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- Load the image here to put it in the image cache --> +<img src="image-20px.png"> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("imagecache_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache/unregister.html b/dom/serviceworkers/test/fetch/imagecache/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js b/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js new file mode 100644 index 0000000000..593171bafe --- /dev/null +++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js @@ -0,0 +1,31 @@ +function sendResponseToParent(response) { + return ` + <!DOCTYPE html> + <script> + window.parent.postMessage({status: "done", data: "${response}"}, "*"); + </script> + `; +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + var response = "good"; + try { + importScripts("http://example.org/tests/dom/workers/test/foreign.js"); + } catch (e) { + dump("Got error " + e + " when importing the script\n"); + } + if (response === "good") { + try { + importScripts("/tests/dom/workers/test/redirect_to_foreign.sjs"); + } catch (e) { + dump("Got error " + e + " when importing the script\n"); + } + } + event.respondWith( + new Response(sendResponseToParent(response), { + headers: { "Content-Type": "text/html" }, + }) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html b/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html new file mode 100644 index 0000000000..41774f70d1 --- /dev/null +++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html b/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/index.html b/dom/serviceworkers/test/fetch/index.html new file mode 100644 index 0000000000..693810c6fc --- /dev/null +++ b/dom/serviceworkers/test/fetch/index.html @@ -0,0 +1,191 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<div id="style-test" style="background-color: white"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + function my_ok(result, msg) { + window.opener.postMessage({status: "ok", result, message: msg}, "*"); + } + + function check_intercepted_script() { + document.getElementById('intercepted-script').test_result = + document.currentScript == document.getElementById('intercepted-script'); + } + + function fetchXHR(name, onload, onerror, headers) { + gExpected++; + + onload = onload || function() { + my_ok(false, "load should not complete successfully"); + finish(); + }; + onerror = onerror || function() { + my_ok(false, "load should be intercepted successfully"); + finish(); + }; + + var x = new XMLHttpRequest(); + x.open('GET', name, true); + x.onload = function() { onload(x) }; + x.onerror = function() { onerror(x) }; + headers = headers || []; + headers.forEach(function(header) { + x.setRequestHeader(header[0], header[1]); + }); + x.send(); + } + + var gExpected = 0; + var gEncountered = 0; + function finish() { + gEncountered++; + if (gEncountered == gExpected) { + window.opener.postMessage({status: "done"}, "*"); + } + } + + function test_onload(creator, complete) { + gExpected++; + var elem = creator(); + elem.onload = function() { + complete.call(elem); + finish(); + }; + elem.onerror = function() { + my_ok(false, elem.tagName + " load should complete successfully"); + finish(); + }; + document.body.appendChild(elem); + } + + function expectAsyncResult() { + gExpected++; + } + + my_ok(navigator.serviceWorker.controller != null, "should be controlled"); +</script> +<script src="fetch_tests.js"></script> +<script> + test_onload(function() { + var elem = document.createElement('img'); + elem.src = "nonexistent_image.gifs"; + elem.id = 'intercepted-img'; + return elem; + }, function() { + my_ok(this.complete, "image should be complete"); + my_ok(this.naturalWidth == 1 && this.naturalHeight == 1, "image should be 1x1 gif"); + }); + + test_onload(function() { + var elem = document.createElement('script'); + elem.id = 'intercepted-script'; + elem.src = "nonexistent_script.js"; + return elem; + }, function() { + my_ok(this.test_result, "script load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('link'); + elem.href = "nonexistent_stylesheet.css"; + elem.rel = "stylesheet"; + return elem; + }, function() { + var styled = document.getElementById('style-test'); + my_ok(window.getComputedStyle(styled).backgroundColor == 'rgb(0, 0, 0)', + "stylesheet load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('iframe'); + elem.id = 'intercepted-iframe'; + elem.src = "nonexistent_page.html"; + return elem; + }, function() { + my_ok(this.test_result, "iframe load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('iframe'); + elem.id = 'intercepted-iframe-2'; + elem.src = "navigate.html"; + return elem; + }, function() { + my_ok(this.test_result, "iframe should successfully load"); + }); + + gExpected++; + var xhr = new XMLHttpRequest(); + xhr.addEventListener("load", function(evt) { + my_ok(evt.target.responseXML === null, "Load synthetic cross origin XML Document should be allowed"); + finish(); + }); + xhr.responseType = "document"; + xhr.open("GET", "load_cross_origin_xml_document_synthetic.xml"); + xhr.send(); + + gExpected++; + fetch( + "load_cross_origin_xml_document_cors.xml", + {mode: "same-origin"} + ).then(function(response) { + // issue: https://github.com/whatwg/fetch/issues/629 + my_ok(false, "Load CORS cross origin XML Document should not be allowed"); + finish(); + }, function(error) { + my_ok(true, "Load CORS cross origin XML Document should not be allowed"); + finish(); + }); + + gExpected++; + fetch( + "load_cross_origin_xml_document_opaque.xml", + {mode: "same-origin"} + ).then(function(response) { + my_ok(false, "Load opaque cross origin XML Document should not be allowed"); + finish(); + }, function(error) { + my_ok(true, "Load opaque cross origin XML Document should not be allowed"); + finish(); + }); + + gExpected++; + var worker = new Worker('nonexistent_worker_script.js'); + worker.onmessage = function(e) { + my_ok(e.data == "worker-intercept-success", "worker load intercepted"); + finish(); + }; + worker.onerror = function() { + my_ok(false, "worker load should be intercepted"); + }; + + gExpected++; + var worker = new Worker('fetch_worker_script.js'); + worker.onmessage = function(e) { + if (e.data == "finish") { + finish(); + } else if (e.data == "expect") { + gExpected++; + } else if (e.data.type == "ok") { + my_ok(e.data.value, "Fetch test on worker: " + e.data.msg); + } + }; + worker.onerror = function() { + my_ok(false, "worker should not cause any errors"); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/fetch/interrupt.sjs b/dom/serviceworkers/test/fetch/interrupt.sjs new file mode 100644 index 0000000000..6e5deeb832 --- /dev/null +++ b/dom/serviceworkers/test/fetch/interrupt.sjs @@ -0,0 +1,20 @@ +function handleRequest(request, response) { + var body = "a"; + for (var i = 0; i < 20; i++) { + body += body; + } + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + var count = 10; + response.write("Content-Length: " + body.length * count + "\r\n"); + response.write("Content-Type: text/plain; charset=utf-8\r\n"); + response.write("Cache-Control: no-cache, must-revalidate\r\n"); + response.write("\r\n"); + + for (var i = 0; i < count; i++) { + response.write(body); + } + + throw Components.Exception("", Components.results.NS_BINDING_ABORTED); +} diff --git a/dom/serviceworkers/test/fetch/origin/https/index-https.sjs b/dom/serviceworkers/test/fetch/origin/https/index-https.sjs new file mode 100644 index 0000000000..5250467ec7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/index-https.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "https://example.org/tests/dom/serviceworkers/test/fetch/origin/https/realindex.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/origin/https/origin_test.js b/dom/serviceworkers/test/fetch/origin/https/origin_test.js new file mode 100644 index 0000000000..5f76befc00 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/origin_test.js @@ -0,0 +1,29 @@ +var prefix = "/tests/dom/serviceworkers/test/fetch/origin/https/"; + +function addOpaqueRedirect(cache, file) { + return fetch(new Request(prefix + file, { redirect: "manual" })).then( + function(response) { + return cache.put(prefix + file, response); + } + ); +} + +self.addEventListener("install", function(event) { + event.waitUntil( + self.caches.open("origin-cache").then(c => { + return addOpaqueRedirect(c, "index-https.sjs"); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index-cached-https.sjs")) { + event.respondWith( + self.caches.open("origin-cache").then(c => { + return c.match(prefix + "index-https.sjs"); + }) + ); + } else { + event.respondWith(fetch(event.request)); + } +}); diff --git a/dom/serviceworkers/test/fetch/origin/https/realindex.html b/dom/serviceworkers/test/fetch/origin/https/realindex.html new file mode 100644 index 0000000000..87f3489455 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/realindex.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + window.opener.postMessage({status: "domain", data: document.domain}, "*"); + window.opener.postMessage({status: "origin", data: location.origin}, "*"); + window.opener.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^ b/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^ new file mode 100644 index 0000000000..5ed82fd065 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: https://example.com diff --git a/dom/serviceworkers/test/fetch/origin/https/register.html b/dom/serviceworkers/test/fetch/origin/https/register.html new file mode 100644 index 0000000000..2e99adba53 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("origin_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/https/unregister.html b/dom/serviceworkers/test/fetch/origin/https/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/index-to-https.sjs b/dom/serviceworkers/test/fetch/origin/index-to-https.sjs new file mode 100644 index 0000000000..2505c03686 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/index-to-https.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "https://example.org/tests/dom/serviceworkers/test/fetch/origin/realindex.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/origin/index.sjs b/dom/serviceworkers/test/fetch/origin/index.sjs new file mode 100644 index 0000000000..ca11827792 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/index.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "http://example.org/tests/dom/serviceworkers/test/fetch/origin/realindex.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/origin/origin_test.js b/dom/serviceworkers/test/fetch/origin/origin_test.js new file mode 100644 index 0000000000..005c3b8c39 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/origin_test.js @@ -0,0 +1,38 @@ +var prefix = "/tests/dom/serviceworkers/test/fetch/origin/"; + +function addOpaqueRedirect(cache, file) { + return fetch(new Request(prefix + file, { redirect: "manual" })).then( + function(response) { + return cache.put(prefix + file, response); + } + ); +} + +self.addEventListener("install", function(event) { + event.waitUntil( + self.caches.open("origin-cache").then(c => { + return Promise.all([ + addOpaqueRedirect(c, "index.sjs"), + addOpaqueRedirect(c, "index-to-https.sjs"), + ]); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index-cached.sjs")) { + event.respondWith( + self.caches.open("origin-cache").then(c => { + return c.match(prefix + "index.sjs"); + }) + ); + } else if (event.request.url.includes("index-to-https-cached.sjs")) { + event.respondWith( + self.caches.open("origin-cache").then(c => { + return c.match(prefix + "index-to-https.sjs"); + }) + ); + } else { + event.respondWith(fetch(event.request)); + } +}); diff --git a/dom/serviceworkers/test/fetch/origin/realindex.html b/dom/serviceworkers/test/fetch/origin/realindex.html new file mode 100644 index 0000000000..87f3489455 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/realindex.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + window.opener.postMessage({status: "domain", data: document.domain}, "*"); + window.opener.postMessage({status: "origin", data: location.origin}, "*"); + window.opener.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/realindex.html^headers^ b/dom/serviceworkers/test/fetch/origin/realindex.html^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/realindex.html^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/serviceworkers/test/fetch/origin/register.html b/dom/serviceworkers/test/fetch/origin/register.html new file mode 100644 index 0000000000..2e99adba53 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("origin_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/unregister.html b/dom/serviceworkers/test/fetch/origin/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/plugin/plugins.html b/dom/serviceworkers/test/fetch/plugin/plugins.html new file mode 100644 index 0000000000..b268f6d99e --- /dev/null +++ b/dom/serviceworkers/test/fetch/plugin/plugins.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<script> + var obj, embed; + + function ok(v, msg) { + window.opener.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function finish() { + document.documentElement.removeChild(obj); + document.documentElement.removeChild(embed); + window.opener.postMessage({status: "done"}, "*"); + } + + function test_object() { + obj = document.createElement("object"); + obj.setAttribute('data', "object"); + document.documentElement.appendChild(obj); + } + + function test_embed() { + embed = document.createElement("embed"); + embed.setAttribute('src', "embed"); + document.documentElement.appendChild(embed); + } + + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.destination === "object") { + ok(false, "<object> should not be intercepted"); + } else if (e.data.destination === "embed") { + ok(false, "<embed> should not be intercepted"); + } else if (e.data.destination === "" && e.data.resource === "foo.txt") { + navigator.serviceWorker.removeEventListener("message", onMessage); + finish(); + } + }); + + test_object(); + test_embed(); + // SW will definitely intercept fetch API, use this to see if plugins are + // intercepted before fetch(). + fetch("foo.txt"); +</script> diff --git a/dom/serviceworkers/test/fetch/plugin/worker.js b/dom/serviceworkers/test/fetch/plugin/worker.js new file mode 100644 index 0000000000..da3c1ba54a --- /dev/null +++ b/dom/serviceworkers/test/fetch/plugin/worker.js @@ -0,0 +1,15 @@ +self.addEventListener("fetch", function(event) { + var resource = event.request.url.split("/").pop(); + event.waitUntil( + clients.matchAll().then(clients => { + clients.forEach(client => { + if (client.url.includes("plugins.html")) { + client.postMessage({ + destination: event.request.destination, + resource, + }); + } + }); + }) + ); +}); diff --git a/dom/serviceworkers/test/fetch/real-file.txt b/dom/serviceworkers/test/fetch/real-file.txt new file mode 100644 index 0000000000..3ca2088ec0 --- /dev/null +++ b/dom/serviceworkers/test/fetch/real-file.txt @@ -0,0 +1 @@ +This is a real file. diff --git a/dom/serviceworkers/test/fetch/redirect.sjs b/dom/serviceworkers/test/fetch/redirect.sjs new file mode 100644 index 0000000000..dab558f4a8 --- /dev/null +++ b/dom/serviceworkers/test/fetch/redirect.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "synthesized-redirect-twice-real-file.txt"); +} diff --git a/dom/serviceworkers/test/fetch/requesturl/index.html b/dom/serviceworkers/test/fetch/requesturl/index.html new file mode 100644 index 0000000000..bc3e400a94 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/index.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = window.onmessage = e => { + window.parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="redirector.html"></iframe> diff --git a/dom/serviceworkers/test/fetch/requesturl/redirect.sjs b/dom/serviceworkers/test/fetch/requesturl/redirect.sjs new file mode 100644 index 0000000000..ae50a78aef --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/redirect.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "http://example.org/tests/dom/serviceworkers/test/fetch/requesturl/secret.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/requesturl/redirector.html b/dom/serviceworkers/test/fetch/requesturl/redirector.html new file mode 100644 index 0000000000..0a3afab9ee --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/redirector.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<meta http-equiv="refresh" content="3;URL=/tests/dom/serviceworkers/test/fetch/requesturl/redirect.sjs"> diff --git a/dom/serviceworkers/test/fetch/requesturl/register.html b/dom/serviceworkers/test/fetch/requesturl/register.html new file mode 100644 index 0000000000..19a2e022c2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("requesturl_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js b/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js new file mode 100644 index 0000000000..4d2680538f --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js @@ -0,0 +1,21 @@ +addEventListener("fetch", event => { + var url = event.request.url; + var badURL = url.indexOf("secret.html") > -1; + event.respondWith( + new Promise(resolve => { + clients.matchAll().then(clients => { + for (var client of clients) { + if (client.url.indexOf("index.html") > -1) { + client.postMessage({ + status: "ok", + result: !badURL, + message: "Should not find a bad URL (" + url + ")", + }); + break; + } + } + resolve(fetch(event.request)); + }); + }) + ); +}); diff --git a/dom/serviceworkers/test/fetch/requesturl/secret.html b/dom/serviceworkers/test/fetch/requesturl/secret.html new file mode 100644 index 0000000000..694c336355 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/secret.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +secret stuff +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/requesturl/unregister.html b/dom/serviceworkers/test/fetch/requesturl/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/index.html b/dom/serviceworkers/test/fetch/sandbox/index.html new file mode 100644 index 0000000000..1094a3995d --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/index.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "ok", result: true, message: "The iframe is not being intercepted"}, "*"); + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html b/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html new file mode 100644 index 0000000000..87261a495f --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "ok", result: false, message: "The iframe is being intercepted"}, "*"); + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/register.html b/dom/serviceworkers/test/fetch/sandbox/register.html new file mode 100644 index 0000000000..427b1a8da9 --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("sandbox_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js b/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js new file mode 100644 index 0000000000..81740edf6d --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js @@ -0,0 +1,5 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(fetch("intercepted_index.html")); + } +}); diff --git a/dom/serviceworkers/test/fetch/sandbox/unregister.html b/dom/serviceworkers/test/fetch/sandbox/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html new file mode 100644 index 0000000000..e99209aa4d --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script> + window.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + if (e.data.status == "protocol") { + document.querySelector("iframe").src = "image.html"; + } + }; +</script> +<iframe src="http://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/index.html"></iframe> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^ new file mode 100644 index 0000000000..602d9dc38d --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: upgrade-insecure-requests diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png b/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png b/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image.html b/dom/serviceworkers/test/fetch/upgrade-insecure/image.html new file mode 100644 index 0000000000..dfcfd80014 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script> +onload=function(){ + var img = new Image(); + img.src = "http://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png"; + img.onload = function() { + window.parent.postMessage({status: "image", data: img.width}, "*"); + }; + img.onerror = function() { + window.parent.postMessage({status: "image", data: "error"}, "*"); + }; +}; +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html b/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html new file mode 100644 index 0000000000..aaa255aad3 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "protocol", data: location.protocol}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/register.html b/dom/serviceworkers/test/fetch/upgrade-insecure/register.html new file mode 100644 index 0000000000..6309b9b218 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("upgrade-insecure_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html b/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js b/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js new file mode 100644 index 0000000000..f79307f4e4 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(fetch("realindex.html")); + } else if (event.request.url.includes("image-20px.png")) { + if (event.request.url.indexOf("https://") == 0) { + event.respondWith(fetch("image-40px.png")); + } else { + event.respondWith(Response.error()); + } + } +}); diff --git a/dom/serviceworkers/test/fetch_event_worker.js b/dom/serviceworkers/test/fetch_event_worker.js new file mode 100644 index 0000000000..2775ef5068 --- /dev/null +++ b/dom/serviceworkers/test/fetch_event_worker.js @@ -0,0 +1,364 @@ +// eslint-disable-next-line complexity +onfetch = function(ev) { + if (ev.request.url.includes("ignore")) { + return; + } + + if (ev.request.url.includes("bare-synthesized.txt")) { + ev.respondWith( + Promise.resolve(new Response("synthesized response body", {})) + ); + } else if (ev.request.url.includes("file_CrossSiteXHR_server.sjs")) { + // N.B. this response would break the rules of CORS if it were allowed, but + // this test relies upon the preflight request not being intercepted and + // thus this response should not be used. + if (ev.request.method == "OPTIONS") { + ev.respondWith( + new Response("", { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "X-Unsafe", + }, + }) + ); + } else if (ev.request.url.includes("example.org")) { + ev.respondWith(fetch(ev.request)); + } + } else if (ev.request.url.includes("synthesized-404.txt")) { + ev.respondWith( + Promise.resolve( + new Response("synthesized response body", { status: 404 }) + ) + ); + } else if (ev.request.url.includes("synthesized-headers.txt")) { + ev.respondWith( + Promise.resolve( + new Response("synthesized response body", { + headers: { + "X-Custom-Greeting": "Hello", + }, + }) + ) + ); + } else if (ev.request.url.includes("test-respondwith-response.txt")) { + ev.respondWith(new Response("test-respondwith-response response body", {})); + } else if (ev.request.url.includes("synthesized-redirect-real-file.txt")) { + ev.respondWith(Promise.resolve(Response.redirect("fetch/real-file.txt"))); + } else if ( + ev.request.url.includes("synthesized-redirect-twice-real-file.txt") + ) { + ev.respondWith( + Promise.resolve(Response.redirect("synthesized-redirect-real-file.txt")) + ); + } else if (ev.request.url.includes("synthesized-redirect-synthesized.txt")) { + ev.respondWith(Promise.resolve(Response.redirect("bare-synthesized.txt"))); + } else if ( + ev.request.url.includes("synthesized-redirect-twice-synthesized.txt") + ) { + ev.respondWith( + Promise.resolve(Response.redirect("synthesized-redirect-synthesized.txt")) + ); + } else if (ev.request.url.includes("rejected.txt")) { + ev.respondWith(Promise.reject()); + } else if (ev.request.url.includes("nonresponse.txt")) { + ev.respondWith(Promise.resolve(5)); + } else if (ev.request.url.includes("nonresponse2.txt")) { + ev.respondWith(Promise.resolve({})); + } else if (ev.request.url.includes("nonpromise.txt")) { + try { + // This should coerce to Promise(5) instead of throwing + ev.respondWith(5); + } catch (e) { + // test is expecting failure, so return a success if we get a thrown + // exception + ev.respondWith(new Response("respondWith(5) threw " + e)); + } + } else if (ev.request.url.includes("headers.txt")) { + var ok = true; + ok &= ev.request.headers.get("X-Test1") == "header1"; + ok &= ev.request.headers.get("X-Test2") == "header2"; + ev.respondWith(Promise.resolve(new Response(ok.toString(), {}))); + } else if (ev.request.url.includes("readable-stream.txt")) { + ev.respondWith( + new Response( + new ReadableStream({ + start(controller) { + controller.enqueue( + new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]) + ); + controller.close(); + }, + }) + ) + ); + } else if (ev.request.url.includes("readable-stream-locked.txt")) { + let stream = new ReadableStream({ + start(controller) { + controller.enqueue( + new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]) + ); + controller.close(); + }, + }); + + ev.respondWith(new Response(stream)); + + // This locks the stream. + stream.getReader(); + } else if (ev.request.url.includes("readable-stream-with-exception.txt")) { + ev.respondWith( + new Response( + new ReadableStream({ + start(controller) {}, + pull() { + throw "EXCEPTION!"; + }, + }) + ) + ); + } else if (ev.request.url.includes("readable-stream-with-exception2.txt")) { + ev.respondWith( + new Response( + new ReadableStream({ + _controller: null, + _count: 0, + + start(controller) { + this._controller = controller; + }, + pull() { + if (++this._count == 5) { + throw "EXCEPTION 2!"; + } + this._controller.enqueue(new Uint8Array([this._count])); + }, + }) + ) + ); + } else if (ev.request.url.includes("readable-stream-already-consumed.txt")) { + let r = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue( + new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]) + ); + controller.close(); + }, + }) + ); + + r.blob(); + + ev.respondWith(r); + } else if (ev.request.url.includes("user-pass")) { + ev.respondWith(new Response(ev.request.url)); + } else if (ev.request.url.includes("nonexistent_image.gif")) { + var imageAsBinaryString = atob( + "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs" + ); + var imageLength = imageAsBinaryString.length; + + // If we just pass |imageAsBinaryString| to the Response constructor, an + // encoding conversion occurs that corrupts the image. Instead, we need to + // convert it to a typed array. + // typed array. + var imageAsArray = new Uint8Array(imageLength); + for (var i = 0; i < imageLength; ++i) { + imageAsArray[i] = imageAsBinaryString.charCodeAt(i); + } + + ev.respondWith( + Promise.resolve( + new Response(imageAsArray, { headers: { "Content-Type": "image/gif" } }) + ) + ); + } else if (ev.request.url.includes("nonexistent_script.js")) { + ev.respondWith( + Promise.resolve(new Response("check_intercepted_script();", {})) + ); + } else if (ev.request.url.includes("nonexistent_stylesheet.css")) { + ev.respondWith( + Promise.resolve( + new Response("#style-test { background-color: black !important; }", { + headers: { + "Content-Type": "text/css", + }, + }) + ) + ); + } else if (ev.request.url.includes("nonexistent_page.html")) { + ev.respondWith( + Promise.resolve( + new Response( + "<script>window.frameElement.test_result = true;</script>", + { + headers: { + "Content-Type": "text/html", + }, + } + ) + ) + ); + } else if (ev.request.url.includes("navigate.html")) { + var requests = [ + // should not throw + new Request(ev.request), + new Request(ev.request, undefined), + new Request(ev.request, null), + new Request(ev.request, {}), + new Request(ev.request, { someUnrelatedProperty: 42 }), + new Request(ev.request, { method: "GET" }), + ]; + ev.respondWith( + Promise.resolve( + new Response( + "<script>window.frameElement.test_result = true;</script>", + { + headers: { + "Content-Type": "text/html", + }, + } + ) + ) + ); + } else if (ev.request.url.includes("nonexistent_worker_script.js")) { + ev.respondWith( + Promise.resolve( + new Response("postMessage('worker-intercept-success')", { + headers: { "Content-Type": "text/javascript" }, + }) + ) + ); + } else if (ev.request.url.includes("nonexistent_imported_script.js")) { + ev.respondWith( + Promise.resolve( + new Response("check_intercepted_script();", { + headers: { "Content-Type": "text/javascript" }, + }) + ) + ); + } else if (ev.request.url.includes("deliver-gzip")) { + // Don't handle the request, this will make Necko perform a network request, at + // which point SetApplyConversion must be re-enabled, otherwise the request + // will fail. + return; + } else if (ev.request.url.includes("hello.gz")) { + ev.respondWith(fetch("fetch/deliver-gzip.sjs")); + } else if (ev.request.url.includes("hello-after-extracting.gz")) { + ev.respondWith( + fetch("fetch/deliver-gzip.sjs").then(function(res) { + return res.text().then(function(body) { + return new Response(body, { + status: res.status, + statusText: res.statusText, + headers: res.headers, + }); + }); + }) + ); + } else if (ev.request.url.includes("opaque-on-same-origin")) { + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200"; + ev.respondWith(fetch(url, { mode: "no-cors" })); + } else if (ev.request.url.includes("opaque-no-cors")) { + if (ev.request.mode != "no-cors") { + ev.respondWith(Promise.reject()); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200"; + ev.respondWith(fetch(url, { mode: ev.request.mode })); + } else if (ev.request.url.includes("cors-for-no-cors")) { + if (ev.request.mode != "no-cors") { + ev.respondWith(Promise.reject()); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*"; + ev.respondWith(fetch(url)); + } else if (ev.request.url.includes("example.com")) { + ev.respondWith(fetch(ev.request)); + } else if (ev.request.url.includes("body-")) { + ev.respondWith( + ev.request.text().then(function(body) { + return new Response(body + body); + }) + ); + } else if (ev.request.url.includes("something.txt")) { + ev.respondWith(Response.redirect("fetch/somethingelse.txt")); + } else if (ev.request.url.includes("somethingelse.txt")) { + ev.respondWith(new Response("something else response body", {})); + } else if (ev.request.url.includes("redirect_serviceworker.sjs")) { + // The redirect_serviceworker.sjs server-side JavaScript file redirects to + // 'http://mochi.test:8888/tests/dom/serviceworkers/test/worker.js' + // The redirected fetch should not go through the SW since the original + // fetch was initiated from a SW. + ev.respondWith(fetch("redirect_serviceworker.sjs")); + } else if ( + ev.request.url.includes("load_cross_origin_xml_document_synthetic.xml") + ) { + ev.respondWith( + Promise.resolve( + new Response("<response>body</response>", { + headers: { "Content-Type": "text/xtml" }, + }) + ) + ); + } else if ( + ev.request.url.includes("load_cross_origin_xml_document_cors.xml") + ) { + if (ev.request.mode != "same-origin") { + ev.respondWith(Promise.reject()); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*"; + ev.respondWith(fetch(url, { mode: "cors" })); + } else if ( + ev.request.url.includes("load_cross_origin_xml_document_opaque.xml") + ) { + if (ev.request.mode != "same-origin") { + Promise.resolve( + new Response("<error>Invalid Request mode</error>", { + headers: { "Content-Type": "text/xtml" }, + }) + ); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200"; + ev.respondWith(fetch(url, { mode: "no-cors" })); + } else if (ev.request.url.includes("xhr-method-test.txt")) { + ev.respondWith(new Response("intercepted " + ev.request.method)); + } else if (ev.request.url.includes("empty-header")) { + if ( + !ev.request.headers.has("emptyheader") || + ev.request.headers.get("emptyheader") !== "" + ) { + ev.respondWith(Promise.reject()); + return; + } + ev.respondWith(new Response("emptyheader")); + } else if (ev.request.url.includes("fetchevent-extendable")) { + if (ev instanceof ExtendableEvent) { + ev.respondWith(new Response("extendable")); + } else { + ev.respondWith(Promise.reject()); + } + } else if (ev.request.url.includes("fetchevent-request")) { + var threw = false; + try { + new FetchEvent("foo"); + } catch (e) { + if (e.name == "TypeError") { + threw = true; + } + } finally { + ev.respondWith(new Response(threw ? "non-nullable" : "nullable")); + } + } +}; diff --git a/dom/serviceworkers/test/file_blob_response_worker.js b/dom/serviceworkers/test/file_blob_response_worker.js new file mode 100644 index 0000000000..05e82b8c5d --- /dev/null +++ b/dom/serviceworkers/test/file_blob_response_worker.js @@ -0,0 +1,39 @@ +function makeFileBlob(obj) { + return new Promise(function(resolve, reject) { + var request = indexedDB.open("file_blob_response_worker", 1); + request.onerror = reject; + request.onupgradeneeded = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var objectStore = db.createObjectStore("test", { autoIncrement: true }); + var index = objectStore.createIndex("test", "index"); + }; + + request.onsuccess = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var blob = new Blob([JSON.stringify(obj)], { type: "application/json" }); + var data = { blob, index: 5 }; + + objectStore = db.transaction("test", "readwrite").objectStore("test"); + objectStore.add(data).onsuccess = function(event) { + var key = event.target.result; + objectStore = db.transaction("test").objectStore("test"); + objectStore.get(key).onsuccess = function(event1) { + resolve(event1.target.result.blob); + }; + }; + }; + }); +} + +self.addEventListener("fetch", function(evt) { + var result = { value: "success" }; + evt.respondWith( + makeFileBlob(result).then(function(blob) { + return new Response(blob); + }) + ); +}); diff --git a/dom/serviceworkers/test/file_js_cache.html b/dom/serviceworkers/test/file_js_cache.html new file mode 100644 index 0000000000..6feb94d872 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Add a tag script to save the bytecode</title> +</head> +<body> + <script id="watchme" src="file_js_cache.js"></script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_js_cache.js b/dom/serviceworkers/test/file_js_cache.js new file mode 100644 index 0000000000..b9b966775c --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache.js @@ -0,0 +1,5 @@ +function baz() {} +function bar() {} +function foo() { bar() } +foo(); + diff --git a/dom/serviceworkers/test/file_js_cache_cleanup.js b/dom/serviceworkers/test/file_js_cache_cleanup.js new file mode 100644 index 0000000000..5a647046a1 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_cleanup.js @@ -0,0 +1,16 @@ +"use strict"; +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +function clearCache() { + const cacheStorageSrv = Cc[ + "@mozilla.org/netwerk/cache-storage-service;1" + ].getService(Ci.nsICacheStorageService); + cacheStorageSrv.clear(); +} + +addMessageListener("teardown", function() { + clearCache(); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/serviceworkers/test/file_js_cache_save_after_load.html b/dom/serviceworkers/test/file_js_cache_save_after_load.html new file mode 100644 index 0000000000..8a696c0026 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_save_after_load.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Save the bytecode when all scripts are executed</title> +</head> +<body> + <script id="watchme" src="file_js_cache_save_after_load.js"></script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_js_cache_save_after_load.js b/dom/serviceworkers/test/file_js_cache_save_after_load.js new file mode 100644 index 0000000000..2c2536c584 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_save_after_load.js @@ -0,0 +1,15 @@ +function send_ping() { + window.dispatchEvent(new Event("ping")); +} +send_ping(); // ping (=1) + +window.addEventListener("load", function() { + send_ping(); // ping (=2) + + // Append a script which should call |foo|, before the encoding of this script + // bytecode. + var script = document.createElement("script"); + script.type = "text/javascript"; + script.innerText = "send_ping();"; // ping (=3) + document.head.appendChild(script); +}); diff --git a/dom/serviceworkers/test/file_js_cache_syntax_error.html b/dom/serviceworkers/test/file_js_cache_syntax_error.html new file mode 100644 index 0000000000..cc4a9b2daa --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_syntax_error.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Do not save bytecode on compilation errors</title> +</head> +<body> + <script id="watchme" src="file_js_cache_syntax_error.js"></script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_js_cache_syntax_error.js b/dom/serviceworkers/test/file_js_cache_syntax_error.js new file mode 100644 index 0000000000..fcf587ae70 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_syntax_error.js @@ -0,0 +1 @@ +var // SyntaxError: missing variable name. diff --git a/dom/serviceworkers/test/file_js_cache_with_sri.html b/dom/serviceworkers/test/file_js_cache_with_sri.html new file mode 100644 index 0000000000..38ecb26984 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_with_sri.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Add a tag script to save the bytecode</title> +</head> +<body> + <script id="watchme" src="file_js_cache.js" + integrity="sha384-8YSwN2ywq1SVThihWhj7uTVZ4UeIDwo3GgdPYnug+C+OS0oa6kH2IXBclwMaDJFb"> + </script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_notification_openWindow.html b/dom/serviceworkers/test/file_notification_openWindow.html new file mode 100644 index 0000000000..f220f4808d --- /dev/null +++ b/dom/serviceworkers/test/file_notification_openWindow.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1578070</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> +window.onload = () => { + navigator.serviceWorker.ready.then(() => { + // Open and close a new window. + window.open("https://example.org/").close(); + + // If we make it here, then we didn't crash. Tell the worker we're done. + navigator.serviceWorker.controller.postMessage("DONE"); + + // We're done! + window.close(); + }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/file_userContextId_openWindow.js b/dom/serviceworkers/test/file_userContextId_openWindow.js new file mode 100644 index 0000000000..649a3152ba --- /dev/null +++ b/dom/serviceworkers/test/file_userContextId_openWindow.js @@ -0,0 +1,3 @@ +onnotificationclick = event => { + clients.openWindow("empty.html"); +}; diff --git a/dom/serviceworkers/test/force_refresh_browser_worker.js b/dom/serviceworkers/test/force_refresh_browser_worker.js new file mode 100644 index 0000000000..9c5be48530 --- /dev/null +++ b/dom/serviceworkers/test/force_refresh_browser_worker.js @@ -0,0 +1,42 @@ +var name = "browserRefresherCache"; + +self.addEventListener("install", function(event) { + event.waitUntil( + Promise.all([ + caches.open(name), + fetch("./browser_cached_force_refresh.html"), + ]).then(function(results) { + var cache = results[0]; + var response = results[1]; + return cache.put("./browser_base_force_refresh.html", response); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + event.respondWith( + caches + .open(name) + .then(function(cache) { + return cache.match(event.request); + }) + .then(function(response) { + return response || fetch(event.request); + }) + ); +}); + +self.addEventListener("message", function(event) { + if (event.data.type === "GET_UNCONTROLLED_CLIENTS") { + event.waitUntil( + clients + .matchAll({ includeUncontrolled: true }) + .then(function(clientList) { + var resultList = clientList.map(function(c) { + return { url: c.url, frameType: c.frameType }; + }); + event.source.postMessage({ type: "CLIENTS", detail: resultList }); + }) + ); + } +}); diff --git a/dom/serviceworkers/test/force_refresh_worker.js b/dom/serviceworkers/test/force_refresh_worker.js new file mode 100644 index 0000000000..2e266b54d7 --- /dev/null +++ b/dom/serviceworkers/test/force_refresh_worker.js @@ -0,0 +1,43 @@ +var name = "refresherCache"; + +self.addEventListener("install", function(event) { + event.waitUntil( + Promise.all([ + caches.open(name), + fetch("./sw_clients/refresher_cached.html"), + fetch("./sw_clients/refresher_cached_compressed.html"), + ]).then(function(results) { + var cache = results[0]; + var response = results[1]; + var compressed = results[2]; + return Promise.all([ + cache.put("./sw_clients/refresher.html", response), + cache.put("./sw_clients/refresher_compressed.html", compressed), + ]); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + event.respondWith( + caches + .open(name) + .then(function(cache) { + return cache.match(event.request); + }) + .then(function(response) { + // If this is one of our primary cached responses, then the window + // must have generated the request via a normal window reload. That + // should be detectable in the event.request.cache attribute. + if (response && event.request.cache !== "no-cache") { + dump( + '### ### FetchEvent.request.cache is "' + + event.request.cache + + '" instead of expected "no-cache"\n' + ); + return Response.error(); + } + return response || fetch(event.request); + }) + ); +}); diff --git a/dom/serviceworkers/test/gtest/TestReadWrite.cpp b/dom/serviceworkers/test/gtest/TestReadWrite.cpp new file mode 100644 index 0000000000..fbe71d7108 --- /dev/null +++ b/dom/serviceworkers/test/gtest/TestReadWrite.cpp @@ -0,0 +1,952 @@ +/* -*- 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 "gtest/gtest.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/ServiceWorkerRegistrar.h" +#include "mozilla/dom/ServiceWorkerRegistrarTypes.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsIServiceWorkerManager.h" + +#include "prtime.h" + +using namespace mozilla::dom; +using namespace mozilla::ipc; + +class ServiceWorkerRegistrarTest : public ServiceWorkerRegistrar { + public: + ServiceWorkerRegistrarTest() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); +#else + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); +#endif + MOZ_DIAGNOSTIC_ASSERT(mProfileDir); + } + + nsresult TestReadData() { return ReadData(); } + nsresult TestWriteData() MOZ_NO_THREAD_SAFETY_ANALYSIS { + return WriteData(mData); + } + void TestDeleteData() { DeleteData(); } + + void TestRegisterServiceWorker(const ServiceWorkerRegistrationData& aData) { + mozilla::MonitorAutoLock lock(mMonitor); + RegisterServiceWorkerInternal(aData); + } + + nsTArray<ServiceWorkerRegistrationData>& TestGetData() { return mData; } +}; + +already_AddRefed<nsIFile> GetFile() { + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE)); + return file.forget(); +} + +bool CreateFile(const nsACString& aData) { + nsCOMPtr<nsIFile> file = GetFile(); + + nsCOMPtr<nsIOutputStream> stream; + nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + uint32_t count; + rv = stream->Write(aData.Data(), aData.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (count != aData.Length()) { + return false; + } + + return true; +} + +TEST(ServiceWorkerRegistrar, TestNoFile) +{ + nsCOMPtr<nsIFile> file = GetFile(); + ASSERT_TRUE(file) + << "GetFile must return a nsIFIle"; + + bool exists; + nsresult rv = file->Exists(&exists); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail"; + + if (exists) { + rv = file->Remove(false); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Remove cannot fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestEmptyFile) +{ + ASSERT_TRUE(CreateFile(""_ns)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_NE(NS_OK, rv) << "ReadData() should fail if the file is empty"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestRightVersionFile) +{ + ASSERT_TRUE(CreateFile(nsLiteralCString(SERVICEWORKERREGISTRAR_VERSION "\n"))) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) + << "ReadData() should not fail when the version is correct"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestWrongVersionFile) +{ + ASSERT_TRUE( + CreateFile(nsLiteralCString(SERVICEWORKERREGISTRAR_VERSION "bla\n"))) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_NE(NS_OK, rv) + << "ReadData() should fail when the version is not correct"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestReadData) +{ + nsAutoCString buffer(SERVICEWORKERREGISTRAR_VERSION "\n"); + + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendInt(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + 16); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("true\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.Append(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendInt(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, 16); + buffer.AppendLiteral("\n"); + PRTime ts = PR_Now(); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(1); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("false\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + ASSERT_EQ(false, data[0].navigationPreloadState().enabled()); + ASSERT_STREQ("true", data[0].navigationPreloadState().headerValue().get()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)ts, data[1].lastUpdateTime()); + ASSERT_EQ(true, data[1].navigationPreloadState().enabled()); + ASSERT_STREQ("false", data[1].navigationPreloadState().headerValue().get()); +} + +TEST(ServiceWorkerRegistrar, TestDeleteData) +{ + ASSERT_TRUE(CreateFile("Foobar"_ns)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + swr->TestDeleteData(); + + nsCOMPtr<nsIFile> file = GetFile(); + + bool exists; + nsresult rv = file->Exists(&exists); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail"; + + ASSERT_FALSE(exists) + << "The file should not exist after a DeleteData()."; +} + +TEST(ServiceWorkerRegistrar, TestWriteData) +{ + { + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + for (int i = 0; i < 2; ++i) { + ServiceWorkerRegistrationData reg; + + reg.scope() = nsPrintfCString("https://scope_write_%d.org", i); + reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i); + reg.currentWorkerHandlesFetch() = true; + reg.cacheName() = + NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i)); + reg.updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + reg.currentWorkerInstalledTime() = PR_Now(); + reg.currentWorkerActivatedTime() = PR_Now(); + reg.lastUpdateTime() = PR_Now(); + + nsAutoCString spec; + spec.AppendPrintf("spec write %d", i); + + reg.principal() = mozilla::ipc::ContentPrincipalInfo( + mozilla::OriginAttributes(i % 2), spec, spec, mozilla::Nothing(), + spec); + + swr->TestRegisterServiceWorker(reg); + } + + nsresult rv = swr->TestWriteData(); + ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + for (int i = 0; i < 2; ++i) { + nsAutoCString test; + + ASSERT_EQ(data[i].principal().type(), + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + const mozilla::ipc::ContentPrincipalInfo& cInfo = data[i].principal(); + + mozilla::OriginAttributes attrs(i % 2); + nsAutoCString suffix, expectSuffix; + attrs.CreateSuffix(expectSuffix); + cInfo.attrs().CreateSuffix(suffix); + + ASSERT_STREQ(expectSuffix.get(), suffix.get()); + + test.AppendPrintf("https://scope_write_%d.org", i); + ASSERT_STREQ(test.get(), cInfo.spec().get()); + + test.Truncate(); + test.AppendPrintf("https://scope_write_%d.org", i); + ASSERT_STREQ(test.get(), data[i].scope().get()); + + test.Truncate(); + test.AppendPrintf("currentWorkerURL write %d", i); + ASSERT_STREQ(test.get(), data[i].currentWorkerURL().get()); + + ASSERT_EQ(true, data[i].currentWorkerHandlesFetch()); + + test.Truncate(); + test.AppendPrintf("cacheName write %d", i); + ASSERT_STREQ(test.get(), NS_ConvertUTF16toUTF8(data[i].cacheName()).get()); + + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[i].updateViaCache()); + + ASSERT_NE((int64_t)0, data[i].currentWorkerInstalledTime()); + ASSERT_NE((int64_t)0, data[i].currentWorkerActivatedTime()); + ASSERT_NE((int64_t)0, data[i].lastUpdateTime()); + } +} + +TEST(ServiceWorkerRegistrar, TestVersion2Migration) +{ + nsAutoCString buffer( + "2" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral( + "spec 0\nhttps://scope_0.org\nscriptSpec 0\ncurrentWorkerURL " + "0\nactiveCache 0\nwaitingCache 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_1.org\nscriptSpec 1\ncurrentWorkerURL " + "1\nactiveCache 1\nwaitingCache 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("activeCache 0", + NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("activeCache 1", + NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion3Migration) +{ + nsAutoCString buffer( + "3" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral( + "spec 0\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion4Migration) +{ + nsAutoCString buffer( + "4" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral( + "https://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "https://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + // default is true + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + // default is true + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion5Migration) +{ + nsAutoCString buffer( + "5" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion6Migration) +{ + nsAutoCString buffer( + "6" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendInt(nsIRequest::LOAD_NORMAL, 16); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendInt(nsIRequest::VALIDATE_ALWAYS, 16); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion7Migration) +{ + nsAutoCString buffer( + "7" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendInt(nsIRequest::LOAD_NORMAL, 16); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendInt(nsIRequest::VALIDATE_ALWAYS, 16); + buffer.AppendLiteral("\n"); + PRTime ts = PR_Now(); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)ts, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestDedupeRead) +{ + nsAutoCString buffer( + "3" + "\n"); + + // unique entries + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral( + "spec 0\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + // dupe entries + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral( + "spec 2\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 3\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestDedupeWrite) +{ + { + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + for (int i = 0; i < 2; ++i) { + ServiceWorkerRegistrationData reg; + + reg.scope() = "https://scope_write.dedupe"_ns; + reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i); + reg.currentWorkerHandlesFetch() = true; + reg.cacheName() = + NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i)); + reg.updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + nsAutoCString spec; + spec.AppendPrintf("spec write dedupe/%d", i); + + reg.principal() = mozilla::ipc::ContentPrincipalInfo( + mozilla::OriginAttributes(false), spec, spec, mozilla::Nothing(), + spec); + + swr->TestRegisterServiceWorker(reg); + } + + nsresult rv = swr->TestWriteData(); + ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + // Duplicate entries should be removed. + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)1, data.Length()) << "1 entry should be found"; + + ASSERT_EQ(data[0].principal().type(), + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + const mozilla::ipc::ContentPrincipalInfo& cInfo = data[0].principal(); + + mozilla::OriginAttributes attrs(false); + nsAutoCString suffix, expectSuffix; + attrs.CreateSuffix(expectSuffix); + cInfo.attrs().CreateSuffix(suffix); + + // Last entry passed to RegisterServiceWorkerInternal() should overwrite + // previous values. So expect "1" in values here. + ASSERT_STREQ(expectSuffix.get(), suffix.get()); + ASSERT_STREQ("https://scope_write.dedupe", cInfo.spec().get()); + ASSERT_STREQ("https://scope_write.dedupe", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL write 1", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName write 1", + NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + + int rv = RUN_ALL_TESTS(); + return rv; +} diff --git a/dom/serviceworkers/test/gtest/moz.build b/dom/serviceworkers/test/gtest/moz.build new file mode 100644 index 0000000000..99e2945332 --- /dev/null +++ b/dom/serviceworkers/test/gtest/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES = [ + "TestReadWrite.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/serviceworkers/test/gzip_redirect_worker.js b/dom/serviceworkers/test/gzip_redirect_worker.js new file mode 100644 index 0000000000..b66df51478 --- /dev/null +++ b/dom/serviceworkers/test/gzip_redirect_worker.js @@ -0,0 +1,15 @@ +self.addEventListener("fetch", function(event) { + if (!event.request.url.endsWith("sw_clients/does_not_exist.html")) { + return; + } + + event.respondWith( + new Response("", { + status: 301, + statusText: "Moved Permanently", + headers: { + Location: "refresher_compressed.html", + }, + }) + ); +}); diff --git a/dom/serviceworkers/test/header_checker.sjs b/dom/serviceworkers/test/header_checker.sjs new file mode 100644 index 0000000000..7061041039 --- /dev/null +++ b/dom/serviceworkers/test/header_checker.sjs @@ -0,0 +1,9 @@ +function handleRequest(request, response) { + if (request.getHeader("Service-Worker") === "script") { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/javascript"); + response.write("// empty"); + } else { + response.setStatusLine("1.1", 404, "Not Found"); + } +} diff --git a/dom/serviceworkers/test/hello.html b/dom/serviceworkers/test/hello.html new file mode 100644 index 0000000000..97eb03c902 --- /dev/null +++ b/dom/serviceworkers/test/hello.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + </head> + <body> + Hello. + </body> +<html> diff --git a/dom/serviceworkers/test/importscript.sjs b/dom/serviceworkers/test/importscript.sjs new file mode 100644 index 0000000000..e075eadd87 --- /dev/null +++ b/dom/serviceworkers/test/importscript.sjs @@ -0,0 +1,11 @@ +function handleRequest(request, response) { + if (request.queryString == "clearcounter") { + setState("counter", ""); + } else if (!getState("counter")) { + response.setHeader("Content-Type", "application/javascript", false); + response.write("callByScript();"); + setState("counter", "1"); + } else { + response.write("no cache no party!"); + } +} diff --git a/dom/serviceworkers/test/importscript_worker.js b/dom/serviceworkers/test/importscript_worker.js new file mode 100644 index 0000000000..6d639e792c --- /dev/null +++ b/dom/serviceworkers/test/importscript_worker.js @@ -0,0 +1,46 @@ +var counter = 0; +function callByScript() { + ++counter; +} + +// Use multiple scripts in this load to verify we support that case correctly. +// See bug 1249351 for a case where we broke this. +importScripts("lorem_script.js", "importscript.sjs"); + +importScripts("importscript.sjs"); + +var missingScriptFailed = false; +try { + importScripts(["there-is-nothing-here.js"]); +} catch (e) { + missingScriptFailed = true; +} + +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + if (!missingScriptFailed) { + res[0].postMessage("KO"); + } + + try { + // new unique script should fail + importScripts(["importscript.sjs?unique=true"]); + res[0].postMessage("KO"); + return; + } catch (ex) {} + + try { + // duplicate script previously offlined should succeed + importScripts(["importscript.sjs"]); + } catch (ex) { + res[0].postMessage("KO"); + return; + } + + res[0].postMessage(counter == 3 ? "OK" : "KO"); + }); +}; diff --git a/dom/serviceworkers/test/install_event_error_worker.js b/dom/serviceworkers/test/install_event_error_worker.js new file mode 100644 index 0000000000..e4208705b3 --- /dev/null +++ b/dom/serviceworkers/test/install_event_error_worker.js @@ -0,0 +1,9 @@ +// Worker that errors on receiving an install event. +oninstall = function(e) { + e.waitUntil( + new Promise(function(resolve, reject) { + undefined.doSomething; + resolve(); + }) + ); +}; diff --git a/dom/serviceworkers/test/install_event_worker.js b/dom/serviceworkers/test/install_event_worker.js new file mode 100644 index 0000000000..1f0815c8f0 --- /dev/null +++ b/dom/serviceworkers/test/install_event_worker.js @@ -0,0 +1,3 @@ +oninstall = function(e) { + dump("Got install event\n"); +}; diff --git a/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js b/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js new file mode 100644 index 0000000000..ccc74bc895 --- /dev/null +++ b/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js @@ -0,0 +1,7 @@ +onfetch = e => { + const url = new URL(e.request.url).searchParams.get("respondWith"); + + if (url) { + e.respondWith(fetch(url)); + } +}; diff --git a/dom/serviceworkers/test/isolated/README.md b/dom/serviceworkers/test/isolated/README.md new file mode 100644 index 0000000000..2b462385af --- /dev/null +++ b/dom/serviceworkers/test/isolated/README.md @@ -0,0 +1,19 @@ +This directory contains tests that are flaky when run with other tests +but that we don't want to disable and where it's not trivial to make +the tests not flaky at this time, but we have a plan to fix them via +systemic fixes that are improving the codebase rather than hacking a +test until it works. + +This directory and ugly hack structure needs to exist because of +multi-e10s propagation races that will go away when we finish +implementing the multi-e10s overhaul for ServiceWorkers. Most +specifically, unregister() calls need to propagate across all +content processes. There are fixes on bug 1318142, but they're +ugly and complicate things. + +Specific test notes and rationalizations: +- multi-e10s-update: This test relies on there being no registrations + existing at its start. The preceding test that induces the breakage + (`browser_force_refresh.js`) was made to clean itself up, but the + unregister() race issue is not easily/cleanly hacked around and this + test will itself become moot when the multi-e10s changes land. diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini b/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini new file mode 100644 index 0000000000..bb913e2583 --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + file_multie10s_update.html + server_multie10s_update.sjs + +[browser_multie10s_update.js] +skip-if = true # bug 1429794 is to re-enable diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js b/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js new file mode 100644 index 0000000000..4b9a2b9ca4 --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js @@ -0,0 +1,143 @@ +"use strict"; + +// Testing if 2 child processes are correctly managed when they both try to do +// an SW update. + +const BASE_URI = + "http://mochi.test:8888/browser/dom/serviceworkers/test/isolated/multi-e10s-update/"; + +add_task(async function test_update() { + info("Setting the prefs to having multi-e10s enabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 4], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + let url = BASE_URI + "file_multie10s_update.html"; + + info("Creating the first tab..."); + let tab1 = BrowserTestUtils.addTab(gBrowser, url); + let browser1 = gBrowser.getBrowserForTab(tab1); + await BrowserTestUtils.browserLoaded(browser1); + + info("Creating the second tab..."); + let tab2 = BrowserTestUtils.addTab(gBrowser, url); + let browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + let sw = BASE_URI + "server_multie10s_update.sjs"; + + info("Let's make sure there are no existing registrations..."); + let existingCount = await SpecialPowers.spawn(browser1, [], async function() { + const regs = await content.navigator.serviceWorker.getRegistrations(); + return regs.length; + }); + is(existingCount, 0, "Previous tests should have cleaned up!"); + + info("Let's start the test..."); + /* eslint-disable no-shadow */ + let status = await SpecialPowers.spawn(browser1, [sw], function(url) { + // Let the SW be served immediately once by triggering a relase immediately. + // We don't need to await this. We do this from a frame script because + // it has fetch. + content.fetch(url + "?release"); + + // Registration of the SW + return ( + content.navigator.serviceWorker + .register(url) + + // Activation + .then(function(r) { + content.registration = r; + return new content.window.Promise(resolve => { + let worker = r.installing; + worker.addEventListener("statechange", () => { + if (worker.state === "installed") { + resolve(true); + } + }); + }); + }) + + // Waiting for the result. + .then(() => { + return new content.window.Promise(resolveResults => { + // Once both updates have been issued and a single update has failed, we + // can tell the .sjs to release a single copy of the SW script. + let updateCount = 0; + const uc = new content.window.BroadcastChannel("update"); + // This promise tracks the updates tally. + const updatesIssued = new Promise(resolveUpdatesIssued => { + uc.onmessage = function(e) { + updateCount++; + console.log("got update() number", updateCount); + if (updateCount === 2) { + resolveUpdatesIssued(); + } + }; + }); + + let results = []; + const rc = new content.window.BroadcastChannel("result"); + // This promise resolves when an update has failed. + const oneFailed = new Promise(resolveOneFailed => { + rc.onmessage = function(e) { + console.log("got result", e.data); + results.push(e.data); + if (e.data === 1) { + resolveOneFailed(); + } + if (results.length != 2) { + return; + } + + resolveResults(results[0] + results[1]); + }; + }); + + Promise.all([updatesIssued, oneFailed]).then(() => { + console.log("releasing update"); + content.fetch(url + "?release").catch(ex => { + console.error("problem releasing:", ex); + }); + }); + + // Let's inform the tabs. + const sc = new content.window.BroadcastChannel("start"); + sc.postMessage("go"); + }); + }) + ); + }); + /* eslint-enable no-shadow */ + + if (status == 0) { + ok(false, "both succeeded. This is wrong."); + } else if (status == 1) { + ok(true, "one succeded, one failed. This is good."); + } else { + ok(false, "both failed. This is definitely wrong."); + } + + // let's clean up the registration and get the fetch count. The count + // should be 1 for the initial fetch and 1 for the update. + /* eslint-disable no-shadow */ + const count = await SpecialPowers.spawn(browser1, [sw], async function(url) { + // We stored the registration on the frame script's wrapper, hence directly + // accesss content without using wrappedJSObject. + await content.registration.unregister(); + const { count } = await content + .fetch(url + "?get-and-clear-count") + .then(r => r.json()); + return count; + }); + /* eslint-enable no-shadow */ + is(count, 2, "SW should have been fetched only twice"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html b/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html new file mode 100644 index 0000000000..d611b01b59 --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html @@ -0,0 +1,40 @@ +<html> +<body> +<script> + +var bc = new BroadcastChannel('start'); +bc.onmessage = function(e) { + // This message is not for us. + if (e.data != 'go') { + return; + } + + // It can happen that we don't have the registrations yet. Let's try with a + // timeout. + function proceed() { + return navigator.serviceWorker.getRegistrations().then(regs => { + if (!regs.length) { + setTimeout(proceed, 200); + return; + } + + bc = new BroadcastChannel('result'); + regs[0].update().then(() => { + bc.postMessage(0); + }, () => { + bc.postMessage(1); + }); + + // Tell the coordinating frame script that we've kicked off our update + // call so that the SW script can be released once both instances of us + // have triggered update() and 1 has failed. + const blockingChannel = new BroadcastChannel('update'); + blockingChannel.postMessage(true); + }); + } + + proceed(); +} +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs b/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs new file mode 100644 index 0000000000..154f8a3ecd --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs @@ -0,0 +1,100 @@ +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) { + x = { + data, + QueryInterface(iid) { + return this; + }, + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function(x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +function completeBlockingRequest(response) { + response.write("42"); + response.finish(); +} + +// This stores the response that's currently blocking, or true if the release +// got here before the blocking request. +const BLOCKING_KEY = "multie10s-update-release"; +// This tracks the number of blocking requests we received up to this point in +// time. This value will be cleared when fetched. It's on the caller to make +// sure that all the blocking requests that might occurr have already occurred. +const COUNT_KEY = "multie10s-update-count"; + +/** + * Serve a request that will only be completed when the ?release variant of this + * .sjs is fetched. This allows us to avoid using a timer, which slows down the + * tests and is brittle under slow hardware. + */ +function handleBlockingRequest(request, response) { + response.processAsync(); + response.setHeader("Content-Type", "application/javascript", false); + + const existingCount = getGlobalState(COUNT_KEY) || 0; + setGlobalState(existingCount + 1, COUNT_KEY); + + const alreadyReleased = getGlobalState(BLOCKING_KEY); + if (alreadyReleased === true) { + completeBlockingRequest(response); + setGlobalState(null, BLOCKING_KEY); + } else if (alreadyReleased) { + // If we've got another response stacked up, this means something is wrong + // with the test. The count mechanism will detect this, so just let this + // one through so we fail fast rather than hanging. + dump("we got multiple blocking requests stacked up!!\n"); + completeBlockingRequest(response); + } else { + setGlobalState(response, BLOCKING_KEY); + } +} + +function handleReleaseRequest(request, response) { + const blockingResponse = getGlobalState(BLOCKING_KEY); + if (blockingResponse) { + completeBlockingRequest(blockingResponse); + setGlobalState(null, BLOCKING_KEY); + } else { + setGlobalState(true, BLOCKING_KEY); + } + + response.setHeader("Content-Type", "application/json", false); + response.write(JSON.stringify({ released: true })); +} + +function handleCountRequest(request, response) { + const count = getGlobalState(COUNT_KEY) || 0; + // --verify requires that we clear this so the test can be re-run. + setGlobalState(0, COUNT_KEY); + + response.setHeader("Content-Type", "application/json", false); + response.write(JSON.stringify({ count })); +} + +Components.utils.importGlobalProperties(["URLSearchParams"]); +function handleRequest(request, response) { + dump( + "server_multie10s_update.sjs: processing request for " + + request.path + + "?" + + request.queryString + + "\n" + ); + const query = new URLSearchParams(request.queryString); + if (query.has("release")) { + handleReleaseRequest(request, response); + } else if (query.has("get-and-clear-count")) { + handleCountRequest(request, response); + } else { + handleBlockingRequest(request, response); + } +} diff --git a/dom/serviceworkers/test/lazy_worker.js b/dom/serviceworkers/test/lazy_worker.js new file mode 100644 index 0000000000..08a0ec13f7 --- /dev/null +++ b/dom/serviceworkers/test/lazy_worker.js @@ -0,0 +1,8 @@ +onactivate = function(event) { + var promise = new Promise(function(res) { + setTimeout(function() { + res(); + }, 500); + }); + event.waitUntil(promise); +}; diff --git a/dom/serviceworkers/test/lorem_script.js b/dom/serviceworkers/test/lorem_script.js new file mode 100644 index 0000000000..bc8f3c8085 --- /dev/null +++ b/dom/serviceworkers/test/lorem_script.js @@ -0,0 +1,8 @@ +var lorem_str = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum +dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, +sunt in culpa qui officia deserunt mollit anim id est laborum. +`; diff --git a/dom/serviceworkers/test/match_all_advanced_worker.js b/dom/serviceworkers/test/match_all_advanced_worker.js new file mode 100644 index 0000000000..d1bc5ab323 --- /dev/null +++ b/dom/serviceworkers/test/match_all_advanced_worker.js @@ -0,0 +1,5 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(clients) { + e.source.postMessage(clients.length); + }); +}; diff --git a/dom/serviceworkers/test/match_all_client/match_all_client_id.html b/dom/serviceworkers/test/match_all_client/match_all_client_id.html new file mode 100644 index 0000000000..7ac6fc9d05 --- /dev/null +++ b/dom/serviceworkers/test/match_all_client/match_all_client_id.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1139425 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + swr.active.postMessage("Start"); + }); + } + + navigator.serviceWorker.onmessage = function(msg) { + // worker message; + testWindow.postMessage(msg.data, "*"); + window.close(); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/match_all_client_id_worker.js b/dom/serviceworkers/test/match_all_client_id_worker.js new file mode 100644 index 0000000000..e8e51a00e7 --- /dev/null +++ b/dom/serviceworkers/test/match_all_client_id_worker.js @@ -0,0 +1,28 @@ +onmessage = function(e) { + dump("MatchAllClientIdWorker:" + e.data + "\n"); + var id = []; + var iterations = 5; + var counter = 0; + + for (var i = 0; i < iterations; i++) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + client = res[0]; + id[counter] = client.id; + counter++; + if (counter >= iterations) { + var response = true; + for (var index = 1; index < iterations; index++) { + if (id[0] != id[index]) { + response = false; + break; + } + } + client.postMessage(response); + } + }); + } +}; diff --git a/dom/serviceworkers/test/match_all_clients/match_all_controlled.html b/dom/serviceworkers/test/match_all_clients/match_all_controlled.html new file mode 100644 index 0000000000..35f064815d --- /dev/null +++ b/dom/serviceworkers/test/match_all_clients/match_all_controlled.html @@ -0,0 +1,83 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - controlled page</title> +<script class="testbody" type="text/javascript"> + var re = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + var frameType = "none"; + var testWindow = parent; + + if (parent != window) { + frameType = "nested"; + } else if (opener) { + frameType = "auxiliary"; + testWindow = opener; + } else if (parent == window) { + frameType = "top-level"; + } else { + postResult(false, "Unexpected frameType"); + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + // Send a message to our SW that will cause it to do clients.matchAll() + // and send a message *to each client about themselves* (rather than + // replying directly to us with all the clients it finds). + swr.active.postMessage("Start"); + }); + } + + function postResult(result, msg) { + response = { + result, + message: msg + }; + + testWindow.postMessage(response, "*"); + } + + navigator.serviceWorker.onmessage = async function(msg) { + // ## Verify the contents of the SW's serialized rep of our client info. + // Clients are opaque identifiers at a spec level, but we want to verify + // that they are UUID's *without wrapping "{}" characters*. + result = re.test(msg.data.id); + postResult(result, "Client id test"); + + result = msg.data.url == window.location; + postResult(result, "Client url test"); + + result = msg.data.visibilityState === document.visibilityState; + postResult(result, "Client visibility test. expected=" +document.visibilityState); + + result = msg.data.focused === document.hasFocus(); + postResult(result, "Client focus test. expected=" + document.hasFocus()); + + result = msg.data.frameType === frameType; + postResult(result, "Client frameType test. expected=" + frameType); + + result = msg.data.type === "window"; + postResult(result, "Client type test. expected=window"); + + // ## Verify the FetchEvent.clientId + // In bug 1446225 it turned out we provided UUID's wrapped with {}'s. We + // now also get coverage by forcing our clients.get() to forbid UUIDs + // with that form. + + const clientIdResp = await fetch('clientId'); + const fetchClientId = await clientIdResp.text(); + result = re.test(fetchClientId); + postResult(result, "Fetch clientId test"); + + postResult(true, "DONE"); + window.close(); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/match_all_properties_worker.js b/dom/serviceworkers/test/match_all_properties_worker.js new file mode 100644 index 0000000000..84156c93e0 --- /dev/null +++ b/dom/serviceworkers/test/match_all_properties_worker.js @@ -0,0 +1,28 @@ +onfetch = function(e) { + if (/\/clientId$/.test(e.request.url)) { + e.respondWith(new Response(e.clientId)); + return; + } +}; + +onmessage = function(e) { + dump("MatchAllPropertiesWorker:" + e.data + "\n"); + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + for (i = 0; i < res.length; i++) { + client = res[i]; + response = { + type: client.type, + id: client.id, + url: client.url, + visibilityState: client.visibilityState, + focused: client.focused, + frameType: client.frameType, + }; + client.postMessage(response); + } + }); +}; diff --git a/dom/serviceworkers/test/match_all_worker.js b/dom/serviceworkers/test/match_all_worker.js new file mode 100644 index 0000000000..3eddbe1191 --- /dev/null +++ b/dom/serviceworkers/test/match_all_worker.js @@ -0,0 +1,10 @@ +function loop() { + self.clients.matchAll().then(function(result) { + setTimeout(loop, 0); + }); +} + +onactivate = function(e) { + // spam matchAll until the worker is closed. + loop(); +}; diff --git a/dom/serviceworkers/test/message_posting_worker.js b/dom/serviceworkers/test/message_posting_worker.js new file mode 100644 index 0000000000..26db997759 --- /dev/null +++ b/dom/serviceworkers/test/message_posting_worker.js @@ -0,0 +1,8 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + res[0].postMessage(e.data); + }); +}; diff --git a/dom/serviceworkers/test/message_receiver.html b/dom/serviceworkers/test/message_receiver.html new file mode 100644 index 0000000000..82cb587c72 --- /dev/null +++ b/dom/serviceworkers/test/message_receiver.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + }; +</script> diff --git a/dom/serviceworkers/test/mochitest-common.ini b/dom/serviceworkers/test/mochitest-common.ini new file mode 100644 index 0000000000..280273a7b4 --- /dev/null +++ b/dom/serviceworkers/test/mochitest-common.ini @@ -0,0 +1,347 @@ +[DEFAULT] +tags = condprof +support-files = + abrupt_completion_worker.js + worker.js + worker2.js + worker3.js + fetch_event_worker.js + parse_error_worker.js + activate_event_error_worker.js + install_event_worker.js + install_event_error_worker.js + simpleregister/index.html + simpleregister/ready.html + controller/index.html + unregister/index.html + unregister/unregister.html + workerUpdate/update.html + sw_clients/simple.html + sw_clients/service_worker_controlled.html + match_all_worker.js + match_all_advanced_worker.js + worker_unregister.js + worker_update.js + message_posting_worker.js + fetch/index.html + fetch/fetch_worker_script.js + fetch/fetch_tests.js + fetch/deliver-gzip.sjs + fetch/redirect.sjs + fetch/real-file.txt + fetch/cookie/cookie_test.js + fetch/cookie/register.html + fetch/cookie/unregister.html + fetch/hsts/hsts_test.js + fetch/hsts/embedder.html + fetch/hsts/image.html + fetch/hsts/image-20px.png + fetch/hsts/image-40px.png + fetch/hsts/realindex.html + fetch/hsts/register.html + fetch/hsts/register.html^headers^ + fetch/hsts/unregister.html + fetch/https/index.html + fetch/https/register.html + fetch/https/unregister.html + fetch/https/https_test.js + fetch/https/clonedresponse/index.html + fetch/https/clonedresponse/register.html + fetch/https/clonedresponse/unregister.html + fetch/https/clonedresponse/https_test.js + fetch/imagecache/image-20px.png + fetch/imagecache/image-40px.png + fetch/imagecache/imagecache_test.js + fetch/imagecache/index.html + fetch/imagecache/postmortem.html + fetch/imagecache/register.html + fetch/imagecache/unregister.html + fetch/imagecache-maxage/index.html + fetch/imagecache-maxage/image-20px.png + fetch/imagecache-maxage/image-40px.png + fetch/imagecache-maxage/maxage_test.js + fetch/imagecache-maxage/register.html + fetch/imagecache-maxage/unregister.html + fetch/importscript-mixedcontent/register.html + fetch/importscript-mixedcontent/unregister.html + fetch/importscript-mixedcontent/https_test.js + fetch/interrupt.sjs + fetch/origin/index.sjs + fetch/origin/index-to-https.sjs + fetch/origin/realindex.html + fetch/origin/realindex.html^headers^ + fetch/origin/register.html + fetch/origin/unregister.html + fetch/origin/origin_test.js + fetch/origin/https/index-https.sjs + fetch/origin/https/realindex.html + fetch/origin/https/realindex.html^headers^ + fetch/origin/https/register.html + fetch/origin/https/unregister.html + fetch/origin/https/origin_test.js + fetch/requesturl/index.html + fetch/requesturl/redirect.sjs + fetch/requesturl/redirector.html + fetch/requesturl/register.html + fetch/requesturl/requesturl_test.js + fetch/requesturl/secret.html + fetch/requesturl/unregister.html + fetch/sandbox/index.html + fetch/sandbox/intercepted_index.html + fetch/sandbox/register.html + fetch/sandbox/unregister.html + fetch/sandbox/sandbox_test.js + fetch/upgrade-insecure/upgrade-insecure_test.js + fetch/upgrade-insecure/embedder.html + fetch/upgrade-insecure/embedder.html^headers^ + fetch/upgrade-insecure/image.html + fetch/upgrade-insecure/image-20px.png + fetch/upgrade-insecure/image-40px.png + fetch/upgrade-insecure/realindex.html + fetch/upgrade-insecure/register.html + fetch/upgrade-insecure/unregister.html + match_all_properties_worker.js + match_all_clients/match_all_controlled.html + test_serviceworker_interfaces.js + serviceworker_wrapper.js + message_receiver.html + serviceworker_not_sharedworker.js + match_all_client/match_all_client_id.html + match_all_client_id_worker.js + source_message_posting_worker.js + scope/scope_worker.js + redirect_serviceworker.sjs + importscript.sjs + importscript_worker.js + bug1151916_worker.js + bug1151916_driver.html + bug1240436_worker.js + notificationclick.html + notificationclick-otherwindow.html + notificationclick.js + notificationclick_focus.html + notificationclick_focus.js + notificationclose.html + notificationclose.js + worker_updatefoundevent.js + worker_updatefoundevent2.js + updatefoundevent.html + empty.html + empty.js + notification_constructor_error.js + notification_get_sw.js + notification/register.html + notification/unregister.html + notification/listener.html + notification_alt/register.html + notification_alt/unregister.html + sanitize/frame.html + sanitize/register.html + sanitize/example_check_and_unregister.html + sanitize_worker.js + streamfilter_server.sjs + streamfilter_worker.js + swa/worker_scope_different.js + swa/worker_scope_different.js^headers^ + swa/worker_scope_different2.js + swa/worker_scope_different2.js^headers^ + swa/worker_scope_precise.js + swa/worker_scope_precise.js^headers^ + swa/worker_scope_too_deep.js + swa/worker_scope_too_deep.js^headers^ + swa/worker_scope_too_narrow.js + swa/worker_scope_too_narrow.js^headers^ + claim_oninstall_worker.js + claim_worker_1.js + claim_worker_2.js + claim_clients/client.html + force_refresh_worker.js + sw_clients/refresher.html + sw_clients/refresher_compressed.html + sw_clients/refresher_compressed.html^headers^ + sw_clients/refresher_cached.html + sw_clients/refresher_cached_compressed.html + sw_clients/refresher_cached_compressed.html^headers^ + strict_mode_warning.js + skip_waiting_installed_worker.js + skip_waiting_scope/index.html + thirdparty/iframe1.html + thirdparty/iframe2.html + thirdparty/register.html + thirdparty/unregister.html + thirdparty/sw.js + thirdparty/worker.js + register_https.html + gzip_redirect_worker.js + sw_clients/navigator.html + eval_worker.js + test_eval_allowed.html^headers^ + opaque_intercept_worker.js + notify_loaded.js + fetch/plugin/worker.js + fetch/plugin/plugins.html + eventsource/* + sw_clients/file_blob_upload_frame.html + redirect_post.sjs + xslt_worker.js + xslt/* + unresolved_fetch_worker.js + header_checker.sjs + openWindow_worker.js + redirect.sjs + open_window/client.sjs + lorem_script.js + file_blob_response_worker.js + file_js_cache_cleanup.js + file_js_cache.html + file_js_cache_with_sri.html + file_js_cache.js + file_js_cache_save_after_load.html + file_js_cache_save_after_load.js + file_js_cache_syntax_error.html + file_js_cache_syntax_error.js + !/dom/security/test/cors/file_CrossSiteXHR_server.sjs + !/dom/notification/test/mochitest/MockServices.js + !/dom/notification/test/mochitest/NotificationTest.js + blocking_install_event_worker.js + sw_bad_mime_type.js + sw_bad_mime_type.js^headers^ + error_reporting_helpers.js + fetch.js + hello.html + create_another_sharedWorker.html + sharedWorker_fetch.js + async_waituntil_worker.js + lazy_worker.js + nofetch_handler_worker.js + service_worker.js + service_worker_client.html + utils.js + sw_storage_not_allow.js + update_worker.sjs + self_update_worker.sjs + !/dom/events/test/event_leak_utils.js + onmessageerror_worker.js + +[test_abrupt_completion.html] +skip-if = + os == 'linux' #Bug 1615164 + win10_2004 # Bug 1615164 +[test_bug1151916.html] +[test_bug1240436.html] +[test_bug1408734.html] +[test_claim.html] +[test_claim_oninstall.html] +[test_controller.html] +[test_cross_origin_url_after_redirect.html] +[test_devtools_bypass_serviceworker.html] +[test_empty_serviceworker.html] +[test_error_reporting.html] +skip-if = serviceworker_e10s +[test_escapedSlashes.html] +[test_eval_allowed.html] +[test_event_listener_leaks.html] +skip-if = (os == "win" && processor == "aarch64") #bug 1535784 +[test_fetch_event.html] +skip-if = debug # Bug 1262224 +[test_fetch_event_with_thirdpartypref.html] +skip-if = debug # Bug 1262224 +[test_fetch_integrity.html] +skip-if = serviceworker_e10s +support-files = console_monitor.js +[test_file_blob_response.html] +[test_file_blob_upload.html] +[test_force_refresh.html] +[test_gzip_redirect.html] +[test_hsts_upgrade_intercept.html] +skip-if = + win10_2004 && !debug # Bug 1717091 + win11_2009 && !debug # Bug 1797751 + os == "linux" && bits == 64 && debug # Bug 1749068 + apple_catalina && !debug # Bug 1717091 +scheme = https +[test_imagecache.html] +[test_imagecache_max_age.html] +skip-if = + os == 'linux' && bits == 64 && !debug && asan && os_version == '18.04' # Bug 1585668 +[test_importscript.html] +[test_install_event.html] +[test_install_event_gc.html] +skip-if = xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object +[test_installation_simple.html] +[test_match_all.html] +[test_match_all_advanced.html] +[test_match_all_client_id.html] +skip-if = toolkit == 'android' && !is_fennec +[test_match_all_client_properties.html] +skip-if = toolkit == 'android' && !is_fennec +[test_navigationPreload_disable_crash.html] +scheme = https +skip-if = + os == "linux" && bits == 64 && debug # Bug 1749068 +[test_navigator.html] +[test_not_intercept_plugin.html] +skip-if = serviceworker_e10s # leaks InterceptedHttpChannel and others things +[test_notification_constructor_error.html] +[test_notification_get.html] +skip-if = xorigin # Bug 1792790 +[test_notification_openWindow.html] +skip-if = + toolkit == 'android' && !is_fennec # Bug 1620052 + xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object +support-files = notification_openWindow_worker.js file_notification_openWindow.html +tags = openwindow +[test_notificationclick.html] +skip-if = xorigin # Bug 1792790 +[test_notificationclick_focus.html] +skip-if = xorigin # Bug 1792790 +[test_notificationclick-otherwindow.html] +skip-if = xorigin # Bug 1792790 +[test_notificationclose.html] +skip-if = xorigin # Bug 1792790 +[test_onmessageerror.html] +skip-if = xorigin # Hangs with no error log +[test_opaque_intercept.html] +[test_origin_after_redirect.html] +[test_origin_after_redirect_cached.html] +[test_origin_after_redirect_to_https.html] +[test_origin_after_redirect_to_https_cached.html] +[test_post_message.html] +[test_post_message_advanced.html] +[test_post_message_source.html] +[test_register_base.html] +[test_register_https_in_http.html] +[test_sandbox_intercept.html] +[test_scopes.html] +[test_script_loader_intercepted_js_cache.html] +skip-if = serviceworker_e10s +[test_sanitize.html] +[test_serviceworker.html] +[test_service_worker_allowed.html] +[test_serviceworker_header.html] +[test_serviceworker_interfaces.html] +[test_serviceworker_not_sharedworker.html] +[test_skip_waiting.html] +[test_streamfilter.html] +[test_strict_mode_warning.html] +[test_third_party_iframes.html] +support-files = + window_party_iframes.html +[test_unregister.html] +[test_unresolved_fetch_interception.html] +skip-if = verify + serviceworker_e10s +[test_workerUnregister.html] +[test_workerUpdate.html] +[test_workerupdatefoundevent.html] +[test_xslt.html] +[test_async_waituntil.html] +[test_worker_reference_gc_timeout.html] +[test_nofetch_handler.html] +[test_bad_script_cache.html] +[test_file_upload.html] +skip-if = toolkit == 'android' #Bug 1430182 +support-files = script_file_upload.js sw_file_upload.js server_file_upload.sjs +[test_self_update_worker.html] +skip-if = serviceworker_e10s + (toolkit == 'android' && !is_fennec) diff --git a/dom/serviceworkers/test/mochitest-dFPI.ini b/dom/serviceworkers/test/mochitest-dFPI.ini new file mode 100644 index 0000000000..37d6171837 --- /dev/null +++ b/dom/serviceworkers/test/mochitest-dFPI.ini @@ -0,0 +1,11 @@ +[DEFAULT] +# Enable dFPI(cookieBehavior 5) for service worker tests. +prefs = + network.cookie.cookieBehavior=5 +tags = serviceworker-dfpi +# We disable service workers for third-party contexts when dFPI is enabled. So, +# we disable xorigin tests for dFPI. +skip-if = xorigin +dupe-manifest = true + +[include:mochitest-common.ini] diff --git a/dom/serviceworkers/test/mochitest.ini b/dom/serviceworkers/test/mochitest.ini new file mode 100644 index 0000000000..49b6c91201 --- /dev/null +++ b/dom/serviceworkers/test/mochitest.ini @@ -0,0 +1,35 @@ +[DEFAULT] +# Mochitests are executed in iframes. Several ServiceWorker tests use iframes +# too. The result is that we have nested iframes. CookieBehavior 4 +# (BEHAVIOR_REJECT_TRACKER) doesn't grant storage access permission to nested +# iframes because trackers could use them to follow users across sites. Let's +# use cookieBehavior 0 (BEHAVIOR_ACCEPT) here. +prefs = + network.cookie.cookieBehavior=0 +dupe-manifest = true +tags = condprof + +# Following tests are not working currently when dFPI is enabled. So, we put +# these tests here instead of mochitest-common.ini so that these tests won't run +# when dFPI is enabled. +[test_cookie_fetch.html] +[test_csp_upgrade-insecure_intercept.html] +[test_eventsource_intercept.html] +[test_https_fetch.html] +skip-if = condprof #: timed out +[test_https_fetch_cloned_response.html] +[test_https_origin_after_redirect.html] +[test_https_origin_after_redirect_cached.html] +skip-if = condprof #: timed out +[test_https_synth_fetch_from_cached_sw.html] +[test_importscript_mixedcontent.html] +tags = mcb +[test_openWindow.html] +skip-if = + toolkit == 'android' && !is_fennec # Bug 1620052 + xorigin # Bug 1792790 + condprof #: timed out +tags = openwindow +[test_sanitize_domain.html] + +[include:mochitest-common.ini] diff --git a/dom/serviceworkers/test/navigationPreload_page.html b/dom/serviceworkers/test/navigationPreload_page.html new file mode 100644 index 0000000000..39d4a79378 --- /dev/null +++ b/dom/serviceworkers/test/navigationPreload_page.html @@ -0,0 +1 @@ +NavigationPreload diff --git a/dom/serviceworkers/test/network_with_utils.html b/dom/serviceworkers/test/network_with_utils.html new file mode 100644 index 0000000000..63f6b0e796 --- /dev/null +++ b/dom/serviceworkers/test/network_with_utils.html @@ -0,0 +1,14 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="utils.js" type="text/javascript"></script> +</head> +<body> +NETWORK +</body> +</html> diff --git a/dom/serviceworkers/test/nofetch_handler_worker.js b/dom/serviceworkers/test/nofetch_handler_worker.js new file mode 100644 index 0000000000..77ba0c734a --- /dev/null +++ b/dom/serviceworkers/test/nofetch_handler_worker.js @@ -0,0 +1,14 @@ +function handleFetch(event) { + event.respondWith(new Response("intercepted")); +} + +self.oninstall = function(event) { + addEventListener("fetch", handleFetch); + self.onfetch = handleFetch; +}; + +// Bug 1325101. Make sure adding event listeners for other events +// doesn't set the fetch flag. +addEventListener("push", function() {}); +addEventListener("message", function() {}); +addEventListener("non-sw-event", function() {}); diff --git a/dom/serviceworkers/test/notification/listener.html b/dom/serviceworkers/test/notification/listener.html new file mode 100644 index 0000000000..1c6e282ece --- /dev/null +++ b/dom/serviceworkers/test/notification/listener.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - proxy to forward messages from SW to test</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.onmessage = function(msg) { + // worker message; + testWindow.postMessage(msg.data, "*"); + if (msg.data.type == 'finish') { + window.close(); + } + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notification/register.html b/dom/serviceworkers/test/notification/register.html new file mode 100644 index 0000000000..b7df73bede --- /dev/null +++ b/dom/serviceworkers/test/notification/register.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + function done() { + parent.callback(); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) { + dump("Registration failure " + e.message + "\n"); + }); +</script> diff --git a/dom/serviceworkers/test/notification/unregister.html b/dom/serviceworkers/test/notification/unregister.html new file mode 100644 index 0000000000..d5a141f830 --- /dev/null +++ b/dom/serviceworkers/test/notification/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.callback(); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/notification_alt/register.html b/dom/serviceworkers/test/notification_alt/register.html new file mode 100644 index 0000000000..b7df73bede --- /dev/null +++ b/dom/serviceworkers/test/notification_alt/register.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + function done() { + parent.callback(); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) { + dump("Registration failure " + e.message + "\n"); + }); +</script> diff --git a/dom/serviceworkers/test/notification_alt/unregister.html b/dom/serviceworkers/test/notification_alt/unregister.html new file mode 100644 index 0000000000..d5a141f830 --- /dev/null +++ b/dom/serviceworkers/test/notification_alt/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.callback(); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/notification_constructor_error.js b/dom/serviceworkers/test/notification_constructor_error.js new file mode 100644 index 0000000000..644dba480e --- /dev/null +++ b/dom/serviceworkers/test/notification_constructor_error.js @@ -0,0 +1 @@ +new Notification("Hi there"); diff --git a/dom/serviceworkers/test/notification_get_sw.js b/dom/serviceworkers/test/notification_get_sw.js new file mode 100644 index 0000000000..9b7c24f496 --- /dev/null +++ b/dom/serviceworkers/test/notification_get_sw.js @@ -0,0 +1,55 @@ +function postAll(data) { + self.clients.matchAll().then(function(clients) { + if (!clients.length) { + dump( + "***************** NO CLIENTS FOUND! Test messages are being lost *******************\n" + ); + } + clients.forEach(function(client) { + client.postMessage(data); + }); + }); +} + +function ok(a, msg) { + postAll({ type: "status", status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + postAll({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + }); +} + +function done() { + postAll({ type: "finish" }); +} + +onmessage = function(e) { + dump("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% MESSAGE " + e.data + "\n"); + var start; + if (e.data == "create") { + start = registration.showNotification("This is a title"); + } else { + start = Promise.resolve(); + } + + start.then(function() { + dump("CALLING getNotification\n"); + registration.getNotifications().then(function(notifications) { + dump("RECD getNotification\n"); + is(notifications.length, 1, "There should be one stored notification"); + var notification = notifications[0]; + if (!notification) { + done(); + return; + } + ok(notification instanceof Notification, "Should be a Notification"); + is(notification.title, "This is a title", "Title should match"); + notification.close(); + done(); + }); + }); +}; diff --git a/dom/serviceworkers/test/notification_openWindow_worker.js b/dom/serviceworkers/test/notification_openWindow_worker.js new file mode 100644 index 0000000000..174199b14a --- /dev/null +++ b/dom/serviceworkers/test/notification_openWindow_worker.js @@ -0,0 +1,25 @@ +const gRoot = "http://mochi.test:8888/tests/dom/serviceworkers/test/"; +const gTestURL = gRoot + "test_notification_openWindow.html"; +const gClientURL = gRoot + "file_notification_openWindow.html"; + +onmessage = function(event) { + if (event.data !== "DONE") { + dump(`ERROR: received unexpected message: ${JSON.stringify(event.data)}\n`); + } + + event.waitUntil( + clients.matchAll({ includeUncontrolled: true }).then(cl => { + for (let client of cl) { + // The |gClientURL| window closes itself after posting the DONE message, + // so we don't need to send it anything here. + if (client.url === gTestURL) { + client.postMessage("DONE"); + } + } + }) + ); +}; + +onnotificationclick = function(event) { + clients.openWindow(gClientURL); +}; diff --git a/dom/serviceworkers/test/notificationclick-otherwindow.html b/dom/serviceworkers/test/notificationclick-otherwindow.html new file mode 100644 index 0000000000..f64e82aabd --- /dev/null +++ b/dom/serviceworkers/test/notificationclick-otherwindow.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + var ifr = document.createElement("iframe"); + document.documentElement.appendChild(ifr); + ifr.contentWindow.ServiceWorkerRegistration.prototype.showNotification + .call(swr, "Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }}); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data.result); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclick.html b/dom/serviceworkers/test/notificationclick.html new file mode 100644 index 0000000000..448764a1cb --- /dev/null +++ b/dom/serviceworkers/test/notificationclick.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + swr.showNotification("Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }}); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data.result); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclick.js b/dom/serviceworkers/test/notificationclick.js new file mode 100644 index 0000000000..ba7a7cb14a --- /dev/null +++ b/dom/serviceworkers/test/notificationclick.js @@ -0,0 +1,23 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +onnotificationclick = function(e) { + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump( + "********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n" + ); + return; + } + + clients.forEach(function(client) { + client.postMessage({ + result: + e.notification.data && + e.notification.data.complex && + e.notification.data.complex[0] == "jsval" && + e.notification.data.complex[1] == 5, + }); + }); + }); +}; diff --git a/dom/serviceworkers/test/notificationclick_focus.html b/dom/serviceworkers/test/notificationclick_focus.html new file mode 100644 index 0000000000..0152d397f3 --- /dev/null +++ b/dom/serviceworkers/test/notificationclick_focus.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1144660 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + swr.showNotification("Hi there. The ServiceWorker should receive a click event for this."); + }); + + navigator.serviceWorker.onmessage = function(msg) { + dump("GOT Message " + JSON.stringify(msg.data) + "\n"); + testWindow.callback(msg.data.ok); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclick_focus.js b/dom/serviceworkers/test/notificationclick_focus.js new file mode 100644 index 0000000000..4c88340041 --- /dev/null +++ b/dom/serviceworkers/test/notificationclick_focus.js @@ -0,0 +1,49 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// + +function promisifyTimerFocus(client, delay) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + client.focus().then(resolve, reject); + }, delay); + }); +} + +onnotificationclick = function(e) { + e.waitUntil( + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump( + "********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n" + ); + return Promise.resolve(); + } + + var immediatePromise = clients[0].focus(); + var withinTimeout = promisifyTimerFocus(clients[0], 100); + + var afterTimeout = promisifyTimerFocus(clients[0], 2000).then( + function() { + throw "Should have failed!"; + }, + function() { + return Promise.resolve(); + } + ); + + return Promise.all([immediatePromise, withinTimeout, afterTimeout]) + .then(function() { + clients.forEach(function(client) { + client.postMessage({ ok: true }); + }); + }) + .catch(function(ex) { + dump("Error " + ex + "\n"); + clients.forEach(function(client) { + client.postMessage({ ok: false }); + }); + }); + }) + ); +}; diff --git a/dom/serviceworkers/test/notificationclose.html b/dom/serviceworkers/test/notificationclose.html new file mode 100644 index 0000000000..f18801122e --- /dev/null +++ b/dom/serviceworkers/test/notificationclose.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1265841 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + return swr.showNotification( + "Hi there. The ServiceWorker should receive a close event for this.", + { data: { complex: ["jsval", 5] }}).then(function() { + return swr; + }); + }).then(function(swr) { + return swr.getNotifications(); + }).then(function(notifications) { + notifications.forEach(function(notification) { + notification.close(); + }); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclose.js b/dom/serviceworkers/test/notificationclose.js new file mode 100644 index 0000000000..75271e604e --- /dev/null +++ b/dom/serviceworkers/test/notificationclose.js @@ -0,0 +1,31 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +onnotificationclose = function(e) { + e.waitUntil( + (async function() { + let windowOpened = true; + await clients.openWindow("hello.html").catch(err => { + windowOpened = false; + }); + + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump("*** CLIENTS LIST EMPTY! Test will timeout! ***\n"); + return; + } + + clients.forEach(function(client) { + client.postMessage({ + result: + e.notification.data && + e.notification.data.complex && + e.notification.data.complex[0] == "jsval" && + e.notification.data.complex[1] == 5, + windowOpened, + }); + }); + }); + })() + ); +}; diff --git a/dom/serviceworkers/test/notify_loaded.js b/dom/serviceworkers/test/notify_loaded.js new file mode 100644 index 0000000000..3bf001abd6 --- /dev/null +++ b/dom/serviceworkers/test/notify_loaded.js @@ -0,0 +1 @@ +parent.postMessage("SCRIPT_LOADED", "*"); diff --git a/dom/serviceworkers/test/onmessageerror_worker.js b/dom/serviceworkers/test/onmessageerror_worker.js new file mode 100644 index 0000000000..15426128a6 --- /dev/null +++ b/dom/serviceworkers/test/onmessageerror_worker.js @@ -0,0 +1,54 @@ +async function getSwContainer() { + const clients = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + + for (let client of clients) { + if (client.url.endsWith("test_onmessageerror.html")) { + return client; + } + } +} + +self.addEventListener("message", async e => { + const config = e.data; + const swContainer = await getSwContainer(); + + if (config == "send-bad-message") { + const serializable = true; + const deserializable = false; + + swContainer.postMessage( + new StructuredCloneTester(serializable, deserializable) + ); + + return; + } + + if (!config.serializable) { + swContainer.postMessage({ + result: "Error", + reason: "Service Worker received an unserializable object", + }); + + return; + } + + if (!config.deserializable) { + swContainer.postMessage({ + result: "Error", + reason: + "Service Worker received (and deserialized) an un-deserializable object", + }); + + return; + } + + swContainer.postMessage({ received: "message" }); +}); + +self.addEventListener("messageerror", async () => { + const swContainer = await getSwContainer(); + swContainer.postMessage({ received: "messageerror" }); +}); diff --git a/dom/serviceworkers/test/opaque_intercept_worker.js b/dom/serviceworkers/test/opaque_intercept_worker.js new file mode 100644 index 0000000000..5dbf6fbc28 --- /dev/null +++ b/dom/serviceworkers/test/opaque_intercept_worker.js @@ -0,0 +1,40 @@ +var name = "opaqueInterceptCache"; + +// Cross origin request to ensure that an opaque response is used +var prefix = "http://example.com/tests/dom/serviceworkers/test/"; + +var testReady = new Promise(resolve => { + self.addEventListener( + "message", + m => { + resolve(); + }, + { once: true } + ); +}); + +self.addEventListener("install", function(event) { + var request = new Request(prefix + "notify_loaded.js", { mode: "no-cors" }); + event.waitUntil( + Promise.all([caches.open(name), fetch(request), testReady]).then(function( + results + ) { + var cache = results[0]; + var response = results[1]; + return cache.put("./sw_clients/does_not_exist.js", response); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + event.respondWith( + caches + .open(name) + .then(function(cache) { + return cache.match(event.request); + }) + .then(function(response) { + return response || fetch(event.request); + }) + ); +}); diff --git a/dom/serviceworkers/test/openWindow_worker.js b/dom/serviceworkers/test/openWindow_worker.js new file mode 100644 index 0000000000..eb440da6e5 --- /dev/null +++ b/dom/serviceworkers/test/openWindow_worker.js @@ -0,0 +1,178 @@ +// the worker won't shut down between events because we increased +// the timeout values. +var client; +var window_count = 0; +var expected_window_count = 9; +var isolated_window_count = 0; +var expected_isolated_window_count = 2; +var resolve_got_all_windows = null; +var got_all_windows = new Promise(function(res, rej) { + resolve_got_all_windows = res; +}); + +// |expected_window_count| needs to be updated for every new call that's +// expected to actually open a new window regardless of what |clients.openWindow| +// returns. +function testForUrl(url, throwType, clientProperties, resultsArray) { + return clients + .openWindow(url) + .then(function(e) { + if (throwType != null) { + resultsArray.push({ + result: false, + message: "openWindow should throw " + throwType, + }); + } else if (clientProperties) { + resultsArray.push({ + result: e instanceof WindowClient, + message: `openWindow should resolve to a WindowClient for url ${url}, got ${e}`, + }); + resultsArray.push({ + result: e.url == clientProperties.url, + message: "Client url should be " + clientProperties.url, + }); + // Add more properties + } else { + resultsArray.push({ + result: e == null, + message: "Open window should resolve to null. Got: " + e, + }); + } + }) + .catch(function(err) { + if (throwType == null) { + resultsArray.push({ + result: false, + message: "Unexpected throw: " + err, + }); + } else { + resultsArray.push({ + result: err.toString().includes(throwType), + message: "openWindow should throw: " + err, + }); + } + }); +} + +onmessage = function(event) { + if (event.data == "testNoPopup") { + client = event.source; + + var results = []; + var promises = []; + promises.push(testForUrl("about:blank", "TypeError", null, results)); + promises.push( + testForUrl("http://example.com", "InvalidAccessError", null, results) + ); + promises.push( + testForUrl("_._*`InvalidURL", "InvalidAccessError", null, results) + ); + event.waitUntil( + Promise.all(promises).then(function(e) { + client.postMessage(results); + }) + ); + } + + if (event.data == "NEW_WINDOW" || event.data == "NEW_ISOLATED_WINDOW") { + window_count += 1; + if (event.data == "NEW_ISOLATED_WINDOW") { + isolated_window_count += 1; + } + if (window_count == expected_window_count) { + resolve_got_all_windows(); + } + } + + if (event.data == "CHECK_NUMBER_OF_WINDOWS") { + event.waitUntil( + got_all_windows + .then(function() { + return clients.matchAll(); + }) + .then(function(cl) { + event.source.postMessage([ + { + result: cl.length == expected_window_count, + message: `The number of windows is correct. ${cl.length} == ${expected_window_count}`, + }, + { + result: isolated_window_count == expected_isolated_window_count, + message: `The number of isolated windows is correct. ${isolated_window_count} == ${expected_isolated_window_count}`, + }, + ]); + for (i = 0; i < cl.length; i++) { + cl[i].postMessage("CLOSE"); + } + }) + ); + } +}; + +onnotificationclick = function(e) { + var results = []; + var promises = []; + + var redirect = + "http://mochi.test:8888/tests/dom/serviceworkers/test/redirect.sjs?"; + var redirect_xorigin = + "http://example.com/tests/dom/serviceworkers/test/redirect.sjs?"; + var same_origin = + "http://mochi.test:8888/tests/dom/serviceworkers/test/open_window/client.sjs"; + var different_origin = + "http://example.com/tests/dom/serviceworkers/test/open_window/client.sjs"; + + promises.push(testForUrl("about:blank", "TypeError", null, results)); + promises.push(testForUrl(different_origin, null, null, results)); + promises.push(testForUrl(same_origin, null, { url: same_origin }, results)); + promises.push( + testForUrl("open_window/client.sjs", null, { url: same_origin }, results) + ); + + // redirect tests + promises.push( + testForUrl( + redirect + "open_window/client.sjs", + null, + { url: same_origin }, + results + ) + ); + promises.push(testForUrl(redirect + different_origin, null, null, results)); + + promises.push( + testForUrl(redirect_xorigin + "open_window/client.sjs", null, null, results) + ); + promises.push( + testForUrl( + redirect_xorigin + same_origin, + null, + { url: same_origin }, + results + ) + ); + + // coop+coep tests + promises.push( + testForUrl( + same_origin + "?crossOriginIsolated=true", + null, + { url: same_origin + "?crossOriginIsolated=true" }, + results + ) + ); + promises.push( + testForUrl( + different_origin + "?crossOriginIsolated=true", + null, + null, + results + ) + ); + + e.waitUntil( + Promise.all(promises).then(function() { + client.postMessage(results); + }) + ); +}; diff --git a/dom/serviceworkers/test/open_window/client.sjs b/dom/serviceworkers/test/open_window/client.sjs new file mode 100644 index 0000000000..236a4a1226 --- /dev/null +++ b/dom/serviceworkers/test/open_window/client.sjs @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const RESPONSE = ` +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1172870 - page opened by ServiceWorkerClients.OpenWindow</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<h1>client.sjs</h1> +<script class="testbody" type="text/javascript"> + + window.onload = function() { + if (document.domain === "mochi.test") { + navigator.serviceWorker.ready.then(function(result) { + navigator.serviceWorker.onmessage = function(event) { + if (event.data !== "CLOSE") { + dump("ERROR: unexepected reply from the service worker.\\n"); + } + if (parent) { + parent.postMessage("CLOSE", "*"); + } + window.close(); + } + + let message = window.crossOriginIsolated ? "NEW_ISOLATED_WINDOW" : "NEW_WINDOW"; + navigator.serviceWorker.controller.postMessage(message); + }) + } else { + window.onmessage = function(event) { + if (event.data !== "CLOSE") { + dump("ERROR: unexepected reply from the iframe.\\n"); + } + window.close(); + } + + var iframe = document.createElement('iframe'); + iframe.src = "http://mochi.test:8888/tests/dom/serviceworkers/test/open_window/client.sjs"; + document.body.appendChild(iframe); + } + } + +</script> +</pre> +</body> +</html> +`; + +function handleRequest(request, response) { + Components.utils.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + // If the request has been marked to be isolated with COOP+COEP, set the appropriate headers. + if (query.get("crossOriginIsolated") == "true") { + response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false); + } + + // Always set the COEP and CORP headers, so that this document can be framed + // by a document which has also set COEP to require-corp. + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp", false); + response.setHeader("Cross-Origin-Resource-Policy", "cross-origin", false); + + response.setHeader("Content-Type", "text/html", false); + response.write(RESPONSE); +} diff --git a/dom/serviceworkers/test/page_post_controlled.html b/dom/serviceworkers/test/page_post_controlled.html new file mode 100644 index 0000000000..27694c0027 --- /dev/null +++ b/dom/serviceworkers/test/page_post_controlled.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +<script type="text/javascript"> + window.parent.postMessage({ + controlled: !!navigator.serviceWorker.controller + }, "*"); + + addEventListener("message", e => { + if (e.data == "create nested iframe") { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.src = location.href; + } else { + window.parent.postMessage(e.data, "*"); + } + }); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/parse_error_worker.js b/dom/serviceworkers/test/parse_error_worker.js new file mode 100644 index 0000000000..b6a8ef0a1a --- /dev/null +++ b/dom/serviceworkers/test/parse_error_worker.js @@ -0,0 +1,2 @@ +// intentional parse error. +var foo = {; diff --git a/dom/serviceworkers/test/redirect.sjs b/dom/serviceworkers/test/redirect.sjs new file mode 100644 index 0000000000..43fec90b5a --- /dev/null +++ b/dom/serviceworkers/test/redirect.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", request.queryString, false); +} diff --git a/dom/serviceworkers/test/redirect_post.sjs b/dom/serviceworkers/test/redirect_post.sjs new file mode 100644 index 0000000000..3b15f397b1 --- /dev/null +++ b/dom/serviceworkers/test/redirect_post.sjs @@ -0,0 +1,39 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + var query = {}; + request.queryString.split("&").forEach(function(val) { + var [name, value] = val.split("="); + query[name] = unescape(value); + }); + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + while ((bodyAvail = bodyStream.available()) > 0) { + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + } + + var body = decodeURIComponent( + escape(String.fromCharCode.apply(null, bodyBytes)) + ); + + var currentHop = query.hop ? parseInt(query.hop) : 0; + + var obj = JSON.parse(body); + if (currentHop < obj.hops) { + var newURL = + "/tests/dom/serviceworkers/test/redirect_post.sjs?hop=" + + (1 + currentHop); + response.setStatusLine(null, 307, "redirect"); + response.setHeader("Location", newURL); + return; + } + + response.setHeader("Content-Type", "application/json"); + response.write(body); +} diff --git a/dom/serviceworkers/test/redirect_serviceworker.sjs b/dom/serviceworkers/test/redirect_serviceworker.sjs new file mode 100644 index 0000000000..858e6d4824 --- /dev/null +++ b/dom/serviceworkers/test/redirect_serviceworker.sjs @@ -0,0 +1,7 @@ +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "http://mochi.test:8888/tests/dom/serviceworkers/test/worker.js" + ); +} diff --git a/dom/serviceworkers/test/register_https.html b/dom/serviceworkers/test/register_https.html new file mode 100644 index 0000000000..572c7ce6b8 --- /dev/null +++ b/dom/serviceworkers/test/register_https.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<script> +function ok(condition, message) { + parent.postMessage({type: "ok", status: condition, msg: message}, "*"); +} + +function done() { + parent.postMessage({type: "done"}, "*"); +} + +ok(location.protocol == "https:", "We should be loaded from HTTPS"); +ok(!window.isSecureContext, "Should not be secure context"); +ok(!("serviceWorker" in navigator), "ServiceWorkerContainer not availalble in insecure context"); +done(); +</script> diff --git a/dom/serviceworkers/test/sanitize/example_check_and_unregister.html b/dom/serviceworkers/test/sanitize/example_check_and_unregister.html new file mode 100644 index 0000000000..8553e442d6 --- /dev/null +++ b/dom/serviceworkers/test/sanitize/example_check_and_unregister.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script> + function done(exists) { + parent.postMessage(exists, '*'); + } + + function fail() { + parent.postMessage("FAIL", '*'); + } + + navigator.serviceWorker.getRegistration(".").then(function(reg) { + if (reg) { + reg.unregister().then(done.bind(undefined, true), fail); + } else { + dump("getRegistration() returned undefined registration\n"); + done(false); + } + }, function(e) { + dump("getRegistration() failed\n"); + fail(); + }); +</script> diff --git a/dom/serviceworkers/test/sanitize/frame.html b/dom/serviceworkers/test/sanitize/frame.html new file mode 100644 index 0000000000..b4bf7a1ff1 --- /dev/null +++ b/dom/serviceworkers/test/sanitize/frame.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + fetch("intercept-this").then(function(r) { + if (!r.ok) { + return "FAIL"; + } + return r.text(); + }).then(function(body) { + parent.postMessage(body, '*'); + }); +</script> diff --git a/dom/serviceworkers/test/sanitize/register.html b/dom/serviceworkers/test/sanitize/register.html new file mode 100644 index 0000000000..4ae74bec11 --- /dev/null +++ b/dom/serviceworkers/test/sanitize/register.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script> + function done() { + parent.postMessage('', '*'); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../sanitize_worker.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/sanitize_worker.js b/dom/serviceworkers/test/sanitize_worker.js new file mode 100644 index 0000000000..efd25a3856 --- /dev/null +++ b/dom/serviceworkers/test/sanitize_worker.js @@ -0,0 +1,5 @@ +onfetch = function(e) { + if (e.request.url.includes("intercept-this")) { + e.respondWith(new Response("intercepted")); + } +}; diff --git a/dom/serviceworkers/test/scope/scope_worker.js b/dom/serviceworkers/test/scope/scope_worker.js new file mode 100644 index 0000000000..4164e7a244 --- /dev/null +++ b/dom/serviceworkers/test/scope/scope_worker.js @@ -0,0 +1,2 @@ +// This worker is used to test if calling register() without a scope argument +// leads to scope being relative to service worker script. diff --git a/dom/serviceworkers/test/script_file_upload.js b/dom/serviceworkers/test/script_file_upload.js new file mode 100644 index 0000000000..ff839744b2 --- /dev/null +++ b/dom/serviceworkers/test/script_file_upload.js @@ -0,0 +1,15 @@ +/* eslint-env mozilla/chrome-script */ + +Cu.importGlobalProperties(["File"]); + +addMessageListener("file.open", function(e) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("prefs.js"); + + File.createFromNsIFile(testFile).then(function(file) { + sendAsyncMessage("file.opened", { file }); + }); +}); diff --git a/dom/serviceworkers/test/self_update_worker.sjs b/dom/serviceworkers/test/self_update_worker.sjs new file mode 100644 index 0000000000..8081b20afd --- /dev/null +++ b/dom/serviceworkers/test/self_update_worker.sjs @@ -0,0 +1,42 @@ +/* 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/. */ +"use strict"; + +const WORKER_BODY = ` +onactivate = function(event) { + let promise = clients.matchAll({includeUncontrolled: true}).then(function(clients) { + for (i = 0; i < clients.length; i++) { + clients[i].postMessage({version: version}); + } + }).then(function() { + return self.registration.update(); + }); + event.waitUntil(promise); +}; +`; + +function handleRequest(request, response) { + if (request.queryString == "clearcounter") { + setState("count", "1"); + response.write("ok"); + return; + } + + let count = getState("count"); + if (count === "") { + count = 1; + } else { + count = parseInt(count); + } + + let worker = "var version = " + count + ";\n"; + worker = worker + WORKER_BODY; + + // This header is necessary for making this script able to be loaded. + response.setHeader("Content-Type", "application/javascript"); + + // If this is the first request, return the first source. + response.write(worker); + setState("count", "" + (count + 1)); +} diff --git a/dom/serviceworkers/test/server_file_upload.sjs b/dom/serviceworkers/test/server_file_upload.sjs new file mode 100644 index 0000000000..a2f960af94 --- /dev/null +++ b/dom/serviceworkers/test/server_file_upload.sjs @@ -0,0 +1,22 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const BinaryOutputStream = CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); + +function handleRequest(request, response) { + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + while ((bodyAvail = bodyStream.available()) > 0) { + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + } + + var bos = new BinaryOutputStream(response.bodyOutputStream); + bos.writeByteArray(bodyBytes, bodyBytes.length); +} diff --git a/dom/serviceworkers/test/service_worker.js b/dom/serviceworkers/test/service_worker.js new file mode 100644 index 0000000000..432b943d8b --- /dev/null +++ b/dom/serviceworkers/test/service_worker.js @@ -0,0 +1,9 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("Error: no clients are currently controlled.\n"); + return; + } + res[0].postMessage(indexedDB ? { available: true } : { available: false }); + }); +}; diff --git a/dom/serviceworkers/test/service_worker_client.html b/dom/serviceworkers/test/service_worker_client.html new file mode 100644 index 0000000000..c1c98eaabb --- /dev/null +++ b/dom/serviceworkers/test/service_worker_client.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +<title>controlled page</title> +<script class="testbody" type="text/javascript"> + if (!parent) { + info("service_worker_client.html should not be launched directly!"); + } + + window.onload = function() { + navigator.serviceWorker.onmessage = function(msg) { + // Forward messages coming from the service worker to the test page. + parent.postMessage(msg.data, "*"); + }; + navigator.serviceWorker.ready.then(function(swr) { + parent.postMessage("READY", "*"); + }); + } +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/serviceworker.html b/dom/serviceworkers/test/serviceworker.html new file mode 100644 index 0000000000..11edd001a2 --- /dev/null +++ b/dom/serviceworkers/test/serviceworker.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + navigator.serviceWorker.register("worker.js"); + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/serviceworker_not_sharedworker.js b/dom/serviceworkers/test/serviceworker_not_sharedworker.js new file mode 100644 index 0000000000..ef89fb01b4 --- /dev/null +++ b/dom/serviceworkers/test/serviceworker_not_sharedworker.js @@ -0,0 +1,20 @@ +function OnMessage(e) { + if (e.data.msg == "whoareyou") { + if ("ServiceWorker" in self) { + self.clients.matchAll().then(function(clients) { + clients[0].postMessage({ result: "serviceworker" }); + }); + } else { + port.postMessage({ result: "sharedworker" }); + } + } +} + +var port; +onconnect = function(e) { + port = e.ports[0]; + port.onmessage = OnMessage; + port.start(); +}; + +onmessage = OnMessage; diff --git a/dom/serviceworkers/test/serviceworker_wrapper.js b/dom/serviceworkers/test/serviceworker_wrapper.js new file mode 100644 index 0000000000..c9716c381f --- /dev/null +++ b/dom/serviceworkers/test/serviceworker_wrapper.js @@ -0,0 +1,92 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +// ServiceWorker equivalent of worker_wrapper.js. + +let client; + +function ok(a, msg) { + dump("OK: " + !!a + " => " + a + ": " + msg + "\n"); + client.postMessage({ type: "status", status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + dump("IS: " + (a === b) + " => " + a + " | " + b + ": " + msg + "\n"); + client.postMessage({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + }); +} + +function workerTestArrayEquals(a, b) { + if (!Array.isArray(a) || !Array.isArray(b) || a.length != b.length) { + return false; + } + for (var i = 0, n = a.length; i < n; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +} + +function workerTestDone() { + client.postMessage({ type: "finish" }); +} + +function workerTestGetHelperData(cb) { + addEventListener("message", function workerTestGetHelperDataCB(e) { + if (e.data.type !== "returnHelperData") { + return; + } + removeEventListener("message", workerTestGetHelperDataCB); + cb(e.data.result); + }); + client.postMessage({ + type: "getHelperData", + }); +} + +function workerTestGetStorageManager(cb) { + addEventListener("message", function workerTestGetStorageManagerCB(e) { + if (e.data.type !== "returnStorageManager") { + return; + } + removeEventListener("message", workerTestGetStorageManagerCB); + cb(e.data.result); + }); + client.postMessage({ + type: "getStorageManager", + }); +} + +let completeInstall; + +addEventListener("message", function workerWrapperOnMessage(e) { + removeEventListener("message", workerWrapperOnMessage); + var data = e.data; + self.clients.matchAll({ includeUncontrolled: true }).then(function(clients) { + for (var i = 0; i < clients.length; ++i) { + if (clients[i].url.includes("message_receiver.html")) { + client = clients[i]; + break; + } + } + try { + importScripts(data.script); + } catch (ex) { + client.postMessage({ + type: "status", + status: false, + msg: + "worker failed to import " + data.script + "; error: " + ex.message, + }); + } + completeInstall(); + }); +}); + +addEventListener("install", e => { + e.waitUntil(new Promise(resolve => (completeInstall = resolve))); +}); diff --git a/dom/serviceworkers/test/serviceworkerinfo_iframe.html b/dom/serviceworkers/test/serviceworkerinfo_iframe.html new file mode 100644 index 0000000000..24103d1757 --- /dev/null +++ b/dom/serviceworkers/test/serviceworkerinfo_iframe.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js", + { updateViaCache: 'all' }); + window.onmessage = function (e) { + if (e.data !== "unregister") { + return; + } + promise.then(function (registration) { + registration.unregister(); + }); + window.onmessage = null; + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/serviceworkermanager_iframe.html b/dom/serviceworkers/test/serviceworkermanager_iframe.html new file mode 100644 index 0000000000..4ea21010cb --- /dev/null +++ b/dom/serviceworkers/test/serviceworkermanager_iframe.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js"); + window.onmessage = function (event1) { + if (event1.data !== "register") { + return; + } + promise = promise.then(function (registration) { + return navigator.serviceWorker.register("worker2.js"); + }); + window.onmessage = function (event2) { + if (event2.data !== "unregister") { + return; + } + promise.then(function (registration) { + registration.unregister(); + }); + window.onmessage = null; + }; + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html b/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html new file mode 100644 index 0000000000..8f382cf0dc --- /dev/null +++ b/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + var reg; + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js"); + window.onmessage = function (e) { + if (e.data === "register") { + promise.then(function() { + return navigator.serviceWorker.register("worker2.js") + .then(function(registration) { + reg = registration; + }); + }); + } else if (e.data === "unregister") { + reg.unregister(); + } + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/sharedWorker_fetch.js b/dom/serviceworkers/test/sharedWorker_fetch.js new file mode 100644 index 0000000000..b0a72674d4 --- /dev/null +++ b/dom/serviceworkers/test/sharedWorker_fetch.js @@ -0,0 +1,30 @@ +var clients = new Array(); +clients.length = 0; + +var broadcast = function(message) { + var length = clients.length; + for (var i = 0; i < length; i++) { + port = clients[i]; + port.postMessage(message); + } +}; + +onconnect = function(e) { + clients.push(e.ports[0]); + if (clients.length == 1) { + clients[0].postMessage("Connected"); + } else if (clients.length == 2) { + broadcast("BothConnected"); + clients[0].onmessage = function(msg) { + if (msg.data == "StartFetchWithWrongIntegrity") { + // The fetch will succeed because the integrity value is invalid and we + // are looking for the console message regarding the bad integrity value. + fetch("SharedWorker_SRIFailed.html", { integrity: "abc" }).then( + function() { + clients[0].postMessage("SRI_failed"); + } + ); + } + }; + } +}; diff --git a/dom/serviceworkers/test/simple_fetch_worker.js b/dom/serviceworkers/test/simple_fetch_worker.js new file mode 100644 index 0000000000..d5b1d8d9a7 --- /dev/null +++ b/dom/serviceworkers/test/simple_fetch_worker.js @@ -0,0 +1,18 @@ +// A simple worker script that forward intercepted url to the controlled window. + +function responseMsg(msg) { + self.clients + .matchAll({ + includeUncontrolled: true, + type: "window", + }) + .then(clients => { + if (clients && clients.length) { + clients[0].postMessage(msg); + } + }); +} + +onfetch = function(e) { + responseMsg(e.request.url); +}; diff --git a/dom/serviceworkers/test/simpleregister/index.html b/dom/serviceworkers/test/simpleregister/index.html new file mode 100644 index 0000000000..99e4fe3f23 --- /dev/null +++ b/dom/serviceworkers/test/simpleregister/index.html @@ -0,0 +1,51 @@ +<html> + <head></head> + <body> + <script type="text/javascript"> + var expectedEvents = 2; + function eventReceived() { + window.parent.postMessage({ type: "check", status: expectedEvents > 0, msg: "updatefound received" }, "*"); + + if (--expectedEvents) { + window.parent.postMessage({ type: "finish" }, "*"); + } + } + + navigator.serviceWorker.getRegistrations().then(function(a) { + window.parent.postMessage({ type: "check", status: Array.isArray(a), + msg: "getRegistrations returns an array" }, "*"); + window.parent.postMessage({ type: "check", status: !!a.length, + msg: "getRegistrations returns an array with 1 item" }, "*"); + for (var i = 0; i < a.length; ++i) { + window.parent.postMessage({ type: "check", status: a[i] instanceof ServiceWorkerRegistration, + msg: "getRegistrations returns an array of ServiceWorkerRegistration objects" }, "*"); + if (a[i].scope.match(/simpleregister\//)) { + a[i].onupdatefound = function(e) { + eventReceived(); + } + } + } + }); + + navigator.serviceWorker.getRegistration('http://mochi.test:8888/tests/dom/serviceworkers/test/simpleregister/') + .then(function(a) { + window.parent.postMessage({ type: "check", status: a instanceof ServiceWorkerRegistration, + msg: "getRegistration returns a ServiceWorkerRegistration" }, "*"); + a.onupdatefound = function(e) { + eventReceived(); + } + }); + + navigator.serviceWorker.getRegistration('http://www.something_else.net/') + .then(function(a) { + window.parent.postMessage({ type: "check", status: false, + msg: "getRegistration should throw for security error!" }, "*"); + }, function(a) { + window.parent.postMessage({ type: "check", status: true, + msg: "getRegistration should throw for security error!" }, "*"); + }); + + window.parent.postMessage({ type: "ready" }, "*"); + </script> + </body> +</html> diff --git a/dom/serviceworkers/test/simpleregister/ready.html b/dom/serviceworkers/test/simpleregister/ready.html new file mode 100644 index 0000000000..6bc163e5f4 --- /dev/null +++ b/dom/serviceworkers/test/simpleregister/ready.html @@ -0,0 +1,14 @@ +<html> + <head></head> + <body> + <script type="text/javascript"> + + window.addEventListener('message', function(evt) { + navigator.serviceWorker.ready.then(function() { + evt.ports[0].postMessage("WOW!"); + }); + }); + + </script> + </body> +</html> diff --git a/dom/serviceworkers/test/skip_waiting_installed_worker.js b/dom/serviceworkers/test/skip_waiting_installed_worker.js new file mode 100644 index 0000000000..a142576b9d --- /dev/null +++ b/dom/serviceworkers/test/skip_waiting_installed_worker.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +self.addEventListener("install", evt => { + evt.waitUntil(self.skipWaiting()); +}); diff --git a/dom/serviceworkers/test/skip_waiting_scope/index.html b/dom/serviceworkers/test/skip_waiting_scope/index.html new file mode 100644 index 0000000000..2b480d8707 --- /dev/null +++ b/dom/serviceworkers/test/skip_waiting_scope/index.html @@ -0,0 +1,33 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("skip_waiting_scope/index.html shouldn't be launched directly!"); + } + + navigator.serviceWorker.oncontrollerchange = function() { + parent.postMessage({ + event: "controllerchange", + controllerScriptURL: navigator.serviceWorker.controller && + navigator.serviceWorker.controller.scriptURL + }, "*"); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/source_message_posting_worker.js b/dom/serviceworkers/test/source_message_posting_worker.js new file mode 100644 index 0000000000..e48c005294 --- /dev/null +++ b/dom/serviceworkers/test/source_message_posting_worker.js @@ -0,0 +1,16 @@ +onmessage = function(e) { + if (!e.source) { + dump("ERROR: message doesn't have a source."); + } + + if (!(e instanceof ExtendableMessageEvent)) { + e.source.postMessage("ERROR. event is not an extendable message event."); + } + + // The client should be a window client + if (e.source instanceof WindowClient) { + e.source.postMessage(e.data); + } else { + e.source.postMessage("ERROR. source is not a window client."); + } +}; diff --git a/dom/serviceworkers/test/storage_recovery_worker.sjs b/dom/serviceworkers/test/storage_recovery_worker.sjs new file mode 100644 index 0000000000..9c9ce6a8d7 --- /dev/null +++ b/dom/serviceworkers/test/storage_recovery_worker.sjs @@ -0,0 +1,23 @@ +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; + +function handleRequest(request, response) { + let redirect = getState("redirect"); + setState("redirect", "false"); + + if (request.queryString.includes("set-redirect")) { + setState("redirect", "true"); + } else if (request.queryString.includes("clear-redirect")) { + setState("redirect", "false"); + } + + response.setHeader("Cache-Control", "no-store"); + + if (redirect === "true") { + response.setStatusLine(request.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", BASE_URI + "empty.js"); + return; + } + + response.setHeader("Content-Type", "application/javascript"); + response.write(""); +} diff --git a/dom/serviceworkers/test/streamfilter_server.sjs b/dom/serviceworkers/test/streamfilter_server.sjs new file mode 100644 index 0000000000..0e5a62d1dd --- /dev/null +++ b/dom/serviceworkers/test/streamfilter_server.sjs @@ -0,0 +1,9 @@ +Components.utils.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + const searchParams = new URLSearchParams(request.queryString); + + if (searchParams.get("syntheticResponse") === "0") { + response.write(String(searchParams)); + } +} diff --git a/dom/serviceworkers/test/streamfilter_worker.js b/dom/serviceworkers/test/streamfilter_worker.js new file mode 100644 index 0000000000..03a0f0a933 --- /dev/null +++ b/dom/serviceworkers/test/streamfilter_worker.js @@ -0,0 +1,9 @@ +onactivate = e => e.waitUntil(clients.claim()); + +onfetch = e => { + const searchParams = new URL(e.request.url).searchParams; + + if (searchParams.get("syntheticResponse") === "1") { + e.respondWith(new Response(String(searchParams))); + } +}; diff --git a/dom/serviceworkers/test/strict_mode_warning.js b/dom/serviceworkers/test/strict_mode_warning.js new file mode 100644 index 0000000000..38418de3d8 --- /dev/null +++ b/dom/serviceworkers/test/strict_mode_warning.js @@ -0,0 +1,4 @@ +function f() { + return 1; + return 2; +} diff --git a/dom/serviceworkers/test/sw_bad_mime_type.js b/dom/serviceworkers/test/sw_bad_mime_type.js new file mode 100644 index 0000000000..f371807db9 --- /dev/null +++ b/dom/serviceworkers/test/sw_bad_mime_type.js @@ -0,0 +1 @@ +// I need some contents. diff --git a/dom/serviceworkers/test/sw_bad_mime_type.js^headers^ b/dom/serviceworkers/test/sw_bad_mime_type.js^headers^ new file mode 100644 index 0000000000..a1f9e38d90 --- /dev/null +++ b/dom/serviceworkers/test/sw_bad_mime_type.js^headers^ @@ -0,0 +1 @@ +Content-Type: text/plain diff --git a/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html b/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html new file mode 100644 index 0000000000..ff9803622a --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html @@ -0,0 +1,76 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>test file blob upload with SW interception</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +if (!parent) { + dump("sw_clients/file_blob_upload_frame.html shouldn't be launched directly!"); +} + +function makeFileBlob(obj) { + return new Promise(function(resolve, reject) { + + var request = indexedDB.open(window.location.pathname, 1); + request.onerror = reject; + request.onupgradeneeded = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var objectStore = db.createObjectStore('test', { autoIncrement: true }); + var index = objectStore.createIndex('test', 'index'); + }; + + request.onsuccess = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var blob = new Blob([JSON.stringify(obj)], + { type: 'application/json' }); + var data = { blob, index: 5 }; + + objectStore = db.transaction('test', 'readwrite').objectStore('test'); + objectStore.add(data).onsuccess = function(evt1) { + var key = evt1.target.result; + objectStore = db.transaction('test').objectStore('test'); + objectStore.get(key).onsuccess = function(evt2) { + resolve(evt2.target.result.blob); + }; + }; + }; + }); +} + +navigator.serviceWorker.ready.then(function() { + parent.postMessage({ status: 'READY' }, '*'); +}); + +var URL = '/tests/dom/serviceworkers/test/redirect_post.sjs'; + +addEventListener('message', function(evt) { + if (evt.data.type = 'TEST') { + makeFileBlob(evt.data.body).then(function(blob) { + return fetch(URL, { method: 'POST', body: blob }); + }).then(function(response) { + return response.json(); + }).then(function(result) { + parent.postMessage({ status: 'OK', result }, '*'); + }).catch(function(e) { + parent.postMessage({ status: 'ERROR', result: e.toString() }, '*'); + }); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/navigator.html b/dom/serviceworkers/test/sw_clients/navigator.html new file mode 100644 index 0000000000..16a4fe9189 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/navigator.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + dump("sw_clients/navigator.html shouldn't be launched directly!\n"); + } + + window.addEventListener("message", function(event) { + if (event.data.type === "NAVIGATE") { + window.location = event.data.url; + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("NAVIGATOR_READY", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/refresher.html b/dom/serviceworkers/test/sw_clients/refresher.html new file mode 100644 index 0000000000..b3c6e00152 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <!-- some tests will intercept this bogus script request --> + <script type="text/javascript" src="does_not_exist.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + dump("sw_clients/simple.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function(event) { + if (event.data === "REFRESH") { + window.location.reload(); + } else if (event.data === "FORCE_REFRESH") { + window.location.reload(true); + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached.html b/dom/serviceworkers/test/sw_clients/refresher_cached.html new file mode 100644 index 0000000000..4a91e46e99 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_cached.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("sw_clients/simple.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function(event) { + if (event.data === "REFRESH") { + window.location.reload(); + } else if (event.data === "FORCE_REFRESH") { + window.location.reload(true); + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY_CACHED", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html Binary files differnew file mode 100644 index 0000000000..6b6a328211 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^ new file mode 100644 index 0000000000..4204d8601d --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Encoding: gzip diff --git a/dom/serviceworkers/test/sw_clients/refresher_compressed.html b/dom/serviceworkers/test/sw_clients/refresher_compressed.html Binary files differnew file mode 100644 index 0000000000..e0861a5180 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html diff --git a/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^ new file mode 100644 index 0000000000..4204d8601d --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Encoding: gzip diff --git a/dom/serviceworkers/test/sw_clients/service_worker_controlled.html b/dom/serviceworkers/test/sw_clients/service_worker_controlled.html new file mode 100644 index 0000000000..e0d7bce573 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/service_worker_controlled.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>controlled page</title> + <!-- + Paged controlled by a service worker for testing matchAll(). + See bug 982726, 1058311. + --> +<script class="testbody" type="text/javascript"> + function fail(msg) { + info("service_worker_controlled.html: " + msg); + opener.postMessage("FAIL", "*"); + } + + if (!parent) { + info("service_worker_controlled.html should not be launched directly!"); + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + parent.postMessage("READY", "*"); + }); + } + + navigator.serviceWorker.onmessage = function(msg) { + // forward message to the test page. + parent.postMessage(msg.data, "*"); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/simple.html b/dom/serviceworkers/test/sw_clients/simple.html new file mode 100644 index 0000000000..bbe6782e2a --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/simple.html @@ -0,0 +1,29 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("sw_clients/simple.html shouldn't be launched directly!"); + } + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_file_upload.js b/dom/serviceworkers/test/sw_file_upload.js new file mode 100644 index 0000000000..c35269924b --- /dev/null +++ b/dom/serviceworkers/test/sw_file_upload.js @@ -0,0 +1,16 @@ +self.skipWaiting(); + +addEventListener("fetch", event => { + const url = new URL(event.request.url); + const params = new URLSearchParams(url.search); + + if (params.get("clone") === "1") { + event.respondWith(fetch(event.request.clone())); + } else { + event.respondWith(fetch(event.request)); + } +}); + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/sw_respondwith_serviceworker.js b/dom/serviceworkers/test/sw_respondwith_serviceworker.js new file mode 100644 index 0000000000..6ddbc3d5c1 --- /dev/null +++ b/dom/serviceworkers/test/sw_respondwith_serviceworker.js @@ -0,0 +1,24 @@ +const SERVICEWORKER_DOC = `<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="utils.js" type="text/javascript"></script> +</head> +<body> +SERVICEWORKER +</body> +</html> +`; + +const SERVICEWORKER_RESPONSE = new Response(SERVICEWORKER_DOC, { + headers: { "content-type": "text/html" }, +}); + +addEventListener("fetch", event => { + // Allow utils.js which we explicitly include to be loaded by resetting + // interception. + if (event.request.url.endsWith("/utils.js")) { + return; + } + event.respondWith(SERVICEWORKER_RESPONSE.clone()); +}); diff --git a/dom/serviceworkers/test/sw_storage_not_allow.js b/dom/serviceworkers/test/sw_storage_not_allow.js new file mode 100644 index 0000000000..c0bd782326 --- /dev/null +++ b/dom/serviceworkers/test/sw_storage_not_allow.js @@ -0,0 +1,33 @@ +let clientId; +addEventListener("fetch", function(event) { + event.respondWith( + (async function() { + if (event.request.url.includes("getClients")) { + // Expected to fail since the storage access is not allowed. + try { + await self.clients.matchAll(); + } catch (e) { + // expected failure + } + } else if (event.request.url.includes("getClient-stage1")) { + let clients = await self.clients.matchAll(); + clientId = clients[0].id; + } else if (event.request.url.includes("getClient-stage2")) { + // Expected to fail since the storage access is not allowed. + try { + await self.clients.get(clientId); + } catch (e) { + // expected failure + } + } + + // Pass through the network request once our various Clients API + // promises have completed. + return await fetch(event.request); + })() + ); +}); + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/sw_with_navigationPreload.js b/dom/serviceworkers/test/sw_with_navigationPreload.js new file mode 100644 index 0000000000..75e06787cd --- /dev/null +++ b/dom/serviceworkers/test/sw_with_navigationPreload.js @@ -0,0 +1,28 @@ +addEventListener("activate", event => { + event.waitUntil(self.registration.navigationPreload.enable()); +}); + +async function post_to_page(data) { + let cs = await self.clients.matchAll(); + for (const client of cs) { + client.postMessage(data); + } +} + +addEventListener("fetch", event => { + if (event.request.url.includes("navigationPreload_page.html")) { + event.respondWith( + new Response("<!DOCTYPE html>", { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }) + ); + + event.waitUntil( + (async function() { + let preloadResponse = await event.preloadResponse; + let text = await preloadResponse.text(); + await post_to_page(text); + })() + ); + } +}); diff --git a/dom/serviceworkers/test/swa/worker_scope_different.js b/dom/serviceworkers/test/swa/worker_scope_different.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different.js diff --git a/dom/serviceworkers/test/swa/worker_scope_different.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_different.js^headers^ new file mode 100644 index 0000000000..e85a7f09de --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: different/path diff --git a/dom/serviceworkers/test/swa/worker_scope_different2.js b/dom/serviceworkers/test/swa/worker_scope_different2.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different2.js diff --git a/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^ new file mode 100644 index 0000000000..e37307d666 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /different/path diff --git a/dom/serviceworkers/test/swa/worker_scope_precise.js b/dom/serviceworkers/test/swa/worker_scope_precise.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_precise.js diff --git a/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^ new file mode 100644 index 0000000000..7488cafbb0 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/serviceworkers/test/swa diff --git a/dom/serviceworkers/test/swa/worker_scope_too_deep.js b/dom/serviceworkers/test/swa/worker_scope_too_deep.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js diff --git a/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^ new file mode 100644 index 0000000000..9a66c3d153 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/serviceworkers/test/swa/deep/way/too/specific diff --git a/dom/serviceworkers/test/swa/worker_scope_too_narrow.js b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js diff --git a/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^ new file mode 100644 index 0000000000..407361a3c7 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/serviceworkers diff --git a/dom/serviceworkers/test/test_abrupt_completion.html b/dom/serviceworkers/test/test_abrupt_completion.html new file mode 100644 index 0000000000..bbf9e965f0 --- /dev/null +++ b/dom/serviceworkers/test/test_abrupt_completion.html @@ -0,0 +1,144 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> + +// Tests a _registered_ ServiceWorker whose script evaluation results in an +// "abrupt completion", e.g. threw an uncaught exception. Such a ServiceWorker's +// first script evaluation must result in a "normal completion", however, for +// the Update algorithm to not abort in its step 18 when registering: +// +// 18. If runResult is failure or an abrupt completion, then: [...] + +const script = "./abrupt_completion_worker.js"; +const scope = "./empty.html"; +const expectedMessage = "handler-before-throw"; +let registration = null; + +// Should only be called once registration.active is non-null. Uses +// implementation details by zero-ing the "idle timeout"s and then sending an +// event to the ServiceWorker, which should immediately cause its termination. +// The idle timeouts are restored after the ServiceWorker is terminated. +async function startAndStopServiceWorker() { + SpecialPowers.registerObservers("service-worker-shutdown"); + + const spTopic = "specialpowers-service-worker-shutdown"; + + const origIdleTimeout = + SpecialPowers.getIntPref("dom.serviceWorkers.idle_timeout"); + + const origIdleExtendedTimeout = + SpecialPowers.getIntPref("dom.serviceWorkers.idle_extended_timeout"); + + await new Promise(resolve => { + const observer = { + async observe(subject, topic, data) { + if (topic !== spTopic) { + return; + } + + SpecialPowers.removeObserver(observer, spTopic); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", origIdleTimeout], + ["dom.serviceWorkers.idle_extended_timeout", origIdleExtendedTimeout] + ] + }); + + resolve(); + }, + }; + + // Speed things up. + SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 0] + ] + }).then(() => { + SpecialPowers.addObserver(observer, spTopic); + + registration.active.postMessage(""); + }); + }); +} + +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ] + }); + + registration = await navigator.serviceWorker.register(script, { scope }); + SimpleTest.registerCleanupFunction(async function unregisterRegistration() { + await registration.unregister(); + }); + + await new Promise(resolve => { + const serviceWorker = registration.installing; + + serviceWorker.onstatechange = () => { + if (serviceWorker.state === "activated") { + resolve(); + } + }; + }); + + ok(registration.active instanceof ServiceWorker, "ServiceWorker is activated"); +}); + +// We expect that the restarted SW that experiences an abrupt completion at +// startup after adding its message handler 1) will be active in order to +// respond to our postMessage and 2) will respond with the global value set +// prior to the importScripts call that throws (and not the global value that +// would have been assigned after the importScripts call if it didn't throw). +add_task(async function testMessageHandler() { + await startAndStopServiceWorker(); + + await new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + is(e.data, expectedMessage, "Correct message handler"); + resolve(); + }; + registration.active.postMessage(""); + }); +}); + +// We expect that the restarted SW that experiences an abrupt completion at +// startup before adding its "fetch" listener will 1) successfully dispatch the +// event and 2) it will not be handled (respondWith() will not be called) so +// interception will be reset and the response will contain the contents of +// empty.html. Before the fix in bug 1603484 the SW would fail to properly start +// up and the fetch event would result in a NetworkError, breaking the +// controlled page. +add_task(async function testFetchHandler() { + await startAndStopServiceWorker(); + + const iframe = document.createElement("iframe"); + SimpleTest.registerCleanupFunction(function removeIframe() { + iframe.remove(); + }); + + await new Promise(resolve => { + iframe.src = scope; + iframe.onload = resolve; + document.body.appendChild(iframe); + }); + + const response = await iframe.contentWindow.fetch(scope); + + // NetworkError will have a status of 0, which is not "ok", and this is + // a stronger guarantee that should be true instead of just checking if there + // isn't a NetworkError. + ok(response.ok, "Fetch succeeded and didn't result in a NetworkError"); + + const text = await response.text(); + is(text, "", "Correct response text"); +}); + +</script> diff --git a/dom/serviceworkers/test/test_async_waituntil.html b/dom/serviceworkers/test/test_async_waituntil.html new file mode 100644 index 0000000000..8c15eb2b11 --- /dev/null +++ b/dom/serviceworkers/test/test_async_waituntil.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test that: + 1. waitUntil() waits for each individual promise separately, even if + one of them was rejected. + 2. waitUntil() can be called asynchronously as long as there is still + a pending extension promise. + --> +<head> + <title>Test for Bug 1263304</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1263304">Mozilla Bug 1263304</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +function wait_for_message(expected_message) { + return new Promise(function(resolve, reject) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + ok(event.data === expected_message, "Received expected message event: " + event.data); + resolve(); + } + }); +} + +add_task(async function async_wait_until() { + var worker; + let registration = await navigator.serviceWorker.register( + "async_waituntil_worker.js", { scope: "./"} ) + .then(function(reg) { + worker = reg.installing; + return waitForState(worker, 'activated', reg); + }); + + // The service worker will claim us when it becomes active. + ok(navigator.serviceWorker.controller, "Controlled"); + + // This will make the service worker die immediately if there are no pending + // waitUntil promises to keep it alive. + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + // The service worker will wait on two promises, one of which + // will be rejected. We check whether the SW is killed using + // the value of a global variable. + let waitForStart = wait_for_message("Started"); + worker.postMessage("Start"); + await waitForStart; + + await new Promise((res, rej) => { + setTimeout(res, 0); + }); + + let waitResult = wait_for_message("Success"); + worker.postMessage("Result"); + await waitResult; + + // Test the behaviour of calling waitUntil asynchronously. The important + // part is that we receive the message event. + let waitForMessage = wait_for_message("Done"); + await fetch("doesnt_exist.html").then(() => { + ok(true, "Fetch was successful."); + }); + await waitForMessage; + + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + await registration.unregister(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bad_script_cache.html b/dom/serviceworkers/test/test_bad_script_cache.html new file mode 100644 index 0000000000..7919802678 --- /dev/null +++ b/dom/serviceworkers/test/test_bad_script_cache.html @@ -0,0 +1,96 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test updating a service worker with a bad script cache.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script src='utils.js'></script> +<script class="testbody" type="text/javascript"> + +async function deleteCaches(cacheStorage) { + let keyList = await cacheStorage.keys(); + let promiseList = []; + keyList.forEach(key => { + promiseList.push(cacheStorage.delete(key)); + }); + return await Promise.all(keyList); +} + +function waitForUpdate(reg) { + return new Promise(resolve => { + reg.addEventListener('updatefound', resolve, { once: true }); + }); +} + +async function runTest() { + let reg; + try { + const script = 'update_worker.sjs'; + const scope = 'bad-script-cache'; + + reg = await navigator.serviceWorker.register(script, { scope }); + await waitForState(reg.installing, 'activated'); + + // Verify the service worker script cache has the worker script stored. + let chromeCaches = SpecialPowers.createChromeCache('chrome', window.origin); + let scriptURL = new URL(script, window.location.href); + let response = await chromeCaches.match(scriptURL.href); + is(response.url, scriptURL.href, 'worker script should be stored'); + + // Force delete the service worker script out from under the service worker. + // Note: Prefs are set to kill the SW thread immediately on idle. + await deleteCaches(chromeCaches); + + // Verify the service script cache no longer knows about the worker script. + response = await chromeCaches.match(scriptURL.href); + is(response, undefined, 'worker script should not be stored'); + + // Force an update and wait for it to fire an update event. + reg.update(); + await waitForUpdate(reg); + await waitForState(reg.installing, 'activated'); + + // Verify that the script cache knows about the worker script again. + response = await chromeCaches.match(scriptURL.href); + is(response.url, scriptURL.href, 'worker script should be stored'); + } catch (e) { + ok(false, e); + } + if (reg) { + await reg.unregister(); + } + + // If this test is run on windows and the process shuts down immediately after, then + // we may fail to remove some of the Cache API body files. This is because the GC + // runs late causing Cache API to cleanup after shutdown begins. It seems something + // during shutdown scans these files and conflicts with removing the file on windows. + // + // To avoid this we perform an explict GC here to ensure that Cache API can cleanup + // earlier. + await new Promise(resolve => SpecialPowers.exactGC(resolve)); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + // standard prefs + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + + // immediately kill the service worker thread when idle + ["dom.serviceWorkers.idle_timeout", 0], + +]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bug1151916.html b/dom/serviceworkers/test/test_bug1151916.html new file mode 100644 index 0000000000..b541129ccb --- /dev/null +++ b/dom/serviceworkers/test/test_bug1151916.html @@ -0,0 +1,104 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1151916 - Test principal is set on cached serviceworkers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<!-- + If the principal is not set, accessing self.caches in the worker will crash. +--> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var frame; + + function listenForMessage() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "failed") { + ok(false, "iframe had error " + e.data.message); + reject(e.data.message); + } else if (e.data.status == "success") { + ok(true, "iframe step success " + e.data.message); + resolve(e.data.message); + } else { + ok(false, "Unexpected message " + e.data); + reject(); + } + } + }); + + return p; + } + + // We have the iframe register for its own scope so that this page is not + // holding any references when we GC. + function register() { + var p = listenForMessage(); + + frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = "bug1151916_driver.html"; + + return p; + } + + function unloadFrame() { + frame.src = "about:blank"; + frame.remove(); + frame = null; + } + + function gc() { + return new Promise(function(resolve) { + SpecialPowers.exactGC(resolve); + }); + } + + function testCaches() { + var p = listenForMessage(); + + frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = "bug1151916_driver.html"; + + return p; + } + + function unregister() { + return navigator.serviceWorker.getRegistration("./bug1151916_driver.html").then(function(reg) { + ok(reg instanceof ServiceWorkerRegistration, "Must have valid registration."); + return reg.unregister(); + }); + } + + function runTest() { + register() + .then(unloadFrame) + .then(gc) + .then(testCaches) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bug1240436.html b/dom/serviceworkers/test/test_bug1240436.html new file mode 100644 index 0000000000..8b76ada6a8 --- /dev/null +++ b/dom/serviceworkers/test/test_bug1240436.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for encoding of service workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + function runTest() { + navigator.serviceWorker.register("bug1240436_worker.js") + .then(reg => reg.unregister()) + .then(() => ok(true, "service worker register script succeed")) + .catch(err => ok(false, "service worker register script faled " + err)) + .then(() => SimpleTest.finish()); + } + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bug1408734.html b/dom/serviceworkers/test/test_bug1408734.html new file mode 100644 index 0000000000..27559e695f --- /dev/null +++ b/dom/serviceworkers/test/test_bug1408734.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1408734</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <script src="utils.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +// setup prefs +add_task(() => { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +// test for bug 1408734 +add_task(async () => { + // register a service worker + let registration = await navigator.serviceWorker.register("fetch.js", + {scope: "./"}); + // wait for service worker be activated + await waitForState(registration.installing, "activated"); + + // get the ServiceWorkerRegistration we just register through GetRegistration + registration = await navigator.serviceWorker.getRegistration("./"); + ok(registration, "should get the registration under scope './'"); + + // call unregister() + await registration.unregister(); + + // access registration.updateViaCache to trigger the bug + // we really care that we don't crash. In the future we will fix + is(registration.updateViaCache, "imports", + "registration.updateViaCache should work after unregister()"); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_claim.html b/dom/serviceworkers/test/test_claim.html new file mode 100644 index 0000000000..e72f1173e8 --- /dev/null +++ b/dom/serviceworkers/test/test_claim.html @@ -0,0 +1,171 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - Test service worker clients claim onactivate </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration_1; + var registration_2; + var client; + + function register_1() { + return navigator.serviceWorker.register("claim_worker_1.js", + { scope: "./" }) + .then((swr) => registration_1 = swr); + } + + function register_2() { + return navigator.serviceWorker.register("claim_worker_2.js", + { scope: "./claim_clients/client.html" }) + .then((swr) => registration_2 = swr); + } + + function unregister(reg) { + return reg.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function createClient() { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + res(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "parent exists."); + + client = document.createElement("iframe"); + client.setAttribute('src', "claim_clients/client.html"); + content.appendChild(client); + + return p; + } + + function testController() { + ok(navigator.serviceWorker.controller.scriptURL.match("claim_worker_1"), + "Controlling service worker has the correct url."); + } + + function testClientWasClaimed(expected) { + var resolveClientMessage, resolveClientControllerChange; + var messageFromClient = new Promise(function(res, rej) { + resolveClientMessage = res; + }); + var controllerChangeFromClient = new Promise(function(res, rej) { + resolveClientControllerChange = res; + }); + window.onmessage = function(e) { + if (!e.data.event) { + ok(false, "Unknown message received: " + e.data); + } + + if (e.data.event === "controllerchange") { + ok(e.data.controller, + "Client was claimed and received controllerchange event."); + resolveClientControllerChange(); + } + + if (e.data.event === "message") { + ok(e.data.data.resolve_value === undefined, + "Claim should resolve with undefined."); + ok(e.data.data.message === expected.message, + "Client received message from claiming worker."); + ok(e.data.data.match_count_before === expected.match_count_before, + "MatchAll clients count before claim should be " + expected.match_count_before); + ok(e.data.data.match_count_after === expected.match_count_after, + "MatchAll clients count after claim should be " + expected.match_count_after); + resolveClientMessage(); + } + } + + return Promise.all([messageFromClient, controllerChangeFromClient]) + .then(() => window.onmessage = null); + } + + function testClaimFirstWorker() { + // wait for the worker to control us + var controllerChange = new Promise(function(res, rej) { + navigator.serviceWorker.oncontrollerchange = function(e) { + ok(true, "controller changed event received."); + res(); + }; + }); + + var messageFromWorker = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(e) { + ok(e.data.resolve_value === undefined, + "Claim should resolve with undefined."); + ok(e.data.message === "claim_worker_1", + "Received message from claiming worker."); + ok(e.data.match_count_before === 0, + "Worker doesn't control any client before claim."); + ok(e.data.match_count_after === 2, "Worker should claim 2 clients."); + res(); + } + }); + + var clientClaim = testClientWasClaimed({ + message: "claim_worker_1", + match_count_before: 0, + match_count_after: 2 + }); + + return Promise.all([controllerChange, messageFromWorker, clientClaim]) + .then(testController); + } + + function testClaimSecondWorker() { + navigator.serviceWorker.oncontrollerchange = function(e) { + ok(false, "Claim_worker_2 shouldn't claim this window."); + } + + navigator.serviceWorker.onmessage = function(e) { + ok(false, "Claim_worker_2 shouldn't claim this window."); + } + + var clientClaim = testClientWasClaimed({ + message: "claim_worker_2", + match_count_before: 0, + match_count_after: 1 + }); + + return clientClaim.then(testController); + } + + function runTest() { + createClient() + .then(register_1) + .then(testClaimFirstWorker) + .then(register_2) + .then(testClaimSecondWorker) + .then(function() { return unregister(registration_1); }) + .then(function() { return unregister(registration_2); }) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_claim_oninstall.html b/dom/serviceworkers/test/test_claim_oninstall.html new file mode 100644 index 0000000000..54933405ce --- /dev/null +++ b/dom/serviceworkers/test/test_claim_oninstall.html @@ -0,0 +1,77 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - Test service worker clients.claim oninstall</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + + function register() { + return navigator.serviceWorker.register("claim_oninstall_worker.js", + { scope: "./" }) + .then((swr) => registration = swr); + } + + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function testClaim() { + ok(registration.installing, "Worker should be in installing state"); + + navigator.serviceWorker.oncontrollerchange = function() { + ok(false, "Claim should not succeed when the worker is not active."); + } + + var p = new Promise(function(res, rej) { + var worker = registration.installing; + worker.onstatechange = function(e) { + if (worker.state === 'installed') { + is(worker, registration.waiting, "Worker should be in waiting state"); + } else if (worker.state === 'activated') { + // The worker will become active only if claim will reject inside the + // install handler. + is(worker, registration.active, + "Claim should reject if the worker is not active"); + ok(navigator.serviceWorker.controller === null, "Client is not controlled."); + e.target.onstatechange = null; + res(); + } + } + }); + + return p; + } + + function runTest() { + register() + .then(testClaim) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_controller.html b/dom/serviceworkers/test/test_controller.html new file mode 100644 index 0000000000..c0e220a36e --- /dev/null +++ b/dom/serviceworkers/test/test_controller.html @@ -0,0 +1,83 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1002570 - test controller instance.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + var content; + var iframe; + var registration; + + function simpleRegister() { + // We use the control scope for the less specific registration. The window will register a worker on controller/ + return navigator.serviceWorker.register("worker.js", { scope: "./control" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then(swr => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + content.removeChild(iframe); + resolve(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "controller/index.html"); + content.appendChild(iframe); + + return p; + } + + // This document just flips the prefs and opens the iframe for the actual test. + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_cookie_fetch.html b/dom/serviceworkers/test/test_cookie_fetch.html new file mode 100644 index 0000000000..8c4324c759 --- /dev/null +++ b/dom/serviceworkers/test/test_cookie_fetch.html @@ -0,0 +1,64 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1331680 - test access to cookies in the documents synthesized from service worker responses</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + // Remove the iframe and recreate a new one to ensure that any traces + // of the cookies have been removed from the child process. + iframe.remove(); + iframe = document.createElement("iframe"); + document.getElementById("content").appendChild(iframe); + + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/synth.html"; + } else if (e.data.status == "done") { + // Note, we can't do an exact is() comparison here since other + // tests can leave cookies on the domain. + ok(e.data.cookie.includes("foo=bar"), + "The synthesized document has access to its cookies"); + + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html b/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html new file mode 100644 index 0000000000..bfd4f700be --- /dev/null +++ b/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test access to a cross origin Request.url property from a service worker for a redirected intercepted iframe</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/index.html"; + } else if (e.data.status == "done") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html b/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html new file mode 100644 index 0000000000..b5ddbb97b6 --- /dev/null +++ b/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that a CSP upgraded request can be intercepted by a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html"; + } else if (e.data.status == "protocol") { + is(e.data.data, "https:", "Correct protocol expected"); + } else if (e.data.status == "image") { + is(e.data.data, 40, "The image request was upgraded before interception"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // This is needed so that we can test upgrading a non-secure load inside an https iframe. + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html b/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html new file mode 100644 index 0000000000..09c05d557a --- /dev/null +++ b/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <title> Verify devtools can utilize nsIChannel::LOAD_BYPASS_SERVICE_WORKER to bypass the service worker </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> +<div id="content" style="display: none"></div> +<script src="utils.js"></script> +<script type="text/javascript"> +"use strict"; + +async function testBypassSW () { + let Ci = SpecialPowers.Ci; + + // Bypass SW imitates the "Disable Cache" option in dev-tools. + // Note: if we put the setter/getter into dev-tools, we should take care of + // the implementation of enabling/disabling cache since it just overwrite the + // defaultLoadFlags of docShell. + function setBypassServiceWorker(aDocShell, aBypass) { + if (aBypass) { + aDocShell.defaultLoadFlags |= Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER; + return; + } + + aDocShell.defaultLoadFlags &= ~Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER; + } + + function getBypassServiceWorker(aDocShell) { + return !!(aDocShell.defaultLoadFlags & + Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER); + } + + async function fetchFakeDocAndCheckIfIntercepted(aWindow) { + const fakeDoc = "fake.html"; + + // Note: The fetching document doesn't exist, so the expected status of the + // repsonse is 404 unless the request is hijacked. + let response = await aWindow.fetch(fakeDoc); + if (response.status === 404) { + return false; + } else if (!response.ok) { + throw(response.statusText); + } + + let text = await response.text(); + if (text.includes("Hello")) { + // Intercepted + return true; + } + + throw("Unexpected error"); + return; + } + + let docShell = SpecialPowers.wrap(window).docShell; + + info("Test 1: Enable bypass service worker for the docShell"); + + setBypassServiceWorker(docShell, true); + ok(getBypassServiceWorker(docShell), + "The loadFlags in docShell does bypass the serviceWorker by default"); + + let intercepted = await fetchFakeDocAndCheckIfIntercepted(window); + ok(!intercepted, + "The fetched document wasn't intercepted by the serviceWorker"); + + info("Test 2: Disable the bypass service worker for the docShell"); + + setBypassServiceWorker(docShell, false); + ok(!getBypassServiceWorker(docShell), + "The loadFlags in docShell doesn't bypass the serviceWorker by default"); + + intercepted = await fetchFakeDocAndCheckIfIntercepted(window); + ok(intercepted, + "The fetched document was intercepted by the serviceWorker"); +} + +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(async function test_bypassServiceWorker() { + const swURL = "fetch.js"; + let registration = await navigator.serviceWorker.register(swURL); + await waitForState(registration.installing, 'activated'); + + try { + await testBypassSW(); + } catch (e) { + ok(false, "Reason:" + e); + } + + await registration.unregister(); +}); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html b/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html new file mode 100644 index 0000000000..ac27ebcd33 --- /dev/null +++ b/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html @@ -0,0 +1,236 @@ +<html> +<head> + <title>Bug 1251238 - track service worker install time</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<iframe id="iframe"></iframe> +<body> + +<script type="text/javascript"> + +const State = { + BYTECHECK: -1, + PARSED: Ci.nsIServiceWorkerInfo.STATE_PARSED, + INSTALLING: Ci.nsIServiceWorkerInfo.STATE_INSTALLING, + INSTALLED: Ci.nsIServiceWorkerInfo.STATE_INSTALLED, + ACTIVATING: Ci.nsIServiceWorkerInfo.STATE_ACTIVATING, + ACTIVATED: Ci.nsIServiceWorkerInfo.STATE_ACTIVATED, + REDUNDANT: Ci.nsIServiceWorkerInfo.STATE_REDUNDANT +}; +let swm = Cc["@mozilla.org/serviceworkers/manager;1"]. + getService(Ci.nsIServiceWorkerManager); + +let EXAMPLE_URL = "https://example.com/chrome/dom/serviceworkers/test/"; + +let swrlistener = null; +let registrationInfo = null; + +// Use it to keep the sw after unregistration. +let astrayServiceWorkerInfo = null; + +let expectedResults = [ + { + // Speacial state for verifying update since we will do the byte-check + // first. + state: State.BYTECHECK, installedTimeRecorded: false, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.PARSED, installedTimeRecorded: false, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.INSTALLING, installedTimeRecorded: false, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.INSTALLED, installedTimeRecorded: true, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.ACTIVATING, installedTimeRecorded: true, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.ACTIVATED, installedTimeRecorded: true, + activatedTimeRecorded: true, redundantTimeRecorded: false + }, + + // When first being marked as unregistered (but the worker can remain + // actively controlling pages) + { + state: State.ACTIVATED, installedTimeRecorded: true, + activatedTimeRecorded: true, redundantTimeRecorded: false + }, + // When cleared (when idle) + { + state: State.REDUNDANT, installedTimeRecorded: true, + activatedTimeRecorded: true, redundantTimeRecorded: true + }, +]; + +function waitForRegister(aScope, aCallback) { + return new Promise(function (aResolve) { + let listener = { + onRegister (aRegistration) { + if (aRegistration.scope !== aScope) { + return; + } + swm.removeListener(listener); + registrationInfo = aRegistration; + aResolve(); + } + }; + swm.addListener(listener); + }); +} + +function waitForUnregister(aScope) { + return new Promise(function (aResolve) { + let listener = { + onUnregister (aRegistration) { + if (aRegistration.scope !== aScope) { + return; + } + swm.removeListener(listener); + aResolve(); + } + }; + swm.addListener(listener); + }); +} + +function register() { + info("Register a ServiceWorker in the iframe"); + + let iframe = document.querySelector("iframe"); + iframe.src = EXAMPLE_URL + "serviceworkerinfo_iframe.html"; + + let promise = new Promise(function(aResolve) { + iframe.onload = aResolve; + }); + + return promise.then(function() { + iframe.contentWindow.postMessage("register", "*"); + return waitForRegister(EXAMPLE_URL); + }) +} + +function verifyServiceWorkTime(aSWRInfo, resolve) { + let expectedResult = expectedResults.shift(); + ok(!!expectedResult, "We should be able to get test from expectedResults"); + + info("Check the ServiceWorker time in its state is " + expectedResult.state); + + // Get serviceWorkerInfo from swrInfo or get the astray one which we hold. + let swInfo = aSWRInfo.evaluatingWorker || + aSWRInfo.installingWorker || + aSWRInfo.waitingWorker || + aSWRInfo.activeWorker || + astrayServiceWorkerInfo; + + ok(!!aSWRInfo.lastUpdateTime, + "We should do the byte-check and update the update timeStamp"); + + if (!swInfo) { + is(expectedResult.state, State.BYTECHECK, + "We shouldn't get sw when we are notified for first time updating"); + return; + } + + ok(!!swInfo); + + is(expectedResult.state, swInfo.state, + "The service worker's state should be " + swInfo.state + ", but got " + + expectedResult.state); + + is(expectedResult.installedTimeRecorded, !!swInfo.installedTime, + "InstalledTime should be recorded when their state is greater than " + + "INSTALLING"); + + is(expectedResult.activatedTimeRecorded, !!swInfo.activatedTime, + "ActivatedTime should be recorded when their state is greater than " + + "ACTIVATING"); + + is(expectedResult.redundantTimeRecorded, !!swInfo.redundantTime, + "RedundantTime should be recorded when their state is REDUNDANT"); + + // We need to hold sw to avoid losing it since we'll unregister the swr later. + if (expectedResult.state === State.ACTIVATED) { + astrayServiceWorkerInfo = aSWRInfo.activeWorker; + + // Resolve the promise for testServiceWorkerInfo after sw is activated. + resolve(); + } +} + +function testServiceWorkerInfo() { + info("Listen onChange event and verify service worker's information"); + + let promise_resolve; + let promise = new Promise(aResolve => promise_resolve = aResolve); + + swrlistener = { + onChange: () => { + verifyServiceWorkTime(registrationInfo, promise_resolve); + } + }; + + registrationInfo.addListener(swrlistener); + + return promise; +} + +async function testHttpCacheUpdateTime() { + let iframe = document.querySelector("iframe"); + let reg = await iframe.contentWindow.navigator.serviceWorker.getRegistration(); + let lastUpdateTime = registrationInfo.lastUpdateTime; + await reg.update(); + is(lastUpdateTime, registrationInfo.lastUpdateTime, + "The update time should not change when SW script is read from http cache."); +} + +function unregister() { + info("Unregister the ServiceWorker"); + + let iframe = document.querySelector("iframe"); + iframe.contentWindow.postMessage("unregister", "*"); + return waitForUnregister(EXAMPLE_URL); +} + +function cleanAll() { + return new Promise((aResolve, aReject) => { + is(expectedResults.length, 0, "All the tests should be tested"); + + registrationInfo.removeListener(swrlistener); + + swm = null; + swrlistener = null; + registrationInfo = null; + astrayServiceWorkerInfo = null; + aResolve(); + }) +} + +function runTest() { + return Promise.resolve() + .then(register) + .then(testServiceWorkerInfo) + .then(testHttpCacheUpdateTime) + .then(unregister) + .catch(aError => ok(false, "Some test failed with error " + aError)) + .then(cleanAll) + .then(SimpleTest.finish); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] +]}, runTest); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_empty_serviceworker.html b/dom/serviceworkers/test/test_empty_serviceworker.html new file mode 100644 index 0000000000..00b77939f8 --- /dev/null +++ b/dom/serviceworkers/test/test_empty_serviceworker.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that registering an empty service worker works</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("empty.js", {scope: "."}); + } + + function done(registration) { + ok(registration.waiting || registration.active, "registration worked"); + registration.unregister().then(function(success) { + ok(success, "unregister worked"); + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_error_reporting.html b/dom/serviceworkers/test/test_error_reporting.html new file mode 100644 index 0000000000..7c2d56fb9e --- /dev/null +++ b/dom/serviceworkers/test/test_error_reporting.html @@ -0,0 +1,241 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test Error Reporting of Service Worker Failures</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <script src="utils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/** + * Test that a bunch of service worker coding errors and failure modes that + * might otherwise be hard to diagnose are surfaced as console error messages. + * The driving use-case is minimizing cursing from a developer looking at a + * document in Firefox testing a page that involves service workers. + * + * This test assumes that errors will be reported via + * ServiceWorkerManager::ReportToAllClients and that that method is reliable and + * tested via some other file. + **/ + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.testing.enabled", true], + ]}); +}); + +/** + * Ensure an error is logged during the initial registration of a SW when a 404 + * is received. + */ +add_task(async function register_404() { + // Start monitoring for the error + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterNetworkError", + [make_absolute_url("network_error/"), "404", make_absolute_url("404.js")]); + + // Register, generating the 404 error. This will reject with a TypeError + // which we need to consume so it doesn't get thrown at our generator. + await navigator.serviceWorker.register("404.js", { scope: "network_error/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "TypeError", "404 failed as expected"); }); + + await wait_for_expected_message(expectedMessage); +}); + +/** + * Ensure an error is logged when the service worker is being served with a + * MIME type of text/plain rather than a JS type. + */ +add_task(async function register_bad_mime_type() { + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterMimeTypeError2", + [make_absolute_url("bad_mime_type/"), "text/plain", + make_absolute_url("sw_bad_mime_type.js")]); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_bad_mime_type.js", { scope: "bad_mime_type/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", "bad MIME type failed as expected"); }); + + await wait_for_expected_message(expectedMessage); +}); + +async function notAllowStorageAccess() { + throw new Error("Storage permissions should be used when bug 1774860 overhauls this test."); +} + +async function allowStorageAccess() { + throw new Error("Storage permissions should be used when bug 1774860 overhauls this test."); +} + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to register a service worker. + */ +add_task(async function register_storage_error() { + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterStorageError", + [make_absolute_url("storage_not_allow/")]); + + await notAllowStorageAccess(); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "storage_not_allow/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to get the service worker registration. + */ +add_task(async function get_registration_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetRegistrationStorageError", []); + + await notAllowStorageAccess(); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.getRegistration() + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to get the service worker registrations. + */ +add_task(async function get_registrations_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetRegistrationStorageError", []); + + await notAllowStorageAccess(); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.getRegistrations() + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to post a message to the service worker. + */ +add_task(async function postMessage_storage_error() { + let expectedMessage = expect_console_message( + "ServiceWorkerPostMessageStorageError", + [make_absolute_url("storage_not_allow/")]); + + let registration; + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "storage_not_allow/" }) + .then(reg => { registration = reg; }) + .then(() => notAllowStorageAccess()) + .then(() => registration.installing || + registration.waiting || + registration.active) + .then(worker => worker.postMessage('ha')) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await registration.unregister(); + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the service + * worker is trying to get its client. + */ +add_task(async function get_client_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetClientStorageError", []); + + await SpecialPowers.pushPrefEnv({"set": [ + // Make the test pass the IsOriginPotentiallyTrustworthy. + ["dom.securecontext.allowlist", "mochi.test"] + ]}); + + let registration; + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "test_error_reporting.html" }) + .then(reg => { + registration = reg; + return waitForState(registration.installing, "activated"); + }) + // Get the client's ID in the stage 1 + .then(() => fetch("getClient-stage1")) + .then(() => notAllowStorageAccess()) + // Trigger the clients.get() in the stage 2 + .then(() => fetch("getClient-stage2")) + .catch(e => ok(false, "fail due to:" + e)); + + await wait_for_expected_message(expectedMessage); + + await registration.unregister(); + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the service + * worker is trying to get its clients. + */ +add_task(async function get_clients_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetClientStorageError", []); + + let registration; + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "test_error_reporting.html" }) + .then(reg => { + registration = reg; + return waitForState(registration.installing, "activated"); + }) + .then(() => notAllowStorageAccess()) + .then(() => fetch("getClients")) + .catch(e => ok(false, "fail due to:" + e)); + + await wait_for_expected_message(expectedMessage); + + await registration.unregister(); + await allowStorageAccess(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_escapedSlashes.html b/dom/serviceworkers/test/test_escapedSlashes.html new file mode 100644 index 0000000000..001c660242 --- /dev/null +++ b/dom/serviceworkers/test/test_escapedSlashes.html @@ -0,0 +1,102 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for escaped slashes in navigator.serviceWorker.register</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +var tests = [ + { status: true, + scriptURL: "a.js?foo%2fbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%2fbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%2Fbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%2Fbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%5cbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%5cbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%2Cbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%5Cbar", + scopeURL: null }, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%2fbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "/foo%2fbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%2Fbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%2Fbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%5cbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%5cbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%5Cbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%5Cbar"}, +]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + navigator.serviceWorker.register(test.scriptURL, test.scopeURL) + .then(reg => { + ok(false, "Register should fail"); + }, err => { + if (!test.status) { + is(err.name, "TypeError", "Registration should fail with TypeError"); + } else { + ok(test.status, "Register should fail"); + } + }) + .then(runTest); +} + +SimpleTest.waitForExplicitFinish(); +onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); +}; + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_eval_allowed.html b/dom/serviceworkers/test/test_eval_allowed.html new file mode 100644 index 0000000000..5d6d7a7d9c --- /dev/null +++ b/dom/serviceworkers/test/test_eval_allowed.html @@ -0,0 +1,51 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1160458 - CSP activated by default in Service Workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + function register() { + return navigator.serviceWorker.register("eval_worker.js"); + } + + function runTest() { + try { + eval("1"); + ok(false, "should throw"); + } + catch (ex) { + ok(true, "did throw"); + } + register() + .then(function(swr) { + ok(true, "eval restriction didn't get inherited"); + swr.unregister() + .then(function() { + SimpleTest.finish(); + }); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_eval_allowed.html^headers^ b/dom/serviceworkers/test/test_eval_allowed.html^headers^ new file mode 100644 index 0000000000..51ffaa71dd --- /dev/null +++ b/dom/serviceworkers/test/test_eval_allowed.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self'" diff --git a/dom/serviceworkers/test/test_event_listener_leaks.html b/dom/serviceworkers/test/test_event_listener_leaks.html new file mode 100644 index 0000000000..33ffeb44c4 --- /dev/null +++ b/dom/serviceworkers/test/test_event_listener_leaks.html @@ -0,0 +1,63 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1447871 - Test some service worker leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="utils.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<script class="testbody" type="text/javascript"> + +const scope = new URL("empty.html?leak_tests", location).href; +const script = new URL("empty.js", location).href; + +// Manipulate service worker DOM objects in the frame's context. +// Its important here that we create a listener callback from +// the DOM objects back to the frame's global in order to +// exercise the leak condition. +async function useServiceWorker(contentWindow) { + contentWindow.navigator.serviceWorker.oncontrollerchange = _ => { + contentWindow.controlledChangeCount += 1; + }; + let reg = await contentWindow.navigator.serviceWorker.getRegistration(scope); + reg.onupdatefound = _ => { + contentWindow.updateCount += 1; + }; + reg.active.onstatechange = _ => { + contentWindow.stateChangeCount += 1; + }; +} + +async function runTest() { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}); + + let reg = await navigator.serviceWorker.register(script, { scope }); + await waitForState(reg.installing, "activated"); + + try { + await checkForEventListenerLeaks("ServiceWorker", useServiceWorker); + } catch (e) { + ok(false, e); + } finally { + await reg.unregister(); + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_eventsource_intercept.html b/dom/serviceworkers/test/test_eventsource_intercept.html new file mode 100644 index 0000000000..b76c0d1d1a --- /dev/null +++ b/dom/serviceworkers/test/test_eventsource_intercept.html @@ -0,0 +1,103 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(e) { + if (e.data.status == "callback") { + switch(e.data.data) { + case "ok": + ok(e.data.condition, e.data.message); + break; + case "ready": + iframe.contentWindow.postMessage({status: "callback", data: "eventsource"}, "*"); + break; + case "done": + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + resolve(); + break; + default: + ok(false, "Something went wrong"); + break; + } + } else { + ok(false, "Something went wrong"); + } + }; + document.body.appendChild(iframe); + }); + } + + function runTest() { + Promise.resolve() + .then(() => { + info("Going to intercept and test opaque responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_opaque_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_opaque_response.html"); + }) + .then(() => { + info("Going to intercept and test cors responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_cors_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_cors_response.html"); + }) + .then(() => { + info("Going to intercept and test synthetic responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_synthetic_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_synthetic_response.html"); + }) + .then(() => { + info("Going to intercept and test mixed content cors responses"); + return testFrame("https://example.com/tests/dom/serviceworkers/test/" + + "eventsource/eventsource_register_worker.html" + + "?script=eventsource_mixed_content_cors_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("https://example.com/tests/dom/serviceworkers/test/" + + "eventsource/eventsource_mixed_content_cors_response.html"); + }) + .then(SimpleTest.finish) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_fetch_event.html b/dom/serviceworkers/test/test_fetch_event.html new file mode 100644 index 0000000000..5227f6ae34 --- /dev/null +++ b/dom/serviceworkers/test/test_fetch_event.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + return navigator.serviceWorker.register("fetch_event_worker.js", { scope: "./fetch" }) + .then(swr => { + registration = swr; + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + w.close(); + resolve(); + } + } + }); + + var w = window.open("fetch/index.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html b/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html new file mode 100644 index 0000000000..53552e03c3 --- /dev/null +++ b/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html @@ -0,0 +1,90 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + // NOTE: This is just test_fetch_event.html but with an alternate cookie + // mode preference set to make sure that setting the preference does + // not break interception as observed in bug 1336364. + // TODO: Refactor this test so it doesn't duplicate so much code logic. + + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + return navigator.serviceWorker.register("fetch_event_worker.js", { scope: "./fetch" }) + .then(swr => { + registration = swr; + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + var reloaded = false; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + if (reloaded) { + window.onmessage = null; + w.close(); + resolve(); + } else { + w.location.reload(); + reloaded = true; + } + } + } + }); + + var w = window.open("fetch/index.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + const COOKIE_BEHAVIOR_REJECTFOREIGN = 1; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_REJECTFOREIGN], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_fetch_integrity.html b/dom/serviceworkers/test/test_fetch_integrity.html new file mode 100644 index 0000000000..35879d5749 --- /dev/null +++ b/dom/serviceworkers/test/test_fetch_integrity.html @@ -0,0 +1,228 @@ +<!DOCTYPE HTML> +<html> +<head> + <title> Test fetch.integrity on console report for serviceWorker and sharedWorker </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> +<div id="content" style="display: none"></div> +<script src="utils.js"></script> +<script type="text/javascript"> +"use strict"; + +let security_localizer = + stringBundleService.createBundle("chrome://global/locale/security/security.properties"); + +let consoleScript; +let monitorCallbacks = []; + +function registerConsoleMonitor() { + return new Promise(resolve => { + var url = SimpleTest.getTestFileURL("console_monitor.js"); + consoleScript = SpecialPowers.loadChromeScript(url); + + consoleScript.addMessageListener("ready", resolve); + consoleScript.addMessageListener("monitor", function(msg) { + for (let i = 0; i < monitorCallbacks.length;) { + if (monitorCallbacks[i](msg)) { + ++i; + } else { + monitorCallbacks.splice(i, 1); + } + } + }); + consoleScript.sendAsyncMessage("load", {}); + }); +} + +function unregisterConsoleMonitor() { + return new Promise(resolve => { + consoleScript.addMessageListener("unloaded", () => { + consoleScript.destroy(); + resolve(); + }); + consoleScript.sendAsyncMessage("unload", {}); + }); +} + +function registerConsoleMonitorCallback(callback) { + monitorCallbacks.push(callback); +} + +function waitForMessages() { + let messages = []; + + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 3) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + messages.push(security_localizer.formatStringFromName(msgId, args)); + } + + return new Promise(resolve => { + registerConsoleMonitorCallback(msg => { + for (let i = 0; i < messages.length; ++i) { + if (messages[i] == msg.errorMessage) { + messages.splice(i, 1); + break; + } + } + + if (!messages.length) { + resolve(); + return false; + } + + return true; + }); + }); +} + +function expect_security_console_message(/* msgId, args, ... */) { + let expectations = []; + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 3) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + let filename = arguments[i + 2]; + expectations.push({ + errorMessage: security_localizer.formatStringFromName(msgId, args), + sourceName: filename, + }); + } + return new Promise(resolve => { + SimpleTest.monitorConsole(resolve, expectations); + }); +} + +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.newtab.preload", false], + ]}); +}); + +add_task(async function test_integrity_serviceWorker() { + var filename = make_absolute_url("fetch.js"); + var filename2 = make_absolute_url("fake.html"); + + let registration = await navigator.serviceWorker.register("fetch.js", + { scope: "./" }); + await waitForState(registration.installing, "activated"); + + info("Test for mNavigationInterceptions.") + // The client_win will reload to another URL after opening filename2. + let client_win = window.open(filename2); + + let expectedMessage = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + let expectedMessage2 = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + + info("Test for mControlledDocuments and report error message to console."); + // The fetch will succeed because the integrity value is invalid and we are + // looking for the console message regarding the bad integrity value. + await fetch("fail.html"); + + await wait_for_expected_message(expectedMessage); + + await wait_for_expected_message(expectedMessage2); + + await registration.unregister(); + client_win.close(); +}); + +add_task(async function test_integrity_sharedWorker() { + var filename = make_absolute_url("sharedWorker_fetch.js"); + + await registerConsoleMonitor(); + + info("Attach main window to a SharedWorker."); + let sharedWorker = new SharedWorker(filename); + let waitForConnected = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "Connected") { + resolve(); + } else { + reject(); + } + } + }); + await waitForConnected; + + info("Attch another window to the same SharedWorker."); + // Open another window and its also managed by the shared worker. + let client_win = window.open("create_another_sharedWorker.html"); + let waitForBothConnected = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "BothConnected") { + resolve(); + } else { + reject(); + } + } + }); + await waitForBothConnected; + + let expectedMessage = waitForMessages( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + + let expectedMessage2 = waitForMessages( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + + info("Start to fetch a URL with wrong integrity.") + sharedWorker.port.start(); + sharedWorker.port.postMessage("StartFetchWithWrongIntegrity"); + + let waitForSRIFailed = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "SRI_failed") { + resolve(); + } else { + reject(); + } + } + }); + await waitForSRIFailed; + + await expectedMessage; + await expectedMessage2; + + client_win.close(); + + await unregisterConsoleMonitor(); +}); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_file_blob_response.html b/dom/serviceworkers/test/test_file_blob_response.html new file mode 100644 index 0000000000..3aa72c3dda --- /dev/null +++ b/dom/serviceworkers/test/test_file_blob_response.html @@ -0,0 +1,78 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1253777 - Test interception using file blob response body</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var scope = './file_blob_response/'; + function start() { + return navigator.serviceWorker.register("file_blob_response_worker.js", + { scope }) + .then(function(swr) { + registration = swr; + return new waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e + "\n"); + }); + } + + function withFrame(url) { + return new Promise(function(resolve, reject) { + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + var frame = document.createElement("iframe"); + frame.setAttribute('src', url); + content.appendChild(frame); + + frame.addEventListener('load', function(evt) { + resolve(frame); + }, {once: true}); + }); + } + + function runTest() { + start() + .then(function() { + return withFrame(scope + 'dummy.txt'); + }) + .then(function(frame) { + var result = JSON.parse(frame.contentWindow.document.body.textContent); + frame.remove(); + is(result.value, 'success'); + }) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }) + .then(unregister) + .then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_file_blob_upload.html b/dom/serviceworkers/test/test_file_blob_upload.html new file mode 100644 index 0000000000..e60e65badd --- /dev/null +++ b/dom/serviceworkers/test/test_file_blob_upload.html @@ -0,0 +1,146 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1203680 - Test interception of file blob uploads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var iframe; + function start() { + return navigator.serviceWorker.register("empty.js", + { scope: "./sw_clients/" }) + .then((swr) => { + registration = swr + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + if (iframe) { + iframe.remove(); + iframe = null; + } + + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e + "\n"); + }); + } + + function withFrame() { + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/file_blob_upload_frame.html"); + content.appendChild(iframe); + + return new Promise(function(resolve, reject) { + window.addEventListener('message', function(evt) { + if (evt.data.status === 'READY') { + resolve(); + } else { + reject(evt.data.result); + } + }, {once: true}); + }); + } + + function postBlob(body) { + return new Promise(function(resolve, reject) { + window.addEventListener('message', function(evt) { + if (evt.data.status === 'OK') { + is(JSON.stringify(body), JSON.stringify(evt.data.result), + 'body echoed back correctly'); + resolve(); + } else { + reject(evt.data.result); + } + }, {once: true}); + + iframe.contentWindow.postMessage({ type: 'TEST', body }, '*'); + }); + } + + function generateMessage(length) { + + var lorem = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas ' + 'vehicula tortor eget ultrices. Sed et luctus est. Nunc eu orci ligula. ' + 'In vel ornare eros, eget lacinia diam. Praesent vel metus mattis, ' + 'cursus nulla sit amet, rhoncus diam. Aliquam nulla tortor, aliquet et ' + 'viverra non, dignissim vel tellus. Praesent sed ex in dolor aliquet ' + 'aliquet. In at facilisis sem, et aliquet eros. Maecenas feugiat nisl ' + 'quis elit blandit posuere. Duis viverra odio sed eros consectetur, ' + 'viverra mattis ligula volutpat.'; + + var result = ''; + + while (result.length < length) { + var remaining = length - result.length; + if (remaining < lorem.length) { + result += lorem.slice(0, remaining); + } else { + result += lorem; + } + } + + return result; + } + + var smallBody = generateMessage(64); + var mediumBody = generateMessage(1024); + + // TODO: Test large bodies over the default pipe size. Currently stalls + // due to bug 1134372. + //var largeBody = generateMessage(100 * 1024); + + function runTest() { + start() + .then(withFrame) + .then(function() { + return postBlob({ hops: 0, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 1, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 10, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 0, message: mediumBody }); + }) + .then(function() { + return postBlob({ hops: 1, message: mediumBody }); + }) + .then(function() { + return postBlob({ hops: 10, message: mediumBody }); + }) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_file_upload.html b/dom/serviceworkers/test/test_file_upload.html new file mode 100644 index 0000000000..0c502686af --- /dev/null +++ b/dom/serviceworkers/test/test_file_upload.html @@ -0,0 +1,68 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1424701 - Test for service worker + file upload</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<input id="input" type="file"> +<script class="testbody" type="text/javascript"> + +function GetFormData(file) { + const formData = new FormData(); + formData.append('file', file); + return formData; +} + +async function onOpened(message) { + let input = document.getElementById("input"); + SpecialPowers.wrap(input).mozSetFileArray([message.file]); + script.destroy(); + + let reg = await navigator.serviceWorker.register('sw_file_upload.js', + {scope: "." }); + let serviceWorker = reg.installing || reg.waiting || reg.active; + await waitForState(serviceWorker, 'activated'); + + let res = await fetch('server_file_upload.sjs?clone=0', { + method: 'POST', + body: input.files[0], + }); + + let data = await res.clone().text(); + ok(data.length, "We have data for an uncloned request!"); + + res = await fetch('server_file_upload.sjs?clone=1', { + method: 'POST', + // Make sure the underlying stream is a file stream + body: GetFormData(input.files[0]), + }); + + data = await res.clone().text(); + ok(data.length, "We have data for a file-stream-backed cloned request!"); + + await reg.unregister(); + SimpleTest.finish(); +} + +let url = SimpleTest.getTestFileURL("script_file_upload.js"); +let script = SpecialPowers.loadChromeScript(url); + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] +]}).then(() => { + script.addMessageListener("file.opened", onOpened); + script.sendAsyncMessage("file.open"); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_force_refresh.html b/dom/serviceworkers/test/test_force_refresh.html new file mode 100644 index 0000000000..69da7b7de3 --- /dev/null +++ b/dom/serviceworkers/test/test_force_refresh.html @@ -0,0 +1,105 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + /** + * + */ + let iframe; + let registration; + + function start() { + return new Promise(resolve => { + const content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute("src", "sw_clients/refresher_compressed.html"); + + /* + * The initial iframe must be the _uncached_ version, which means its + * load must happen before the Service Worker's `activate` event. + * Rather than `waitUntil`-ing the Service Worker's `install` event + * until the load finishes (more concurrency, but involves coordinating + * `postMessage`s), just ensure the load finishes before registering + * the Service Worker (which is simpler). + */ + iframe.onload = resolve; + + content.appendChild(iframe); + }).then(async () => { + /* + * There's no need _here_ to explicitly wait for this Service Worker to be + * "activated"; this test will progress when the "READY"/"READY_CACHED" + * messages are received from the iframe, and the iframe will only send + * those messages once the Service Worker is "activated" (by chaining on + * its `navigator.serviceWorker.ready` promise). + */ + registration = await navigator.serviceWorker.register( + "force_refresh_worker.js", { scope: "./sw_clients/" }); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testForceRefresh(swr) { + return new Promise(function(res, rej) { + var count = 0; + var cachedCount = 0; + window.onmessage = function(e) { + if (e.data === "READY") { + count += 1; + if (count == 2) { + is(cachedCount, 1, "should have received cached message before " + + "second non-cached message"); + res(); + } + iframe.contentWindow.postMessage("REFRESH", "*"); + } else if (e.data === "READY_CACHED") { + cachedCount += 1; + is(count, 1, "should have received non-cached message before " + + "cached message"); + iframe.contentWindow.postMessage("FORCE_REFRESH", "*"); + } + } + }).then(() => document.getElementById("content").removeChild(iframe)); + } + + function runTest() { + start() + .then(testForceRefresh) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_gzip_redirect.html b/dom/serviceworkers/test/test_gzip_redirect.html new file mode 100644 index 0000000000..8119303ae7 --- /dev/null +++ b/dom/serviceworkers/test/test_gzip_redirect.html @@ -0,0 +1,88 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + function start() { + return navigator.serviceWorker.register("gzip_redirect_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testGzipRedirect(swr) { + var p = new Promise(function(res, rej) { + var navigatorReady = false; + var finalReady = false; + + window.onmessage = function(e) { + if (e.data === "NAVIGATOR_READY") { + ok(!navigatorReady, "should only get navigator ready message once"); + ok(!finalReady, "should get navigator ready before final redirect ready message"); + navigatorReady = true; + iframe.contentWindow.postMessage({ + type: "NAVIGATE", + url: "does_not_exist.html" + }, "*"); + } else if (e.data === "READY") { + ok(navigatorReady, "should only get navigator ready message once"); + ok(!finalReady, "should get final ready message only once"); + finalReady = true; + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/navigator.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testGzipRedirect) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_hsts_upgrade_intercept.html b/dom/serviceworkers/test/test_hsts_upgrade_intercept.html new file mode 100644 index 0000000000..59fef0ec14 --- /dev/null +++ b/dom/serviceworkers/test/test_hsts_upgrade_intercept.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that an HSTS upgraded request can be intercepted by a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + var framesLoaded = 0; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "http://example.com/tests/dom/serviceworkers/test/fetch/hsts/index.html"; + } else if (e.data.status == "protocol") { + is(e.data.data, "https:", "Correct protocol expected"); + ok(e.data.securityInfoPresent, "Security info present on intercepted value"); + switch (++framesLoaded) { + case 1: + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/embedder.html"; + break; + case 2: + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/image.html"; + break; + } + } else if (e.data.status == "image") { + is(e.data.data, 40, "The image request was upgraded before interception"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SpecialPowers.cleanUpSTSData("http://example.com"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // This is needed so that we can test upgrading a non-secure load inside an https iframe. + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_fetch.html b/dom/serviceworkers/test/test_https_fetch.html new file mode 100644 index 0000000000..4ac4255889 --- /dev/null +++ b/dom/serviceworkers/test/test_https_fetch.html @@ -0,0 +1,62 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1133763 - test fetch event in HTTPS origins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/index.html"; + } else if (e.data.status == "done") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth-sw.html"; + } else if (e.data.status == "done-synth-sw") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth-window.html"; + } else if (e.data.status == "done-synth-window") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth.html"; + } else if (e.data.status == "done-synth") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_fetch_cloned_response.html b/dom/serviceworkers/test/test_https_fetch_cloned_response.html new file mode 100644 index 0000000000..8c7129d39d --- /dev/null +++ b/dom/serviceworkers/test/test_https_fetch_cloned_response.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1133763 - test fetch event in HTTPS origins with a cloned response</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/index.html"; + } else if (e.data.status == "done") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_origin_after_redirect.html b/dom/serviceworkers/test/test_https_origin_after_redirect.html new file mode 100644 index 0000000000..31ce173f34 --- /dev/null +++ b/dom/serviceworkers/test/test_https_origin_after_redirect.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/index-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html b/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html new file mode 100644 index 0000000000..8bce413f21 --- /dev/null +++ b/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/index-cached-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html b/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html new file mode 100644 index 0000000000..4186cfc340 --- /dev/null +++ b/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1156847 - test fetch event generating a synthesized response in HTTPS origins from a cached SW</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" tyle="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + + // In order to load synth.html from a cached service worker, we first + // remove the existing window that is keeping the service worker alive, + // and do a GC to ensure that the SW is destroyed. This way, when we + // load synth.html for the second time, we will first recreate the + // service worker from the cache. This is intended to test that we + // properly store and retrieve the security info from the cache. + iframe.remove(); + iframe = null; + SpecialPowers.exactGC(function() { + iframe = document.createElement("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth.html"; + document.body.appendChild(iframe); + }); + } else if (e.data.status == "done-synth") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_imagecache.html b/dom/serviceworkers/test/test_imagecache.html new file mode 100644 index 0000000000..52a793bfb9 --- /dev/null +++ b/dom/serviceworkers/test/test_imagecache.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1202085 - Test that images from different controllers don't cached together</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/index.html"; + } else if (e.data.status == "result") { + is(e.data.url, "image-40px.png", "Correct url expected"); + is(e.data.width, 40, "Correct width expected"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/postmortem.html"; + } else if (e.data.status == "postmortem") { + is(e.data.width, 20, "Correct width expected"); + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_imagecache_max_age.html b/dom/serviceworkers/test/test_imagecache_max_age.html new file mode 100644 index 0000000000..fcb8d3e306 --- /dev/null +++ b/dom/serviceworkers/test/test_imagecache_max_age.html @@ -0,0 +1,71 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that the image cache respects a synthesized image's Cache headers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + var framesLoaded = 0; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/index.html"; + } else if (e.data.status == "result") { + switch (++framesLoaded) { + case 1: + is(e.data.url, "image-20px.png", "Correct url expected"); + is(e.data.url2, "image-20px.png", "Correct url expected"); + is(e.data.width, 20, "Correct width expected"); + is(e.data.width2, 20, "Correct width expected"); + // Wait for 100ms so that the image gets expired. + setTimeout(function() { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/index.html?new" + }, 100); + break; + case 2: + is(e.data.url, "image-40px.png", "Correct url expected"); + is(e.data.url2, "image-40px.png", "Correct url expected"); + is(e.data.width, 40, "Correct width expected"); + is(e.data.width2, 40, "Correct width expected"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html"; + break; + default: + ok(false, "This should never happen"); + } + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SimpleTest.finish(); + } + }; + } + + SimpleTest.requestFlakyTimeout("This test needs to simulate the passing of time"); + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_importscript.html b/dom/serviceworkers/test/test_importscript.html new file mode 100644 index 0000000000..c0a894cf3c --- /dev/null +++ b/dom/serviceworkers/test/test_importscript.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test service worker - script cache policy</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"></div> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + function start() { + return navigator.serviceWorker.register("importscript_worker.js", + { scope: "./sw_clients/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then(swr => registration = swr); + } + + function unregister() { + return fetch("importscript.sjs?clearcounter").then(function() { + return registration.unregister(); + }).then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + swr.active.postMessage("do magic"); + return; + } + + ok(e.data === "OK", "Worker posted the correct value: " + e.data); + res(); + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_importscript_mixedcontent.html b/dom/serviceworkers/test/test_importscript_mixedcontent.html new file mode 100644 index 0000000000..15fe5e88b6 --- /dev/null +++ b/dom/serviceworkers/test/test_importscript_mixedcontent.html @@ -0,0 +1,53 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1198078 - test that we respect mixed content blocking in importScript() inside service workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/index.html"; + } else if (e.data.status == "done") { + is(e.data.data, "good", "Mixed content blocking should work correctly for service workers"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["security.mixed_content.block_active_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_install_event.html b/dom/serviceworkers/test/test_install_event.html new file mode 100644 index 0000000000..87f89725dc --- /dev/null +++ b/dom/serviceworkers/test/test_install_event.html @@ -0,0 +1,143 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + var p = navigator.serviceWorker.register("worker.js", { scope: "./install_event" }); + return p; + } + + function nextRegister(reg) { + ok(reg instanceof ServiceWorkerRegistration, "reg should be a ServiceWorkerRegistration"); + var p = navigator.serviceWorker.register("install_event_worker.js", { scope: "./install_event" }); + return p.then(function(swr) { + ok(reg === swr, "register should resolve to the same registration object"); + var update_found_promise = new Promise(function(resolve, reject) { + swr.addEventListener('updatefound', function(e) { + ok(true, "Received onupdatefound"); + resolve(); + }); + }); + + var worker_activating = new Promise(function(res, reject) { + ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves."); + ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'"); + swr.installing.onstatechange = function(e) { + if (e.target.state == "activating") { + e.target.onstatechange = null; + res(); + } + } + }); + + return Promise.all([update_found_promise, worker_activating]); + }, function(e) { + ok(false, "Unexpected Error in nextRegister! " + e); + }); + } + + function installError() { + // Silence worker errors so they don't cause the test to fail. + window.onerror = function(e) {} + return navigator.serviceWorker.register("install_event_error_worker.js", { scope: "./install_event" }) + .then(function(swr) { + ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves."); + ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'"); + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + ok(e.target.state == "redundant", "Installation of worker with error should fail."); + resolve(); + } + }); + }).then(function() { + return navigator.serviceWorker.getRegistration("./install_event").then(function(swr) { + var newest = swr.waiting || swr.active; + ok(newest, "Waiting or active worker should still exist"); + ok(newest.scriptURL.match(/install_event_worker.js$/), "Previous worker should remain the newest worker"); + }); + }); + } + + function testActive(worker) { + is(worker.state, "activating", "Should be activating"); + return new Promise(function(resolve, reject) { + worker.onstatechange = function(e) { + e.target.onstatechange = null; + is(e.target.state, "activated", "Activation of worker with error in activate event handler should still succeed."); + resolve(); + } + }); + } + + function activateErrorShouldSucceed() { + // Silence worker errors so they don't cause the test to fail. + window.onerror = function() { } + return navigator.serviceWorker.register("activate_event_error_worker.js", { scope: "./activate_error" }) + .then(function(swr) { + var p = new Promise(function(resolve, reject) { + ok(swr.installing.state == "installing", "activateErrorShouldSucceed(): Installing worker's state should be 'installing'"); + swr.installing.onstatechange = function(e) { + e.target.onstatechange = null; + if (swr.waiting) { + swr.waiting.onstatechange = function(event) { + event.target.onstatechange = null; + testActive(swr.active).then(resolve, reject); + } + } else { + testActive(swr.active).then(resolve, reject); + } + } + }); + + return p.then(function() { + return Promise.resolve(swr); + }); + }).then(function(swr) { + return swr.unregister(); + }); + } + + function unregister() { + return navigator.serviceWorker.getRegistration("./install_event").then(function(reg) { + return reg.unregister(); + }); + } + + function runTest() { + Promise.resolve() + .then(simpleRegister) + .then(nextRegister) + .then(installError) + .then(activateErrorShouldSucceed) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_install_event_gc.html b/dom/serviceworkers/test/test_install_event_gc.html new file mode 100644 index 0000000000..8b68b8ac47 --- /dev/null +++ b/dom/serviceworkers/test/test_install_event_gc.html @@ -0,0 +1,121 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test install event being GC'd before waitUntil fulfills</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +var script = 'blocking_install_event_worker.js'; +var scope = 'sw_clients/simple.html?install-event-gc'; +var registration; + +function register() { + return navigator.serviceWorker.register(script, { scope }) + .then(swr => registration = swr); +} + +function unregister() { + if (!registration) { + return; + } + return registration.unregister(); +} + +function waitForInstallEvent() { + return new Promise((resolve, reject) => { + navigator.serviceWorker.addEventListener('message', evt => { + if (evt.data.type === 'INSTALL_EVENT') { + resolve(); + } + }); + }); +} + +function gcWorker() { + return new Promise(function(resolve, reject) { + // We are able to trigger asynchronous garbage collection and cycle + // collection by emitting "child-cc-request" and "child-gc-request" + // observer notifications. The worker RuntimeService will translate + // these notifications into the appropriate operation on all known + // worker threads. + // + // In the failure case where GC/CC causes us to abort the installation, + // we will know something happened from the statechange event. + const statechangeHandler = evt => { + // Reject rather than resolving to avoid the possibility of us seeing + // an unrelated racing statechange somehow. Since in the success case we + // will still see a state change on termination, we do explicitly need to + // be removed on the success path. + ok(registration.installing, 'service worker is still installing?'); + reject(); + }; + registration.installing.addEventListener('statechange', statechangeHandler); + // In the success case since the service worker installation is effectively + // hung, we instead depend on sending a 'ping' message to the service worker + // and hearing it 'pong' back. Since we issue our postMessage after we + // trigger the GC/CC, our 'ping' will only be processed after the GC/CC and + // therefore the pong will also strictly occur after the cycle collection. + navigator.serviceWorker.addEventListener('message', evt => { + if (evt.data.type === 'pong') { + registration.installing.removeEventListener( + 'statechange', statechangeHandler); + resolve(); + } + }); + // At the current time, the service worker will exist in our same process + // and notifyObservers is synchronous. However, in the future, service + // workers may end up in a separate process and in that case it will be + // appropriate to use notifyObserversInParentProcess or something like it. + // (notifyObserversInParentProcess is a synchronous IPC call to the parent + // process's main thread. IPDL PContent::CycleCollect is an async message. + // Ordering will be maintained if the postMessage goes via PContent as well, + // but that seems unlikely.) + SpecialPowers.notifyObservers(null, 'child-gc-request'); + SpecialPowers.notifyObservers(null, 'child-cc-request'); + SpecialPowers.notifyObservers(null, 'child-gc-request'); + // (Only send the ping after we set the gc/cc/gc in motion.) + registration.installing.postMessage({ type: 'ping' }); + }); +} + +function terminateWorker() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 0] + ] + }).then(_ => { + registration.installing.postMessage({ type: 'RESET_TIMER' }); + }); +} + +function runTest() { + Promise.all([ + waitForInstallEvent(), + register() + ]).then(_ => ok(registration.installing, 'service worker is installing')) + .then(gcWorker) + .then(_ => ok(registration.installing, 'service worker is still installing')) + .then(terminateWorker) + .catch(e => ok(false, e)) + .then(unregister) + .then(SimpleTest.finish); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], +]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_installation_simple.html b/dom/serviceworkers/test/test_installation_simple.html new file mode 100644 index 0000000000..69c9518ea0 --- /dev/null +++ b/dom/serviceworkers/test/test_installation_simple.html @@ -0,0 +1,208 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + var p = navigator.serviceWorker.register("worker.js", { scope: "simpleregister/" }); + ok(p instanceof Promise, "register() should return a Promise"); + return Promise.resolve(); + } + + function sameOriginWorker() { + p = navigator.serviceWorker.register("http://some-other-origin/worker.js"); + return p.then(function(w) { + ok(false, "Worker from different origin should fail"); + }, function(e) { + ok(e.name === "SecurityError", "Should fail with a SecurityError"); + }); + } + + function sameOriginScope() { + p = navigator.serviceWorker.register("worker.js", { scope: "http://www.example.com/" }); + return p.then(function(w) { + ok(false, "Worker controlling scope for different origin should fail"); + }, function(e) { + ok(e.name === "SecurityError", "Should fail with a SecurityError"); + }); + } + + function httpsOnly() { + return SpecialPowers.pushPrefEnv({'set': [["dom.serviceWorkers.testing.enabled", false]] }) + .then(function() { + return navigator.serviceWorker.register("/worker.js"); + }).then(function(w) { + ok(false, "non-HTTPS pages cannot register ServiceWorkers"); + }, function(e) { + ok(e.name === "TypeError", "navigator.serviceWorker should be undefined"); + }).then(function() { + return SpecialPowers.popPrefEnv(); + }); + } + + function realWorker() { + var p = navigator.serviceWorker.register("worker.js", { scope: "realworker" }); + return p.then(function(wr) { + ok(wr instanceof ServiceWorkerRegistration, "Register a ServiceWorker"); + + info(wr.scope); + ok(wr.scope == (new URL("realworker", document.baseURI)).href, "Scope should match"); + // active, waiting, installing should return valid worker instances + // because the registration is for the realworker scope, so the workers + // should be obtained for that scope and not for + // test_installation_simple.html + var worker = wr.installing; + ok(worker && wr.scope.match(/realworker$/) && + worker.scriptURL.match(/worker.js$/), "Valid worker instance should be available."); + return wr.unregister().then(function(success) { + ok(success, "The worker should be unregistered successfully"); + }, function(e) { + dump("Error unregistering the worker: " + e + "\n"); + }); + }, function(e) { + info("Error: " + e.name); + ok(false, "realWorker Registration should have succeeded!"); + }); + } + + function networkError404() { + return navigator.serviceWorker.register("404.js", { scope: "network_error/"}).then(function(w) { + ok(false, "404 response should fail with TypeError"); + }, function(e) { + ok(e.name === "TypeError", "404 response should fail with TypeError"); + }); + } + + function redirectError() { + return navigator.serviceWorker.register("redirect_serviceworker.sjs", { scope: "redirect_error/" }).then(function(swr) { + ok(false, "redirection should fail"); + }, function (e) { + ok(e.name === "SecurityError", "redirection should fail with SecurityError"); + }); + } + + function parseError() { + var p = navigator.serviceWorker.register("parse_error_worker.js", { scope: "parse_error/" }); + return p.then(function(wr) { + ok(false, "Registration should fail with parse error"); + return navigator.serviceWorker.getRegistration("parse_error/").then(function(swr) { + // See https://github.com/slightlyoff/ServiceWorker/issues/547 + is(swr, undefined, "A failed registration for a scope with no prior controllers should clear itself"); + }); + }, function(e) { + ok(e instanceof Error, "Registration should fail with parse error"); + }); + } + + // FIXME(nsm): test for parse error when Update step doesn't happen (directly from register). + + function updatefound() { + var frame = document.createElement("iframe"); + frame.setAttribute("id", "simpleregister-frame"); + frame.setAttribute("src", new URL("simpleregister/index.html", document.baseURI).href); + document.body.appendChild(frame); + var resolve, reject; + var p = new Promise(function(res, rej) { + resolve = res; + reject = rej; + }); + + var regPromise; + function continueTest() { + regPromise = navigator.serviceWorker.register( + "worker2.js", { scope: "simpleregister/" }); + } + + window.onmessage = function(e) { + if (e.data.type == "ready") { + continueTest(); + } else if (e.data.type == "finish") { + window.onmessage = null; + // We have to make frame navigate away, otherwise it will call + // MaybeStopControlling() when this document is unloaded. At that point + // the pref has been disabled, so the ServiceWorkerManager is not available. + frame.setAttribute("src", new URL("about:blank").href); + regPromise.then(function(reg) { + reg.unregister().then(function(success) { + ok(success, "The worker should be unregistered successfully"); + resolve(); + }, function(error) { + dump("Error unregistering the worker: " + error + "\n"); + }); + }); + } else if (e.data.type == "check") { + ok(e.data.status, e.data.msg); + } + } + return p; + } + + var readyPromiseResolved = false; + + function readyPromise() { + var frame = document.createElement("iframe"); + frame.setAttribute("id", "simpleregister-frame-ready"); + frame.setAttribute("src", new URL("simpleregister/ready.html", document.baseURI).href); + document.body.appendChild(frame); + + var channel = new MessageChannel(); + frame.addEventListener('load', function() { + frame.contentWindow.postMessage('your port!', '*', [channel.port2]); + }); + + channel.port1.onmessage = function() { + readyPromiseResolved = true; + } + + return Promise.resolve(); + } + + function checkReadyPromise() { + ok(readyPromiseResolved, "The ready promise has been resolved!"); + return Promise.resolve(); + } + + function runTest() { + simpleRegister() + .then(sameOriginWorker) + .then(sameOriginScope) + .then(httpsOnly) + .then(readyPromise) + .then(realWorker) + .then(networkError404) + .then(redirectError) + .then(parseError) + .then(updatefound) + .then(checkReadyPromise) + // put more tests here. + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all.html b/dom/serviceworkers/test/test_match_all.html new file mode 100644 index 0000000000..a1ee01507c --- /dev/null +++ b/dom/serviceworkers/test/test_match_all.html @@ -0,0 +1,83 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + // match_all_worker will call matchAll until the worker shuts down. + // Test passes if the browser doesn't crash on leaked promise objects. + var registration; + var content; + var iframe; + + function simpleRegister() { + return navigator.serviceWorker.register("match_all_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function closeAndUnregister() { + content.removeChild(iframe); + + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function openClient() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "READY") { + resolve(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/simple.html"); + content.appendChild(iframe); + + return p; + } + + function runTest() { + simpleRegister() + .then(openClient) + .then(closeAndUnregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(function() { + ok(true, "Didn't crash on resolving matchAll promises while worker shuts down."); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all_advanced.html b/dom/serviceworkers/test/test_match_all_advanced.html new file mode 100644 index 0000000000..b4359511f3 --- /dev/null +++ b/dom/serviceworkers/test/test_match_all_advanced.html @@ -0,0 +1,102 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test matchAll with multiple clients</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var client_iframes = []; + var registration; + + function start() { + return navigator.serviceWorker.register("match_all_advanced_worker.js", + { scope: "./sw_clients/" }).then(function(swr) { + registration = swr; + return waitForState(swr.installing, 'activated'); + }).then(_ => { + window.onmessage = function (e) { + if (e.data === "READY") { + ok(registration.active, "Worker is active."); + registration.active.postMessage("RUN"); + } + } + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testMatchAll() { + var p = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function (e) { + ok(e.data === client_iframes.length, "MatchAll returned the correct number of clients."); + res(); + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + client_iframes.push(iframe); + return p; + } + + function removeAndTest() { + content = document.getElementById("content"); + ok(content, "Parent exists."); + + content.removeChild(client_iframes.pop()); + content.removeChild(client_iframes.pop()); + + return testMatchAll(); + } + + function runTest() { + start() + .then(testMatchAll) + .then(testMatchAll) + .then(testMatchAll) + .then(removeAndTest) + .then(function(e) { + content = document.getElementById("content"); + while (client_iframes.length) { + content.removeChild(client_iframes.pop()); + } + }).then(unregister).catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(function() { + SimpleTest.finish(); + }); + + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all_client_id.html b/dom/serviceworkers/test/test_match_all_client_id.html new file mode 100644 index 0000000000..0294c00aba --- /dev/null +++ b/dom/serviceworkers/test/test_match_all_client_id.html @@ -0,0 +1,95 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - Test matchAll client id </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var clientURL = "match_all_client/match_all_client_id.html"; + function start() { + return navigator.serviceWorker.register("match_all_client_id_worker.js", + { scope: "./match_all_client/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getMessageListener() { + return new Promise(function(res, rej) { + window.onmessage = function(e) { + ok(e.data, "Same client id for multiple calls."); + is(e.origin, "http://mochi.test:8888", "Event should have the correct origin"); + + if (!e.data) { + rej(); + return; + } + + info("DONE from: " + e.source); + res(); + } + }); + } + + function testNestedWindow() { + var p = getMessageListener(); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', clientURL); + + return p.then(() => content.removeChild(iframe)); + } + + function testAuxiliaryWindow() { + var p = getMessageListener(); + var w = window.open(clientURL); + + return p.then(() => w.close()); + } + + function runTest() { + info(window.opener == undefined); + start() + .then(testAuxiliaryWindow) + .then(testNestedWindow) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all_client_properties.html b/dom/serviceworkers/test/test_match_all_client_properties.html new file mode 100644 index 0000000000..c8a0b448c2 --- /dev/null +++ b/dom/serviceworkers/test/test_match_all_client_properties.html @@ -0,0 +1,101 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - Test matchAll clients properties </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var clientURL = "match_all_clients/match_all_controlled.html"; + function start() { + return navigator.serviceWorker.register("match_all_properties_worker.js", + { scope: "./match_all_clients/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getMessageListener() { + return new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data.message === undefined) { + info("rejecting promise"); + rej(); + return; + } + + ok(e.data.result, e.data.message); + + if (!e.data.result) { + rej(); + } + if (e.data.message == "DONE") { + info("DONE from: " + e.source); + res(); + } + } + }); + } + + function testNestedWindow() { + var p = getMessageListener(); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', clientURL); + + return p.then(() => content.removeChild(iframe)); + } + + function testAuxiliaryWindow() { + var p = getMessageListener(); + var w = window.open(clientURL); + + return p.then(() => w.close()); + } + + function runTest() { + info("catalin"); + info(window.opener == undefined); + start() + .then(testAuxiliaryWindow) + .then(testNestedWindow) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_navigationPreload_disable_crash.html b/dom/serviceworkers/test/test_navigationPreload_disable_crash.html new file mode 100644 index 0000000000..ea6439284d --- /dev/null +++ b/dom/serviceworkers/test/test_navigationPreload_disable_crash.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Failure to create a Promise shouldn't crash</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + async function runTest() { + const iframe = document.createElement('iframe'); + document.getElementById("content").appendChild(iframe); + + const serviceWorker = iframe.contentWindow.navigator.serviceWorker; + const worker = await iframe.contentWindow.navigator.serviceWorker.register("empty.js", {}); + + iframe.remove(); + + // We can't wait for this promise to settle, because the global's + // browsing context has been discarded when the iframe was removed. + // We're just checking if this call crashes, which would happen + // immediately, so ignoring the promise should be fine. + worker.navigationPreload.disable(); + ok(true, "navigationPreload.disable() failed but didn't crash."); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + // We can't call unregister on the worker after its browsing context has been + // discarded, so use SpecialPowers.removeAllServiceWorkerData. + SimpleTest.registerCleanupFunction(() => SpecialPowers.removeAllServiceWorkerData()); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.navigationPreload.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_navigator.html b/dom/serviceworkers/test/test_navigator.html new file mode 100644 index 0000000000..aaac04e926 --- /dev/null +++ b/dom/serviceworkers/test/test_navigator.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function checkEnabled() { + ok(navigator.serviceWorker, "navigator.serviceWorker should exist when ServiceWorkers are enabled."); + ok(typeof navigator.serviceWorker.register === "function", "navigator.serviceWorker.register() should be a function."); + ok(typeof navigator.serviceWorker.getRegistration === "function", "navigator.serviceWorker.getAll() should be a function."); + ok(typeof navigator.serviceWorker.getRegistrations === "function", "navigator.serviceWorker.getAll() should be a function."); + ok(navigator.serviceWorker.ready instanceof Promise, "navigator.serviceWorker.ready should be a Promise."); + ok(navigator.serviceWorker.controller === null, "There should be no controller worker for an uncontrolled document."); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, function() { + checkEnabled(); + SimpleTest.finish(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_nofetch_handler.html b/dom/serviceworkers/test/test_nofetch_handler.html new file mode 100644 index 0000000000..0725a68561 --- /dev/null +++ b/dom/serviceworkers/test/test_nofetch_handler.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bugs 1181127 and 1325101</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181127">Mozilla Bug 1181127</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181127">Mozilla Bug 1325101</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // Make sure the event handler during the install event persists. This ensures + // the reason for which the interception doesn't occur is because of the + // handlesFetch=false flag from ServiceWorkerInfo. + ["dom.serviceWorkers.idle_timeout", 299999], + ]}); +}); + +var iframeg; +function create_iframe(url) { + return new Promise(function(res) { + iframe = document.createElement('iframe'); + iframe.src = url; + iframe.onload = function() { res(iframe) } + document.body.appendChild(iframe); + iframeg = iframe; + }) +} + +add_task(async function test_nofetch_worker() { + let registration = await navigator.serviceWorker.register( + "nofetch_handler_worker.js", { scope: "./nofetch_handler_worker/"} ) + .then(swr => waitForState(swr.installing, 'activated', swr)); + + let iframe = await create_iframe("./nofetch_handler_worker/doesnt_exist.html"); + ok(!iframe.contentDocument.body.innerHTML.includes("intercepted"), "Request was not intercepted."); + + await SpecialPowers.popPrefEnv(); + await registration.unregister(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_not_intercept_plugin.html b/dom/serviceworkers/test/test_not_intercept_plugin.html new file mode 100644 index 0000000000..4e7654deea --- /dev/null +++ b/dom/serviceworkers/test/test_not_intercept_plugin.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1187766 - Test loading plugins scenarios with fetch interception.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + var p = navigator.serviceWorker.register("./fetch/plugin/worker.js", { scope: "./fetch/plugin/" }); + return p.then(function(swr) { + registration = swr; + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testPlugins() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + w.close(); + resolve(); + } + } + }); + + var w = window.open("fetch/plugin/plugins.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testPlugins) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notification_constructor_error.html b/dom/serviceworkers/test/test_notification_constructor_error.html new file mode 100644 index 0000000000..46d93e781f --- /dev/null +++ b/dom/serviceworkers/test/test_notification_constructor_error.html @@ -0,0 +1,51 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug XXXXXXX - Check that Notification constructor throws in ServiceWorkerGlobalScope</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("notification_constructor_error.js", { scope: "notification_constructor_error/" }).then(function(swr) { + ok(false, "Registration should fail."); + }, function(e) { + is(e.name, 'TypeError', "Registration should fail with a TypeError."); + }); + } + + function runTest() { + MockServices.register(); + simpleRegister() + .then(function() { + MockServices.unregister(); + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + MockServices.unregister(); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notification_get.html b/dom/serviceworkers/test/test_notification_get.html new file mode 100644 index 0000000000..44239b4f9e --- /dev/null +++ b/dom/serviceworkers/test/test_notification_get.html @@ -0,0 +1,213 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>ServiceWorkerRegistration.getNotifications() on main thread and worker thread.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript"> + + SimpleTest.requestFlakyTimeout("untriaged"); + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(result); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame('notification/register.html').then(function() { + ok(true, "Registered service worker."); + }); + } + + function unregisterSW() { + return testFrame('notification/unregister.html').then(function() { + ok(true, "Unregistered service worker."); + }); + } + + // To check that the scope is respected when retrieving notifications. + function registerAlternateSWAndAddNotification() { + return testFrame('notification_alt/register.html').then(function() { + ok(true, "Registered alternate service worker."); + return navigator.serviceWorker.getRegistration("./notification_alt/").then(function(reg) { + return reg.showNotification("This is a notification_alt"); + }); + }); + } + + function unregisterAlternateSWAndAddNotification() { + return testFrame('notification_alt/unregister.html').then(function() { + ok(true, "unregistered alternate service worker."); + }); + } + + function testDismiss() { + // Dismissed persistent notifications should be removed from the + // notification list. + var alertsService = SpecialPowers.Cc["@mozilla.org/alerts-service;1"] + .getService(SpecialPowers.Ci.nsIAlertsService); + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification( + "This is a notification that will be closed", { tag: "dismiss" }) + .then(function() { + return reg; + }); + }).then(function(reg) { + return reg.getNotifications() + .then(function(notifications) { + is(notifications.length, 1, "There should be one visible notification"); + is(notifications[0].tag, "dismiss", "Tag should match"); + + // Simulate dismissing the notification by using the alerts service + // directly, instead of `Notification#close`. + var principal = SpecialPowers.wrap(document).nodePrincipal; + var id = principal.origin + "#tag:dismiss"; + alertsService.closeAlert(id, principal); + + return reg; + }); + }).then(function(reg) { + return reg.getNotifications(); + }).then(function(notifications) { + // Make sure dismissed notifications are no longer retrieved. + is(notifications.length, 0, "There should be no more stored notifications"); + }); + } + + function testGet() { + // Non persistent notifications will not show up in getNotification(). + var n = new Notification("Scope does not match"); + var options = NotificationTest.payload; + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification("This is a title", options) + .then(function() { + return reg; + }); + }).then(function(reg) { + return registerAlternateSWAndAddNotification().then(function() { + return reg; + }); + }).then(function(reg) { + return reg.getNotifications(); + }).then(function(notifications) { + is(notifications.length, 1, "There should be one stored notification"); + var notification = notifications[0]; + ok(notification instanceof Notification, "Should be a Notification"); + is(notification.title, "This is a title", "Title should match"); + for (var key in options) { + if (key === "data") { + ok(NotificationTest.customDataMatches(notification.data), + "data property should match"); + continue; + } + is(notification[key], options[key], key + " property should match"); + } + notification.close(); + }).then(function() { + return navigator.serviceWorker.getRegistration("./notification/").then(function(reg) { + return reg.getNotifications(); + }); + }).then(function(notifications) { + // Make sure closed notifications are no longer retrieved. + is(notifications.length, 0, "There should be no more stored notifications"); + }).catch(function(e) { + ok(false, "Something went wrong " + e.message); + }).then(unregisterAlternateSWAndAddNotification); + } + + function testGetWorker() { + todo(false, "navigator.serviceWorker is not available on workers yet"); + return Promise.resolve(); + } + + function waitForSWTests(reg, msg) { + return new Promise(function(resolve, reject) { + var content = document.getElementById("content"); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', "notification/listener.html"); + + window.onmessage = function(e) { + if (e.data.type == 'status') { + ok(e.data.status, "Service worker test: " + e.data.msg); + } else if (e.data.type == 'finish') { + content.removeChild(iframe); + resolve(); + } + } + + iframe.onload = function(e) { + iframe.onload = null; + reg.active.postMessage(msg); + } + }); + } + + function testGetServiceWorker() { + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return waitForSWTests(reg, 'create'); + }); + } + + // Create a Notification here, make sure ServiceWorker sees it. + function testAcrossThreads() { + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification("This is a title") + .then(function() { + return reg; + }); + }).then(function(reg) { + return waitForSWTests(reg, 'do-not-create'); + }); + } + + SimpleTest.waitForExplicitFinish(); + + MockServices.register(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, function() { + registerSW() + .then(testGet) + .then(testGetWorker) + .then(testGetServiceWorker) + .then(testAcrossThreads) + .then(testDismiss) + .then(unregisterSW) + .then(function() { + MockServices.unregister(); + SimpleTest.finish(); + }); + }); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notification_openWindow.html b/dom/serviceworkers/test/test_notification_openWindow.html new file mode 100644 index 0000000000..830c9e03d2 --- /dev/null +++ b/dom/serviceworkers/test/test_notification_openWindow.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1578070</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="utils.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999] + ]}); + + MockServices.register(); + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + SimpleTest.registerCleanupFunction(() => { + MockServices.unregister(); + }); +}); + +add_task(async function test() { + info("Registering service worker."); + let swr = await navigator.serviceWorker.register("notification_openWindow_worker.js"); + await waitForState(swr.installing, "activated"); + + SimpleTest.registerCleanupFunction(async () => { + await swr.unregister(); + navigator.serviceWorker.onmessage = null; + }); + + for (let prefValue of [ + SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, + SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW, + SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + ]) { + if (prefValue == SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW) { + // Let's open a new tab and focus on it. When the service + // worker notification is shown, the document will open in the focused tab. + // If we don't open a new tab, the document will be opened in the + // current test-runner tab and mess up the test setup. + window.open(""); + } + info(`Setting browser.link.open_newwindow to ${prefValue}.`); + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", prefValue]], + }); + + // The onclicknotification handler uses Clients.openWindow() to open a new + // window. This newly created window will attempt to open another window with + // Window.open() and some arbitrary URL. We crash before the second window + // finishes loading. + info("Showing notification."); + await swr.showNotification("notification"); + + info("Waiting for \"DONE\" from worker."); + await new Promise(resolve => { + navigator.serviceWorker.onmessage = event => { + if (event.data !== "DONE") { + ok(false, `Unexpected message from service worker: ${JSON.stringify(event.data)}`); + } + resolve(); + } + }); + + // If we make it here, then we didn't crash. + ok(true, "Didn't crash!"); + + navigator.serviceWorker.onmessage = null; + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclick-otherwindow.html b/dom/serviceworkers/test/test_notificationclick-otherwindow.html new file mode 100644 index 0000000000..a1ffb71c39 --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclick-otherwindow.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "Got notificationclick event with correct data."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick-otherwindow.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + testFrame('notificationclick-otherwindow.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclick.html b/dom/serviceworkers/test/test_notificationclick.html new file mode 100644 index 0000000000..f733a3703f --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclick.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "Got notificationclick event with correct data."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + // Now that we know the document will be controlled, create the frame. + testFrame('notificationclick.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclick_focus.html b/dom/serviceworkers/test/test_notificationclick_focus.html new file mode 100644 index 0000000000..6a99112313 --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclick_focus.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1144660 - Test client.focus() permissions on notification click</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "All tests passed."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclick_focus.js", { scope: "notificationclick_focus.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + testFrame('notificationclick_focus.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclose.html b/dom/serviceworkers/test/test_notificationclose.html new file mode 100644 index 0000000000..a1ae4f0a4e --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclose.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1265841 +--> +<head> + <title>Bug 1265841 - Test ServiceWorkerGlobalScope.notificationclose event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265841">Bug 1265841</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show, click, and close events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(data) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(data.result, "Got notificationclose event with correct data."); + ok(!data.windowOpened, + "Shouldn't allow to openWindow in notificationclose"); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclose.js", { scope: "notificationclose.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + testFrame('notificationclose.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_onmessageerror.html b/dom/serviceworkers/test/test_onmessageerror.html new file mode 100644 index 0000000000..425b890951 --- /dev/null +++ b/dom/serviceworkers/test/test_onmessageerror.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test onmessageerror event handlers</title> + </head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="utils.js"></script> + <script> + /** + * Test that ServiceWorkerGlobalScope and ServiceWorkerContainer handle + * `messageerror` events, using a test helper class `StructuredCloneTester`. + * Intances of this class can be configured to fail to serialize or + * deserialize, as it's difficult to artificially create the case where an + * object successfully serializes but fails to deserialize (which can be + * caused by out-of-memory failures or the target global not supporting a + * serialized interface). + */ + + let registration = null; + let serviceWorker = null; + let serviceWorkerContainer = null; + const swScript = 'onmessageerror_worker.js'; + + add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ['dom.serviceWorkers.enabled', true], + ['dom.serviceWorkers.testing.enabled', true], + ['dom.testing.structuredclonetester.enabled', true], + ], + }); + + swContainer = navigator.serviceWorker; + + registration = await swContainer.register(swScript); + ok(registration, 'Service Worker regsisters'); + + serviceWorker = registration.installing; + await waitForState(serviceWorker, 'activated'); + }); // setup + + add_task(async () => { + const serializable = true; + const deserializable = true; + let sct = new StructuredCloneTester(serializable, deserializable); + + const p = new Promise((resolve, reject) => { + function onMessage(e) { + const expectedBehavior = 'Serializable and deserializable ' + + 'StructuredCloneTester serializes and deserializes'; + + is(e.data.received, 'message', expectedBehavior); + swContainer.removeEventListener('message', onMessage); + resolve(); + } + + swContainer.addEventListener('message', onMessage); + }); + + serviceWorker.postMessage({ serializable, deserializable, sct }); + + await p; + }); + + add_task(async () => { + const serializable = false; + // if it's not serializable, being deserializable or not doesn't matter + const deserializable = false; + let sct = new StructuredCloneTester(serializable, deserializable); + + try { + serviceWorker.postMessage({ serializable, deserializable, sct }); + ok(false, 'StructuredCloneTester serialization should have thrown -- ' + + 'this line should not have been reached.'); + } catch (e) { + const expectedBehavior = 'Unserializable StructuredCloneTester fails ' + + `to send, with exception name: ${e.name}`; + is(e.name, 'DataCloneError', expectedBehavior); + } + }); + + add_task(async () => { + const serializable = true; + const deserializable = false; + let sct = new StructuredCloneTester(serializable, deserializable); + + const p = new Promise((resolve, reject) => { + function onMessage(e) { + const expectedBehavior = 'ServiceWorkerGlobalScope handles ' + + 'messageerror events'; + + is(e.data.received, 'messageerror', expectedBehavior); + swContainer.removeEventListener('message', onMessage); + resolve(); + } + + swContainer.addEventListener('message', onMessage); + }); + + serviceWorker.postMessage({ serializable, deserializable, sct }); + + await p; + }); // test ServiceWorkerGlobalScope onmessageerror + + add_task(async () => { + const p = new Promise((resolve, reject) => { + function onMessageError(e) { + ok(true, 'ServiceWorkerContainer handles messageerror events'); + swContainer.removeEventListener('messageerror', onMessageError); + resolve(); + } + + swContainer.addEventListener('messageerror', onMessageError); + }); + + serviceWorker.postMessage('send-bad-message'); + + await p; + }); // test ServiceWorkerContainer onmessageerror + + add_task(async () => { + await SpecialPowers.popPrefEnv(); + ok(await registration.unregister(), 'Service Worker unregisters'); + }); // teardown + </script> + <body> + </body> +</html> diff --git a/dom/serviceworkers/test/test_opaque_intercept.html b/dom/serviceworkers/test/test_opaque_intercept.html new file mode 100644 index 0000000000..f0e40e0402 --- /dev/null +++ b/dom/serviceworkers/test/test_opaque_intercept.html @@ -0,0 +1,93 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + function start() { + return navigator.serviceWorker.register("opaque_intercept_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testOpaqueIntercept(swr) { + var p = new Promise(function(res, rej) { + var ready = false; + var scriptLoaded = false; + window.onmessage = function(e) { + if (e.data === "READY") { + ok(!ready, "ready message should only be received once"); + ok(!scriptLoaded, "ready message should be received before script loaded"); + if (ready) { + res(); + return; + } + ready = true; + iframe.contentWindow.postMessage("REFRESH", "*"); + } else if (e.data === "SCRIPT_LOADED") { + ok(ready, "script loaded should be received after ready"); + ok(!scriptLoaded, "script loaded message should be received only once"); + scriptLoaded = true; + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + var iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/refresher.html"); + content.appendChild(iframe); + + // Our service worker waits for us to finish installing. If it didn't do + // this, then loading our frame would race with it becoming active, + // possibly intercepting the first load of the iframe. This guarantees + // that our iframe will load first directly from the network. Note that + // refresher.html explicitly waits for the service worker to transition to + // active. + registration.installing.postMessage("ready"); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testOpaqueIntercept) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_openWindow.html b/dom/serviceworkers/test/test_openWindow.html new file mode 100644 index 0000000000..448d143edd --- /dev/null +++ b/dom/serviceworkers/test/test_openWindow.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1172870 +--> +<head> + <title>Bug 1172870 - Test clients.openWindow</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1172870">Bug 1172870</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function setup(ctx) { + MockServices.register(); + + return navigator.serviceWorker.register("openWindow_worker.js", {scope: "./"}) + .then(function(swr) { + ok(swr, "Registration successful"); + ctx.registration = swr; + return waitForState(swr.installing, 'activated', ctx); + }); + } + + function setupMessageHandler(ctx) { + return new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + for (i = 0; i < event.data.length; i++) { + ok(event.data[i].result, event.data[i].message); + } + res(ctx); + } + }); + } + + function testPopupNotAllowed(ctx) { + var p = setupMessageHandler(ctx); + ok(ctx.registration.active, "Worker is active."); + ctx.registration.active.postMessage("testNoPopup"); + + return p; + } + + function testPopupAllowed(ctx) { + var p = setupMessageHandler(ctx); + ctx.registration.showNotification("testPopup"); + + return p; + } + + function checkNumberOfWindows(ctx) { + return new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + for (i = 0; i < event.data.length; i++) { + ok(event.data[i].result, event.data[i].message); + } + res(ctx); + } + ctx.registration.active.postMessage("CHECK_NUMBER_OF_WINDOWS"); + }); + } + + function clear(ctx) { + MockServices.unregister(); + + return ctx.registration.unregister().then(function(result) { + ctx.registration = null; + ok(result, "Unregister was successful."); + }); + } + + function runTest() { + setup({}) + // Permission to allow popups persists for some time after a notification + // click event, so the order here is important. + .then(testPopupNotAllowed) + .then(testPopupAllowed) + .then(checkNumberOfWindows) + .then(clear) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999], + ["dom.securecontext.allowlist", "mochi.test,example.com"], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect.html b/dom/serviceworkers/test/test_origin_after_redirect.html new file mode 100644 index 0000000000..e4c0af23be --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "http://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ["dom.security.https_first", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect_cached.html b/dom/serviceworkers/test/test_origin_after_redirect_cached.html new file mode 100644 index 0000000000..ca79581ccf --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect_cached.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-cached.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "http://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ["dom.security.https_first", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect_to_https.html b/dom/serviceworkers/test/test_origin_after_redirect_to_https.html new file mode 100644 index 0000000000..927a68ef3a --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect_to_https.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-to-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html b/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html new file mode 100644 index 0000000000..29686e2302 --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-to-https-cached.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_post_message.html b/dom/serviceworkers/test/test_post_message.html new file mode 100644 index 0000000000..b72f948dd6 --- /dev/null +++ b/dom/serviceworkers/test/test_post_message.html @@ -0,0 +1,80 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var magic_value = "MAGIC_VALUE_123"; + var registration; + function start() { + return navigator.serviceWorker.register("message_posting_worker.js", + { scope: "./sw_clients/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + swr.active.postMessage(magic_value); + } else if (e.data === magic_value) { + ok(true, "Worker posted the correct value."); + res(); + } else { + ok(false, "Wrong value. Expected: " + magic_value + + ", got: " + e.data); + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_post_message_advanced.html b/dom/serviceworkers/test/test_post_message_advanced.html new file mode 100644 index 0000000000..580dfd3f07 --- /dev/null +++ b/dom/serviceworkers/test/test_post_message_advanced.html @@ -0,0 +1,109 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message advanced </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var base = ["string", true, 42]; + var blob = new Blob(["blob_content"]); + var file = new File(["file_content"], "file"); + var obj = { body : "object_content" }; + + function readBlob(blobToRead) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsText(blobToRead); + }); + } + + function equals(v1, v2) { + return Promise.all([v1, v2]).then(function(val) { + ok(val[0] === val[1], "Values should match."); + }); + } + + function blob_equals(b1, b2) { + return equals(readBlob(b1), readBlob(b2)); + } + + function file_equals(f1, f2) { + return equals(f1.name, f2.name).then(blob_equals(f1, f2)); + } + + function obj_equals(o1, o2) { + return equals(o1.body, o2.body); + } + + function start() { + return navigator.serviceWorker.register("message_posting_worker.js", + { scope: "./sw_clients/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testPostMessageObject(object, test) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + registration.active.postMessage(object) + } else { + test(object, e.data).then(res); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessageObject.bind(this, base[0], equals)) + .then(testPostMessageObject.bind(this, base[1], equals)) + .then(testPostMessageObject.bind(this, base[2], equals)) + .then(testPostMessageObject.bind(this, blob, blob_equals)) + .then(testPostMessageObject.bind(this, file, file_equals)) + .then(testPostMessageObject.bind(this, obj, obj_equals)) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_post_message_source.html b/dom/serviceworkers/test/test_post_message_source.html new file mode 100644 index 0000000000..b72ebe3a7c --- /dev/null +++ b/dom/serviceworkers/test/test_post_message_source.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1142015 - Test service worker post message source </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var magic_value = "MAGIC_VALUE_RANDOM"; + var registration; + function start() { + return navigator.serviceWorker.register("source_message_posting_worker.js", + { scope: "./nonexistent_scope/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(e) { + ok(e.data === magic_value, "Worker posted the correct value."); + res(); + } + }); + + ok(swr.installing, "Installing worker exists."); + swr.installing.postMessage(magic_value); + return p; + } + + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_privateBrowsing.html b/dom/serviceworkers/test/test_privateBrowsing.html new file mode 100644 index 0000000000..6a10f375f9 --- /dev/null +++ b/dom/serviceworkers/test/test_privateBrowsing.html @@ -0,0 +1,103 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for ServiceWorker - Private Browsing</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<body> + +<script type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + +var mainWindow; + +var contentPage = "http://mochi.test:8888/chrome/dom/workers/test/empty.html"; +var workerScope = "http://mochi.test:8888/chrome/dom/serviceworkers/test/"; +var workerURL = workerScope + "worker.js"; + +function testOnWindow(aIsPrivate, aCallback) { + var win = mainWindow.OpenBrowserWindow({private: aIsPrivate}); + win.addEventListener("load", function() { + win.addEventListener("DOMContentLoaded", function onInnerLoad() { + if (win.content.location.href != contentPage) { + BrowserTestUtils.loadURI(win.gBrowser, contentPage); + return; + } + + win.removeEventListener("DOMContentLoaded", onInnerLoad, true); + SimpleTest.executeSoon(function() { aCallback(win); }); + }, true); + }, {capture: true, once: true}); +} + +function setupWindow() { + mainWindow = window.browsingContext.topChromeWindow; + runTest(); +} + +var wN; +var registration; +var wP; + +function testPrivateWindow() { + testOnWindow(true, function(aWin) { + wP = aWin; + ok(!wP.content.eval('"serviceWorker" in navigator'), "ServiceWorkers are not available for private windows"); + runTest(); + }); +} + +function doTests() { + testOnWindow(false, function(aWin) { + wN = aWin; + ok("serviceWorker" in wN.content.navigator, "ServiceWorkers are available for normal windows"); + + wN.content.navigator.serviceWorker.register(workerURL, + { scope: workerScope }) + .then(function(aRegistration) { + registration = aRegistration; + ok(registration, "Registering a service worker in a normal window should succeed"); + + // Bug 1255621: We should be able to load a controlled document in a private window. + testPrivateWindow(); + }, function(aError) { + ok(false, "Error registering worker in normal window: " + aError); + testPrivateWindow(); + }); + }); +} + +var steps = [ + setupWindow, + doTests +]; + +function cleanup() { + wN.close(); + wP.close(); + + SimpleTest.finish(); +} + +function runTest() { + if (!steps.length) { + registration.unregister().then(cleanup, cleanup); + + return; + } + + var step = steps.shift(); + step(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.startup.page", 0], + ["browser.startup.homepage_override.mstone", "ignore"], +]}, runTest); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_register_base.html b/dom/serviceworkers/test/test_register_base.html new file mode 100644 index 0000000000..3a1f2f2621 --- /dev/null +++ b/dom/serviceworkers/test/test_register_base.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that registering a service worker uses the docuemnt URI for the secure origin check</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + ok(!("serviceWorker" in navigator), "ServiceWorkerContainer shouldn't be defined"); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_register_https_in_http.html b/dom/serviceworkers/test/test_register_https_in_http.html new file mode 100644 index 0000000000..096c3733a0 --- /dev/null +++ b/dom/serviceworkers/test/test_register_https_in_http.html @@ -0,0 +1,45 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1172948 - Test that registering a service worker from inside an HTTPS iframe embedded in an HTTP iframe doesn't work</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + var iframe = document.createElement("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/register_https.html"; + document.body.appendChild(iframe); + + window.onmessage = event => { + switch (event.data.type) { + case "ok": + ok(event.data.status, event.data.msg); + break; + case "done": + SimpleTest.finish(); + break; + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_sandbox_intercept.html b/dom/serviceworkers/test/test_sandbox_intercept.html new file mode 100644 index 0000000000..2aa120994f --- /dev/null +++ b/dom/serviceworkers/test/test_sandbox_intercept.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1142727 - Test that sandboxed iframes are not intercepted</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe id="normal-frame"></iframe> +<iframe sandbox="allow-scripts" id="sandbox-frame"></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var normalFrame; + var sandboxFrame; + function runTest() { + normalFrame = document.getElementById("normal-frame"); + sandboxFrame = document.getElementById("sandbox-frame"); + normalFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + normalFrame.src = "about:blank"; + sandboxFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/index.html"; + } else if (e.data.status == "done") { + sandboxFrame.src = "about:blank"; + normalFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + normalFrame.src = "about:blank"; + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_sanitize.html b/dom/serviceworkers/test/test_sanitize.html new file mode 100644 index 0000000000..dd6bd42c8f --- /dev/null +++ b/dom/serviceworkers/test/test_sanitize.html @@ -0,0 +1,86 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1080109 - Clear ServiceWorker registrations for all domains</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function start() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + + function testNotIntercepted() { + testFrame("sanitize/frame.html").then(function(body) { + is(body, "FAIL", "Expected frame to not be controlled"); + // No need to unregister since that already happened. + navigator.serviceWorker.getRegistration("sanitize/foo").then(function(reg) { + ok(reg === undefined, "There should no longer be a valid registration"); + }, function(e) { + ok(false, "getRegistration() should not error"); + }).then(function(e) { + SimpleTest.finish(); + }); + }); + } + + registerSW().then(function() { + return testFrame("sanitize/frame.html").then(function(body) { + is(body, "intercepted", "Expected serviceworker to intercept request"); + }); + }).then(function() { + return navigator.serviceWorker.getRegistration("sanitize/foo"); + }).then(function(reg) { + reg.active.onstatechange = function(e) { + e.target.onstatechange = null; + is(e.target.state, "redundant", "On clearing data, serviceworker should become redundant"); + testNotIntercepted(); + }; + }).then(function() { + SpecialPowers.removeAllServiceWorkerData(); + }); + } + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(message) { + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(message.data); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame("sanitize/register.html"); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function() { + start(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_sanitize_domain.html b/dom/serviceworkers/test/test_sanitize_domain.html new file mode 100644 index 0000000000..d0f5f7f69a --- /dev/null +++ b/dom/serviceworkers/test/test_sanitize_domain.html @@ -0,0 +1,89 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1080109 - Clear ServiceWorker registrations for specific domains</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function start() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + + function checkDomainRegistration(domain, exists) { + return testFrame("http://" + domain + "/tests/dom/serviceworkers/test/sanitize/example_check_and_unregister.html").then(function(body) { + if (body === "FAIL") { + ok(false, "Error acquiring registration or unregistering for " + domain); + } else { + if (exists) { + ok(body === true, "Expected " + domain + " to still have a registration."); + } else { + ok(body === false, "Expected " + domain + " to have no registration."); + } + } + }); + } + + registerSW().then(function() { + return testFrame("http://example.com/tests/dom/serviceworkers/test/sanitize/frame.html").then(function(body) { + is(body, "intercepted", "Expected serviceworker to intercept request"); + }); + }).then(function() { + return SpecialPowers.removeServiceWorkerDataForExampleDomain(); + }).then(function() { + return checkDomainRegistration("prefixexample.com", true /* exists */) + .then(function(e) { + return checkDomainRegistration("example.com", false /* exists */); + }).then(function(e) { + SimpleTest.finish(); + }); + }) + } + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(message) { + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(message.data); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame("http://example.com/tests/dom/serviceworkers/test/sanitize/register.html") + .then(function(e) { + // Register for prefixexample.com and then ensure it does not get unregistered. + return testFrame("http://prefixexample.com/tests/dom/serviceworkers/test/sanitize/register.html"); + }); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function() { + start(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_scopes.html b/dom/serviceworkers/test/test_scopes.html new file mode 100644 index 0000000000..77e997766d --- /dev/null +++ b/dom/serviceworkers/test/test_scopes.html @@ -0,0 +1,143 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test scope glob matching.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var scriptsAndScopes = [ + [ "worker.js", "./sub/dir/"], + [ "worker.js", "./sub/dir" ], + [ "worker.js", "./sub/dir.html" ], + [ "worker.js", "./sub/dir/a" ], + [ "worker.js", "./sub" ], + [ "worker.js", "./star*" ], // '*' has no special meaning + ]; + + function registerWorkers() { + var registerArray = []; + scriptsAndScopes.forEach(function(item) { + registerArray.push(navigator.serviceWorker.register(item[0], { scope: item[1] })); + }); + + // Check register()'s step 4 which uses script's url with "./" as the scope if no scope is passed. + // The other tests already check step 5. + registerArray.push(navigator.serviceWorker.register("scope/scope_worker.js")); + + // Check that SW cannot be registered for a scope "above" the script's location. + registerArray.push(new Promise(function(resolve, reject) { + navigator.serviceWorker.register("scope/scope_worker.js", { scope: "./" }) + .then(function() { + ok(false, "registration scope has to be inside service worker script scope."); + reject(); + }, function() { + ok(true, "registration scope has to be inside service worker script scope."); + resolve(); + }); + })); + return Promise.all(registerArray); + } + + function unregisterWorkers() { + var unregisterArray = []; + scriptsAndScopes.forEach(function(item) { + var p = navigator.serviceWorker.getRegistration(item[1]); + unregisterArray.push(p.then(function(reg) { + return reg.unregister(); + })); + }); + + unregisterArray.push(navigator.serviceWorker.getRegistration("scope/").then(function (reg) { + return reg.unregister(); + })); + + return Promise.all(unregisterArray); + } + + async function testScopes() { + function chromeScriptSource() { + /* eslint-env mozilla/chrome-script */ + + let swm = Cc["@mozilla.org/serviceworkers/manager;1"] + .getService(Ci.nsIServiceWorkerManager); + let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + addMessageListener("getScope", (msg) => { + let principal = secMan.createContentPrincipalFromOrigin(msg.principal); + try { + return { scope: swm.getScopeForUrl(principal, msg.path) }; + } catch (e) { + return { exception: e.message }; + } + }); + } + + let chromeScript = SpecialPowers.loadChromeScript(chromeScriptSource); + let docPrincipal = SpecialPowers.wrap(document).nodePrincipal.spec; + + getScope = async (path) => { + let rv = await chromeScript.sendQuery("getScope", { principal: docPrincipal, path }); + if (rv.exception) + throw rv.exception; + return rv.scope; + }; + + var base = new URL(".", document.baseURI); + + function p(s) { + return base + s; + } + + async function fail(fn) { + try { + await getScope(p("index.html")); + ok(false, "No registration"); + } catch(e) { + ok(true, "No registration"); + } + } + + is(await getScope(p("sub.html")), p("sub"), "Scope should match"); + is(await getScope(p("sub/dir.html")), p("sub/dir.html"), "Scope should match"); + is(await getScope(p("sub/dir")), p("sub/dir"), "Scope should match"); + is(await getScope(p("sub/dir/foo")), p("sub/dir/"), "Scope should match"); + is(await getScope(p("sub/dir/afoo")), p("sub/dir/a"), "Scope should match"); + is(await getScope(p("star*wars")), p("star*"), "Scope should match"); + is(await getScope(p("scope/some_file.html")), p("scope/"), "Scope should match"); + await fail("index.html"); + await fail("sua.html"); + await fail("star/a.html"); + } + + function runTest() { + registerWorkers() + .then(testScopes) + .then(unregisterWorkers) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html b/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html new file mode 100644 index 0000000000..d0073705bb --- /dev/null +++ b/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html @@ -0,0 +1,224 @@ +<!DOCTYPE html> +<html> +<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1350359 --> +<!-- The JS bytecode cache is not supposed to be observable. To make it + observable, the ScriptLoader is instrumented to trigger events on the + script tag. These events are followed to reconstruct the code path taken by + the script loader and associate a simple name which is checked in these + test cases. +--> +<head> + <meta charset="utf-8"> + <title>Test for saving and loading bytecode in/from the necko cache</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="utils.js"></script> + <script type="application/javascript"> + + // This is the state machine of the trace events produced by the + // ScriptLoader. This state machine is used to give a name to each + // code path, such that we can assert each code path with a single word. + var scriptLoaderStateMachine = { + "scriptloader_load_source": { + "scriptloader_execute": { + "scriptloader_encode": { + "scriptloader_bytecode_saved": "bytecode_saved", + "scriptloader_bytecode_failed": "bytecode_failed" + }, + "scriptloader_no_encode": "source_exec" + } + }, + "scriptloader_load_bytecode": { + "scriptloader_fallback": { + // Replicate the top-level state machine without + // "scriptloader_load_bytecode" transition. + "scriptloader_load_source": { + "scriptloader_execute": { + "scriptloader_encode": { + "scriptloader_bytecode_saved": "fallback_bytecode_saved", + "scriptloader_bytecode_failed": "fallback_bytecode_failed" + }, + "scriptloader_no_encode": "fallback_source_exec" + } + } + }, + "scriptloader_execute": "bytecode_exec" + } + }; + + var gScript = SpecialPowers. + loadChromeScript('http://mochi.test:8888/tests/dom/serviceworkers/test/file_js_cache_cleanup.js'); + + function WaitForScriptTagEvent(url) { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + + var stateMachine = scriptLoaderStateMachine; + var stateHistory = []; + var stateMachineResolve, stateMachineReject; + var statePromise = new Promise((resolve, reject) => { + stateMachineResolve = resolve; + stateMachineReject = reject; + }); + var ping = 0; + + // Walk the script loader state machine with the emitted events. + function log_event(evt) { + // If we have multiple script tags in the loaded source, make sure + // we only watch a single one. + if (evt.target.id != "watchme") + return; + + dump("## ScriptLoader event: " + evt.type + "\n"); + stateHistory.push(evt.type) + if (typeof stateMachine == "object") + stateMachine = stateMachine[evt.type]; + if (typeof stateMachine == "string") { + // We arrived to a final state, report the name of it. + var result = stateMachine; + if (ping) { + result = `${result} & ping(=${ping})`; + } + stateMachineResolve(result); + } else if (stateMachine === undefined) { + // We followed an unknown transition, report the known history. + stateMachineReject(stateHistory); + } + } + + var iwin = iframe.contentWindow; + iwin.addEventListener("scriptloader_load_source", log_event); + iwin.addEventListener("scriptloader_load_bytecode", log_event); + iwin.addEventListener("scriptloader_generate_bytecode", log_event); + iwin.addEventListener("scriptloader_execute", log_event); + iwin.addEventListener("scriptloader_encode", log_event); + iwin.addEventListener("scriptloader_no_encode", log_event); + iwin.addEventListener("scriptloader_bytecode_saved", log_event); + iwin.addEventListener("scriptloader_bytecode_failed", log_event); + iwin.addEventListener("scriptloader_fallback", log_event); + iwin.addEventListener("ping", (evt) => { + ping += 1; + dump(`## Content event: ${evt.type} (=${ping})\n`); + }); + iframe.src = url; + + statePromise.then(() => { + document.body.removeChild(iframe); + }); + return statePromise; + } + + promise_test(async function() { + // Setting dom.expose_test_interfaces pref causes the + // nsScriptLoadRequest to fire event on script tags, with information + // about its internal state. The ScriptLoader source send events to + // trace these and resolve a promise with the path taken by the + // script loader. + // + // Setting dom.script_loader.bytecode_cache.strategy to -1 causes the + // nsScriptLoadRequest to force all the conditions necessary to make a + // script be saved as bytecode in the alternate data storage provided + // by the channel (necko cache). + await SpecialPowers.pushPrefEnv({set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ['dom.script_loader.bytecode_cache.enabled', true], + ['dom.expose_test_interfaces', true], + ['dom.script_loader.bytecode_cache.strategy', -1] + ]}); + + // Register the service worker that perform the pass-through fetch. + var registration = await navigator.serviceWorker + .register("fetch.js", {scope: "./"}); + let sw = registration.installing || registration.active; + + // wait for service worker be activated + await waitForState(sw, 'activated'); + + await testCheckTheJSBytecodeCache(); + await testSavebytecodeAfterTheInitializationOfThePage(); + await testDoNotSaveBytecodeOnCompilationErrors(); + + await registration.unregister(); + await teardown(); + }); + + function teardown() { + return new Promise((resolve, reject) => { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.destroy(); + resolve(); + }); + gScript.sendAsyncMessage("teardown"); + }); + } + + async function testCheckTheJSBytecodeCache() { + dump("## Test: Check the JS bytecode cache\n"); + + // Load the test page, and verify that the code path taken by the + // nsScriptLoadRequest corresponds to the code path which is loading a + // source and saving it as bytecode. + var stateMachineResult = WaitForScriptTagEvent("file_js_cache.html"); + assert_equals(await stateMachineResult, "bytecode_saved", + "[1] ScriptLoadRequest status after the first visit"); + + // Reload the same test page, and verify that the code path taken by + // the nsScriptLoadRequest corresponds to the code path which is + // loading bytecode and executing it. + stateMachineResult = WaitForScriptTagEvent("file_js_cache.html"); + assert_equals(await stateMachineResult, "bytecode_exec", + "[2] ScriptLoadRequest status after the second visit"); + + // Load another page which loads the same script with an SRI, while + // the cached bytecode does not have any. This should fallback to + // loading the source before saving the bytecode once more. + stateMachineResult = WaitForScriptTagEvent("file_js_cache_with_sri.html"); + assert_equals(await stateMachineResult, "fallback_bytecode_saved", + "[3] ScriptLoadRequest status after the SRI hash"); + + // Loading a page, which has the same SRI should verify the SRI and + // continue by executing the bytecode. + var stateMachineResult1 = WaitForScriptTagEvent("file_js_cache_with_sri.html"); + + // Loading a page which does not have a SRI while we have one in the + // cache should not change anything. We should also be able to load + // the cache simultanesouly. + var stateMachineResult2 = WaitForScriptTagEvent("file_js_cache.html"); + + assert_equals(await stateMachineResult1, "bytecode_exec", + "[4] ScriptLoadRequest status after same SRI hash"); + assert_equals(await stateMachineResult2, "bytecode_exec", + "[5] ScriptLoadRequest status after visit with no SRI"); + } + + async function testSavebytecodeAfterTheInitializationOfThePage() { + dump("## Test: Save bytecode after the initialization of the page"); + + // The test page add a new script which generate a "ping" event, which + // should be recorded before the bytecode is stored in the cache. + var stateMachineResult = + WaitForScriptTagEvent("file_js_cache_save_after_load.html"); + assert_equals(await stateMachineResult, "bytecode_saved & ping(=3)", + "Wait on all scripts to be executed"); + } + + async function testDoNotSaveBytecodeOnCompilationErrors() { + dump("## Test: Do not save bytecode on compilation errors"); + + // The test page loads a script which contains a syntax error, we should + // not attempt to encode any bytecode for it. + var stateMachineResult = + WaitForScriptTagEvent("file_js_cache_syntax_error.html"); + assert_equals(await stateMachineResult, "source_exec", + "Check the lack of bytecode encoding"); + } + + done(); + </script> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1350359">Mozilla Bug 1350359</a> +</body> +</html> diff --git a/dom/serviceworkers/test/test_self_update_worker.html b/dom/serviceworkers/test/test_self_update_worker.html new file mode 100644 index 0000000000..d6d4544dd9 --- /dev/null +++ b/dom/serviceworkers/test/test_self_update_worker.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test that a self updating service worker can't keep running forever when the + script changes. + + - self_update_worker.sjs is a stateful server-side js script that returns a + SW script with a different version every time it's invoked. (version=1..n) + - The SW script will trigger an update when it reaches the activating state, + which, if not for the update delaying mechanism, would result in an iterative + cycle. + - We currently delay registration.update() calls originating from SWs not currently + controlling any clients. The delay is: 0s, 30s, 900s etc, but for the purpose of + this test, the delay is: 0s, infinite etc. + - We assert that the SW script never reaches version 3, meaning it will only + successfully update once. + - We give the worker reasonable time to self update by repeatedly registering + and unregistering an empty service worker. + --> +<head> + <title>Test for Bug 1432846</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432846">Mozilla Bug 1432846</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +function activateDummyWorker() { + return navigator.serviceWorker.register("empty.js", + { scope: "./empty?random=" + Date.now() }) + .then(function(registration) { + var worker = registration.installing; + return waitForState(worker, 'activated', registration).then(function() { + ok(true, "got dummy!"); + return registration.unregister(); + }); + }); +} + +add_task(async function test_update() { + navigator.serviceWorker.onmessage = function(event) { + ok (event.data.version < 3, "Service worker updated too many times." + event.data.version); + } + + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.update_delay", 30000], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + // clear version counter + await fetch("self_update_worker.sjs?clearcounter"); + + var worker; + let registration = await navigator.serviceWorker.register( + "self_update_worker.sjs", + { scope: "./test_self_update_worker.html?random=" + Date.now()}) + .then(function(reg) { + worker = reg.installing; + // We can't wait for 'activated' here, since it's possible for + // the update process to kill the worker before it activates. + // See: https://github.com/w3c/ServiceWorker/issues/1285 + return waitForState(worker, 'activating', reg); + }); + + // We need to wait a reasonable time to give the self updating worker a chance + // to change to a newer version. Register and activate an empty worker 5 times. + for (i = 0; i < 5; i++) { + await activateDummyWorker(); + } + + + await registration.unregister(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +}); + +// Test variant to ensure that we properly keep the timer alive by having a +// non-zero but small timer duration. In this case, the delay is simply our +// exponential growth rate of 30, so if we end up getting to version 4, that's +// okay and the test may need to be updated. +add_task(async function test_delay_update() { + let version; + navigator.serviceWorker.onmessage = function(event) { + ok (event.data.version <= 3, "Service worker updated too many times." + event.data.version); + version = event.data.version; + } + + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.update_delay", 1], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + // clear version counter + await fetch("self_update_worker.sjs?clearcounter"); + + var worker; + let registration = await navigator.serviceWorker.register( + "self_update_worker.sjs", + { scope: "./test_self_update_worker.html?random=" + Date.now()}) + .then(function(reg) { + worker = reg.installing; + // We can't wait for 'activated' here, since it's possible for + // the update process to kill the worker before it activates. + // See: https://github.com/w3c/ServiceWorker/issues/1285 + return waitForState(worker, 'activating', reg); + }); + + // We need to wait a reasonable time to give the self updating worker a chance + // to change to a newer version. Register and activate an empty worker 5 times. + for (i = 0; i < 5; i++) { + await activateDummyWorker(); + } + + is(version, 3, "Service worker version should be 3."); + + await registration.unregister(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_service_worker_allowed.html b/dom/serviceworkers/test/test_service_worker_allowed.html new file mode 100644 index 0000000000..a74379f383 --- /dev/null +++ b/dom/serviceworkers/test/test_service_worker_allowed.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the Service-Worker-Allowed header</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"></div> +<script class="testbody" type="text/javascript"> + var gTests = [ + "worker_scope_different.js", + "worker_scope_different2.js", + "worker_scope_too_deep.js", + ]; + + function testPermissiveHeader() { + // Make sure that this registration succeeds, as the prefix check should pass. + return navigator.serviceWorker.register("swa/worker_scope_too_narrow.js", {scope: "swa/"}) + .then(swr => { + ok(true, "Registration should finish successfully"); + return swr.unregister(); + }, err => { + ok(false, "Unexpected error when registering the service worker: " + err); + }); + } + + function testPreciseHeader() { + // Make sure that this registration succeeds, as the prefix check should pass + // given that we parse the use the full pathname from this URL.. + return navigator.serviceWorker.register("swa/worker_scope_precise.js", {scope: "swa/"}) + .then(swr => { + ok(true, "Registration should finish successfully"); + return swr.unregister(); + }, err => { + ok(false, "Unexpected error when registering the service worker: " + err); + }); + } + + function runTest() { + Promise.all(gTests.map(testName => { + return new Promise((resolve, reject) => { + // Make sure that registration fails. + navigator.serviceWorker.register("swa/" + testName, {scope: "swa/"}) + .then(reject, resolve); + }); + })).then(values => { + values.forEach(error => { + is(error.name, "SecurityError", "Registration should fail"); + }); + Promise.all([ + testPermissiveHeader(), + testPreciseHeader(), + ]).then(SimpleTest.finish, SimpleTest.finish); + }, (x) => { + ok(false, "Registration should not succeed, but it did"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker.html b/dom/serviceworkers/test/test_serviceworker.html new file mode 100644 index 0000000000..bfc5749405 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker.html @@ -0,0 +1,79 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1137245 - Allow IndexedDB usage in ServiceWorkers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + var regisration; + function simpleRegister() { + return navigator.serviceWorker.register("service_worker.js", { + scope: 'service_worker_client.html' + }).then(swr => waitForState(swr.installing, 'activated', swr)); + } + + function unregister() { + return registration.unregister(); + } + + function testIndexedDBAvailable(sw) { + registration = sw; + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "READY") { + sw.active.postMessage("GO"); + return; + } + + if (!("available" in e.data)) { + ok(false, "Something went wrong"); + reject(); + return; + } + + ok(e.data.available, "IndexedDB available in service worker."); + resolve(); + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "service_worker_client.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + simpleRegister() + .then(testIndexedDBAvailable) + .then(unregister) + .then(SimpleTest.finish) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker_header.html b/dom/serviceworkers/test/test_serviceworker_header.html new file mode 100644 index 0000000000..f607aeba3d --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_header.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that service worker scripts are fetched with a Service-Worker: script header</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker.register("http://mochi.test:8888/tests/dom/serviceworkers/test/header_checker.sjs") + .then(reg => { + ok(true, "Register should succeed"); + reg.unregister().then(() => SimpleTest.finish()); + }, err => { + ok(false, "Register should not fail"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker_interfaces.html b/dom/serviceworkers/test/test_serviceworker_interfaces.html new file mode 100644 index 0000000000..3b4ec19134 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_interfaces.html @@ -0,0 +1,116 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Validate Interfaces Exposed to Service Workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="../worker_driver.js"></script> +</head> +<body> +<script class="testbody" type="text/javascript"> + + function setupSW(registration) { + var iframe; + var worker = registration.installing || + registration.waiting || + registration.active; + window.onmessage = function(event) { + if (event.data.type == 'finish') { + iframe.remove(); + registration.unregister().then(function(success) { + ok(success, "The service worker should be unregistered successfully"); + + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + + } else if (event.data.type == 'getPrefs') { + let result = {}; + event.data.prefs.forEach(function(pref) { + result[pref] = SpecialPowers.Services.prefs.getBoolPref(pref); + }); + worker.postMessage({ + type: 'returnPrefs', + prefs: event.data.prefs, + result + }); + + } else if (event.data.type == 'getHelperData') { + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const isNightly = AppConstants.NIGHTLY_BUILD; + const isEarlyBetaOrEarlier = AppConstants.EARLY_BETA_OR_EARLIER; + const isRelease = AppConstants.RELEASE_OR_BETA; + const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); + const isMac = AppConstants.platform == "macosx"; + const isWindows = AppConstants.platform == "win"; + const isAndroid = AppConstants.platform == "android"; + const isLinux = AppConstants.platform == "linux"; + const isInsecureContext = !window.isSecureContext; + // Currently, MOZ_APP_NAME is always "fennec" for all mobile builds, so we can't use AppConstants for this + const isFennec = isAndroid && SpecialPowers.Cc["@mozilla.org/android/bridge;1"].getService(SpecialPowers.Ci.nsIAndroidBridge).isFennec; + + const result = { + isNightly, isEarlyBetaOrEarlier, isRelease, isDesktop, isMac, + isWindows, isAndroid, isLinux, isInsecureContext, isFennec + }; + + worker.postMessage({ + type: 'returnHelperData', result + }); + } + } + + worker.onerror = function(event) { + ok(false, 'Worker had an error: ' + event.data); + SimpleTest.finish(); + }; + + iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + worker.postMessage({ script: "test_serviceworker_interfaces.js" }); + }; + document.body.appendChild(iframe); + } + + function runTest() { + navigator.serviceWorker.register("serviceworker_wrapper.js", {scope: "."}) + .then(setupSW); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + // The handling of "dom.caches.enabled" here is a bit complicated. What we + // want to happen is that Cache is always enabled in service workers. So + // if service workers are disabled by default we want to force on both + // service workers and "dom.caches.enabled". But if service workers are + // enabled by default, we do not want to mess with the "dom.caches.enabled" + // value, since that would defeat the purpose of the test. Use a subframe + // to decide whether service workers are enabled by default, so we don't + // force creation of our own Navigator object before our prefs are set. + var prefs = [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]; + + var subframe = document.createElement("iframe"); + document.body.appendChild(subframe); + if (!("serviceWorker" in subframe.contentWindow.navigator)) { + prefs.push(["dom.caches.enabled", true]); + } + subframe.remove(); + + SpecialPowers.pushPrefEnv({"set": prefs}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker_interfaces.js b/dom/serviceworkers/test/test_serviceworker_interfaces.js new file mode 100644 index 0000000000..379b3faec9 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_interfaces.js @@ -0,0 +1,549 @@ +// This is a list of all interfaces that are exposed to workers. +// Please only add things to this list with great care and proper review +// from the associated module peers. + +// This file lists global interfaces we want exposed and verifies they +// are what we intend. Each entry in the arrays below can either be a +// simple string with the interface name, or an object with a 'name' +// property giving the interface name as a string, and additional +// properties which qualify the exposure of that interface. For example: +// +// [ +// "AGlobalInterface", +// { name: "ExperimentalThing", release: false }, +// { name: "ReallyExperimentalThing", nightly: true }, +// { name: "DesktopOnlyThing", desktop: true }, +// { name: "FancyControl", xbl: true }, +// { name: "DisabledEverywhere", disabled: true }, +// ]; +// +// See createInterfaceMap() below for a complete list of properties. + +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +let wasmGlobalEntry = { + name: "WebAssembly", + insecureContext: true, + disabled: !getJSTestingFunctions().wasmIsSupportedByHardware(), +}; +let wasmGlobalInterfaces = [ + { name: "Module", insecureContext: true }, + { name: "Instance", insecureContext: true }, + { name: "Memory", insecureContext: true }, + { name: "Table", insecureContext: true }, + { name: "Global", insecureContext: true }, + { name: "CompileError", insecureContext: true }, + { name: "LinkError", insecureContext: true }, + { name: "RuntimeError", insecureContext: true }, + { name: "Function", insecureContext: true, nightly: true }, + { name: "Exception", insecureContext: true }, + { name: "Tag", insecureContext: true }, + { name: "compile", insecureContext: true }, + { name: "compileStreaming", insecureContext: true }, + { name: "instantiate", insecureContext: true }, + { name: "instantiateStreaming", insecureContext: true }, + { name: "validate", insecureContext: true }, +]; +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +let ecmaGlobals = [ + "AggregateError", + "Array", + "ArrayBuffer", + "Atomics", + "Boolean", + "BigInt", + "BigInt64Array", + "BigUint64Array", + "DataView", + "Date", + "Error", + "EvalError", + "FinalizationRegistry", + "Float32Array", + "Float64Array", + "Function", + "Infinity", + "Int16Array", + "Int32Array", + "Int8Array", + "InternalError", + "Intl", + "JSON", + "Map", + "Math", + "NaN", + "Number", + "Object", + "Promise", + "Proxy", + "RangeError", + "ReferenceError", + "Reflect", + "RegExp", + "Set", + { + name: "SharedArrayBuffer", + crossOriginIsolated: true, + }, + "String", + "Symbol", + "SyntaxError", + "TypeError", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "URIError", + "WeakMap", + "WeakRef", + "WeakSet", + wasmGlobalEntry, + "decodeURI", + "decodeURIComponent", + "encodeURI", + "encodeURIComponent", + "escape", + "eval", + "globalThis", + "isFinite", + "isNaN", + "parseFloat", + "parseInt", + "undefined", + "unescape", +]; +// IMPORTANT: Do not change the list above without review from +// a JavaScript Engine peer! + +// IMPORTANT: Do not change the list below without review from a DOM peer! +let interfaceNamesInGlobalScope = [ + // IMPORTANT: Do not change this list without review from a DOM peer! + "AbortController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "AbortSignal", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Blob", + // IMPORTANT: Do not change this list without review from a DOM peer! + "BroadcastChannel", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ByteLengthQueuingStrategy", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Cache", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CacheStorage", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CanvasGradient", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CanvasPattern", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Client", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Clients", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CloseEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CountQueuingStrategy", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Crypto", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CustomEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Directory", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMException", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMMatrix", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMMatrixReadOnly", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMPoint", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMPointReadOnly", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMQuad", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRect", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRectReadOnly", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMStringList", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ErrorEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Event", + // IMPORTANT: Do not change this list without review from a DOM peer! + "EventTarget", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ExtendableEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ExtendableMessageEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FetchEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "File", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FileList", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FileReader", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemDirectoryHandle", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemFileHandle", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemHandle", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemWritableFileStream", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "FontFace", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FontFaceSet", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FontFaceSetLoadEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FormData", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Headers", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursor", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursorWithValue", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBDatabase", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBFactory", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBIndex", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBKeyRange", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBObjectStore", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBOpenDBRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBTransaction", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBVersionChangeEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmap", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmapRenderingContext", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ImageData", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Lock", + // IMPORTANT: Do not change this list without review from a DOM peer! + "LockManager", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MediaCapabilities", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MediaCapabilitiesInfo", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MessageChannel", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MessageEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MessagePort", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "NetworkInformation", disabled: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "NavigationPreloadManager", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Notification", + // IMPORTANT: Do not change this list without review from a DOM peer! + "NotificationEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "OffscreenCanvas", + // IMPORTANT: Do not change this list without review from a DOM peer! + "OffscreenCanvasRenderingContext2D", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Path2D", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Performance", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceEntry", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMark", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMeasure", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceObserver", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceObserverEntryList", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceResourceTiming", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceServerTiming", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ProgressEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PromiseRejectionEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushEvent" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushManager" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushMessageData" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushSubscription" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushSubscriptionOptions" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableByteStreamController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamBYOBReader", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamBYOBRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamDefaultController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamDefaultReader", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Request", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Response", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Scheduler", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorker", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorkerGlobalScope", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorkerRegistration", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "StorageManager", fennec: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "SubtleCrypto", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskController", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskPriorityChangeEvent", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskSignal", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextDecoder", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextDecoderStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextEncoder", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextEncoderStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextMetrics", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TransformStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TransformStreamDefaultController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "URL", + // IMPORTANT: Do not change this list without review from a DOM peer! + "URLSearchParams", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebSocket", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGL2RenderingContext", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLActiveInfo", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLBuffer", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLContextEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLFramebuffer", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLProgram", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLQuery", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLRenderbuffer", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLRenderingContext", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLSampler", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLShader", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLShaderPrecisionFormat", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLSync", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLTexture", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLTransformFeedback", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLUniformLocation", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLVertexArrayObject", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WindowClient", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerGlobalScope", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerLocation", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerNavigator", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WritableStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WritableStreamDefaultController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WritableStreamDefaultWriter", + // IMPORTANT: Do not change this list without review from a DOM peer! + "clients", + // IMPORTANT: Do not change this list without review from a DOM peer! + "console", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onactivate", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onfetch", + // IMPORTANT: Do not change this list without review from a DOM peer! + "oninstall", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onmessage", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onmessageerror", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onnotificationclick", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onnotificationclose", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onpush", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onpushsubscriptionchange", + // IMPORTANT: Do not change this list without review from a DOM peer! + "registration", + // IMPORTANT: Do not change this list without review from a DOM peer! + "skipWaiting", + // IMPORTANT: Do not change this list without review from a DOM peer! +]; +// IMPORTANT: Do not change the list above without review from a DOM peer! + +// List of functions defined on the global by the test harness or this test +// file. +let testFunctions = [ + "ok", + "is", + "workerTestArrayEquals", + "workerTestDone", + "workerTestGetHelperData", + "workerTestGetStorageManager", + "entryDisabled", + "createInterfaceMap", + "runTest", +]; + +function entryDisabled( + entry, + { + isNightly, + isEarlyBetaOrEarlier, + isRelease, + isDesktop, + isAndroid, + isInsecureContext, + isFennec, + isCrossOriginIsolated, + } +) { + return ( + entry.nightly === !isNightly || + (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) || + (entry.nonReleaseAndroid === !(isAndroid && !isRelease) && isAndroid) || + entry.desktop === !isDesktop || + (entry.android === !isAndroid && + !entry.nonReleaseAndroid && + !entry.nightlyAndroid) || + entry.fennecOrDesktop === (isAndroid && !isFennec) || + entry.fennec === !isFennec || + entry.release === !isRelease || + entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier || + entry.crossOriginIsolated === !isCrossOriginIsolated || + entry.disabled + ); +} + +function createInterfaceMap(data, ...interfaceGroups) { + var interfaceMap = {}; + + function addInterfaces(interfaces) { + for (var entry of interfaces) { + if (typeof entry === "string") { + ok(!(entry in interfaceMap), "duplicate entry for " + entry); + interfaceMap[entry] = true; + } else { + ok(!(entry.name in interfaceMap), "duplicate entry for " + entry.name); + ok(!("pref" in entry), "Bogus pref annotation for " + entry.name); + if (entryDisabled(entry, data)) { + interfaceMap[entry.name] = false; + } else if (entry.optional) { + interfaceMap[entry.name] = "optional"; + } else { + interfaceMap[entry.name] = true; + } + } + } + } + + for (let interfaceGroup of interfaceGroups) { + addInterfaces(interfaceGroup); + } + + return interfaceMap; +} + +function runTest(parentName, parent, data, ...interfaceGroups) { + var interfaceMap = createInterfaceMap(data, ...interfaceGroups); + for (var name of Object.getOwnPropertyNames(parent)) { + // Ignore functions on the global that are part of the test (harness). + if (parent === self && testFunctions.includes(name)) { + continue; + } + ok( + interfaceMap[name] === "optional" || interfaceMap[name], + "If this is failing: DANGER, are you sure you want to expose the new interface " + + name + + " to all webpages as a property on " + + parentName + + "? Do not make a change to this file without a " + + " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)" + ); + delete interfaceMap[name]; + } + for (var name of Object.keys(interfaceMap)) { + if (interfaceMap[name] === "optional") { + delete interfaceMap[name]; + } else { + ok( + name in parent === interfaceMap[name], + name + + " should " + + (interfaceMap[name] ? "" : " NOT") + + " be defined on " + + parentName + ); + if (!interfaceMap[name]) { + delete interfaceMap[name]; + } + } + } + is( + Object.keys(interfaceMap).length, + 0, + "The following interface(s) are not enumerated: " + + Object.keys(interfaceMap).join(", ") + ); +} + +workerTestGetHelperData(function(data) { + runTest("self", self, data, ecmaGlobals, interfaceNamesInGlobalScope); + if (WebAssembly && !entryDisabled(wasmGlobalEntry, data)) { + runTest("WebAssembly", WebAssembly, data, wasmGlobalInterfaces); + } + workerTestDone(); +}); diff --git a/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html b/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html new file mode 100644 index 0000000000..33b4428e95 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1141274 - test that service workers and shared workers are separate</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + const SCOPE = "http://mochi.test:8888/tests/dom/serviceworkers/test/"; + function runTest() { + navigator.serviceWorker.ready.then(setupSW); + navigator.serviceWorker.register("serviceworker_not_sharedworker.js", + {scope: SCOPE}); + } + + var sw, worker; + function setupSW(registration) { + sw = registration.waiting || registration.active; + worker = new SharedWorker("serviceworker_not_sharedworker.js", SCOPE); + worker.port.start(); + iframe = document.querySelector("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + window.onmessage = function(e) { + is(e.data.result, "serviceworker", "We should be talking to a service worker"); + window.onmessage = null; + worker.port.onmessage = function(msg) { + is(msg.data.result, "sharedworker", "We should be talking to a shared worker"); + registration.unregister().then(function(success) { + ok(success, "unregister should succeed"); + SimpleTest.finish(); + }, function(ex) { + dump("Unregistering the SW failed with " + ex + "\n"); + SimpleTest.finish(); + }); + }; + worker.port.postMessage({msg: "whoareyou"}); + }; + sw.postMessage({msg: "whoareyou"}); + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworkerinfo.xhtml b/dom/serviceworkers/test/test_serviceworkerinfo.xhtml new file mode 100644 index 0000000000..a3b0fe8e53 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworkerinfo.xhtml @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerInfo" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkerinfo_iframe.html"; + + function wait_for_active_worker(registration) { + ok(registration, "Registration is valid."); + return new Promise(function(res, rej) { + if (registration.activeWorker) { + res(registration); + return; + } + let listener = { + onChange: function() { + if (registration.activeWorker) { + registration.removeListener(listener); + res(registration); + } + } + } + registration.addListener(listener); + }); + } + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.idle_extended_timeout", 1000000], + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + (async function() { + let iframe = $("iframe"); + let promise = new Promise(function (resolve) { + iframe.onload = function () { + resolve(); + }; + }); + iframe.src = IFRAME_URL; + await promise; + + info("Check that a service worker eventually shuts down."); + promise = Promise.all([ + waitForRegister(EXAMPLE_URL), + waitForServiceWorkerShutdown() + ]); + iframe.contentWindow.postMessage("register", "*"); + let [registration] = await promise; + + // Make sure the worker is active. + registration = await wait_for_active_worker(registration); + + let activeWorker = registration.activeWorker; + ok(activeWorker !== null, "Worker is not active!"); + ok(activeWorker.debugger === null); + + info("Attach a debugger to the service worker, and check that the " + + "service worker is restarted."); + activeWorker.attachDebugger(); + let workerDebugger = activeWorker.debugger; + ok(workerDebugger !== null); + + // Verify debugger properties + ok(workerDebugger.principal instanceof Ci.nsIPrincipal); + is(workerDebugger.url, EXAMPLE_URL + "worker.js"); + + info("Verify that getRegistrationByPrincipal return the same " + + "nsIServiceWorkerRegistrationInfo"); + let reg = swm.getRegistrationByPrincipal(workerDebugger.principal, + workerDebugger.url); + is(reg, registration); + + info("Check that getWorkerByID returns the same nsIWorkerDebugger"); + is(activeWorker, reg.getWorkerByID(workerDebugger.serviceWorkerID)); + + info("Detach the debugger from the service worker, and check that " + + "the service worker eventually shuts down again."); + promise = waitForServiceWorkerShutdown(); + activeWorker.detachDebugger(); + await promise; + ok(activeWorker.debugger === null); + + promise = waitForUnregister(EXAMPLE_URL); + iframe.contentWindow.postMessage("unregister", "*"); + registration = await promise; + + SimpleTest.finish(); + })(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/serviceworkers/test/test_serviceworkermanager.xhtml b/dom/serviceworkers/test/test_serviceworkermanager.xhtml new file mode 100644 index 0000000000..b118b7d3f1 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworkermanager.xhtml @@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkermanager_iframe.html"; + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + (async function() { + let registrations = swm.getAllRegistrations(); + is(registrations.length, 0); + + let iframe = $("iframe"); + let promise = waitForIframeLoad(iframe); + iframe.src = IFRAME_URL; + await promise; + + info("Check that the service worker manager notifies its listeners " + + "when a service worker is registered."); + promise = waitForRegister(EXAMPLE_URL); + iframe.contentWindow.postMessage("register", "*"); + let registration = await promise; + + registrations = swm.getAllRegistrations(); + is(registrations.length, 1); + is(registrations.queryElementAt(0, Ci.nsIServiceWorkerRegistrationInfo), + registration); + + info("Check that the service worker manager does not notify its " + + "listeners when a service worker is registered with the same " + + "scope as an existing registration."); + let listener = { + onRegister: function () { + ok(false, "Listener should not have been notified."); + } + }; + swm.addListener(listener); + iframe.contentWindow.postMessage("register", "*"); + + info("Check that the service worker manager notifies its listeners " + + "when a service worker is unregistered."); + promise = waitForUnregister(EXAMPLE_URL); + iframe.contentWindow.postMessage("unregister", "*"); + registration = await promise; + swm.removeListener(listener); + + registrations = swm.getAllRegistrations(); + is(registrations.length, 0); + + SimpleTest.finish(); + })(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml b/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml new file mode 100644 index 0000000000..5b39350897 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml @@ -0,0 +1,155 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerRegistrationInfo" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkerregistrationinfo_iframe.html"; + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + (async function() { + let iframe = $("iframe"); + let promise = waitForIframeLoad(iframe); + iframe.src = IFRAME_URL; + await promise; + + // The change handler is not guaranteed to be called within the same + // tick of the event loop as the one in which the change happened. + // Because of this, the exact state of the service worker registration + // is only known until the handler returns. + // + // Because then-handlers are resolved asynchronously, the following + // checks are done using callbacks, which are called synchronously + // when then handler is called. These callbacks can return a promise, + // which is used to resolve the promise returned by the function. + + info("Check that a service worker registration notifies its " + + "listeners when its state changes."); + promise = waitForRegister(EXAMPLE_URL, function (registration) { + is(registration.scriptSpec, ""); + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Got change event for updating (byte-check) + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + is(registration.scriptSpec, EXAMPLE_URL + "worker.js"); + ok(registration.evaluatingWorker !== null); + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker !== null); + is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker.js"); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker !== null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activating + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activated + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return registration; + }); + }); + }); + }); + }); + }); + }); + iframe.contentWindow.postMessage("register", "*"); + let registration = await promise; + + promise = waitForServiceWorkerRegistrationChange(registration, function () { + // Got change event for updating (byte-check) + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + is(registration.scriptSpec, EXAMPLE_URL + "worker2.js"); + ok(registration.evaluatingWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker !== null); + is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker2.js"); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker !== null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activating + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activated + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return registration; + }); + }); + }); + }); + }); + }); + iframe.contentWindow.postMessage("register", "*"); + await promise; + + iframe.contentWindow.postMessage("unregister", "*"); + await waitForUnregister(EXAMPLE_URL); + + SimpleTest.finish(); + })(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/serviceworkers/test/test_skip_waiting.html b/dom/serviceworkers/test/test_skip_waiting.html new file mode 100644 index 0000000000..6147ad6b38 --- /dev/null +++ b/dom/serviceworkers/test/test_skip_waiting.html @@ -0,0 +1,86 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration, iframe, content; + + function start() { + return navigator.serviceWorker.register("worker.js", + {scope: "./skip_waiting_scope/"}); + } + + async function waitForActivated(swr) { + registration = swr; + await waitForState(registration.installing, "activated") + + iframe = document.createElement("iframe"); + iframe.setAttribute("src", "skip_waiting_scope/index.html"); + + content = document.getElementById("content"); + content.appendChild(iframe); + + await new Promise(resolve => iframe.onload = resolve); + } + + function checkWhetherItSkippedWaiting() { + var promise = new Promise(function(resolve, reject) { + window.onmessage = function (evt) { + if (evt.data.event === "controllerchange") { + ok(evt.data.controllerScriptURL.match("skip_waiting_installed_worker"), + "The controller changed after skiping the waiting step"); + resolve(); + } else { + ok(false, "Wrong value. Somenting went wrong"); + resolve(); + } + }; + }); + + navigator.serviceWorker.register("skip_waiting_installed_worker.js", + {scope: "./skip_waiting_scope/"}) + .then(swr => { + registration = swr; + }); + + return promise; + } + + function clean() { + content.removeChild(iframe); + + return registration.unregister(); + } + + function runTest() { + start() + .then(waitForActivated) + .then(checkWhetherItSkippedWaiting) + .then(clean) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_streamfilter.html b/dom/serviceworkers/test/test_streamfilter.html new file mode 100644 index 0000000000..7367fb8b84 --- /dev/null +++ b/dom/serviceworkers/test/test_streamfilter.html @@ -0,0 +1,207 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title> + Test StreamFilter-monitored responses for ServiceWorker-intercepted requests + </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + SimpleTest.waitForExplicitFinish(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + const registration = await navigator.serviceWorker.register( + "streamfilter_worker.js" + ); + + SimpleTest.registerCleanupFunction(async function unregisterRegistration() { + await registration.unregister(); + }); + + await new Promise(resolve => { + const serviceWorker = registration.installing; + + serviceWorker.onstatechange = () => { + if (serviceWorker.state == "activated") { + resolve(); + } + }; + }); + + ok(navigator.serviceWorker.controller, "Page is controlled"); +}); + +async function getExtension() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + // This WebExtension only proxies a response's data through a StreamFilter; + // it doesn't modify the data itself in any way. + background() { + class FilterWrapper { + constructor(requestId) { + const filter = browser.webRequest.filterResponseData(requestId); + const arrayBuffers = []; + + filter.onstart = () => { + browser.test.sendMessage("start"); + }; + + filter.ondata = ({ data }) => { + arrayBuffers.push(data); + }; + + filter.onstop = () => { + browser.test.sendMessage("stop"); + new Blob(arrayBuffers).arrayBuffer().then(buffer => { + filter.write(buffer); + filter.close(); + }); + }; + + filter.onerror = () => { + // We only ever expect a redirect error here. + browser.test.assertEq(filter.error, "ServiceWorker fallback redirection"); + browser.test.sendMessage("error"); + }; + } + } + + browser.webRequest.onBeforeRequest.addListener( + details => { + new FilterWrapper(details.requestId); + }, + { + urls: ["<all_urls>"], + types: ["xmlhttprequest"], + }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + return extension; +} + +const streamFilterServerUrl = `${location.origin}/tests/dom/serviceworkers/test/streamfilter_server.sjs`; + +const requestUrlForServerQueryString = "syntheticResponse=0"; + +// streamfilter_server.sjs is expected to respond to a request to this URL. +const requestUrlForServer = `${streamFilterServerUrl}?${requestUrlForServerQueryString}`; + +const requestUrlForServiceWorkerQueryString = "syntheticResponse=1"; + +// streamfilter_worker.js is expected to respond to a request to this URL. +const requestUrlForServiceWorker = `${streamFilterServerUrl}?${requestUrlForServiceWorkerQueryString}`; + +// startNetworkerRequestFn must be a function that, when called, starts a +// network request and returns a promise that resolves after the request +// completes (or fails). This function will return the value that that promise +// resolves with (or throw if it rejects). +async function observeFilteredNetworkRequest(startNetworkRequestFn, promises) { + const networkRequestPromise = startNetworkRequestFn(); + await Promise.all(promises); + return networkRequestPromise; +} + +// Returns a promise that resolves with the XHR's response text. +function callXHR(requestUrl, promises) { + return observeFilteredNetworkRequest(() => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.onload = () => { + resolve(xhr.responseText); + }; + xhr.onerror = reject; + xhr.open("GET", requestUrl); + xhr.send(); + }); + }, promises); +} + +// Returns a promise that resolves with the Fetch's response text. +function callFetch(requestUrl, promises) { + return observeFilteredNetworkRequest(() => { + return fetch(requestUrl).then(response => response.text()); + }, promises); +} + +// The expected response text is always the query string (without the leading +// "?") of the request URL. +add_task(async function callXhrExpectServerResponse() { + info(`Performing XHR at ${requestUrlForServer}...`); + let extension = await getExtension(); + is( + await callXHR(requestUrlForServer, [ + extension.awaitMessage("start"), + extension.awaitMessage("error"), + extension.awaitMessage("stop"), + ]), + requestUrlForServerQueryString, + "Server-supplied response for XHR completed successfully" + ); + await extension.unload(); +}); + +add_task(async function callXhrExpectServiceWorkerResponse() { + info(`Performing XHR at ${requestUrlForServiceWorker}...`); + let extension = await getExtension(); + is( + await callXHR(requestUrlForServiceWorker, [ + extension.awaitMessage("start"), + extension.awaitMessage("stop"), + ]), + requestUrlForServiceWorkerQueryString, + "ServiceWorker-supplied response for XHR completed successfully" + ); + await extension.unload(); +}); + +add_task(async function callFetchExpectServerResponse() { + info(`Performing Fetch at ${requestUrlForServer}...`); + let extension = await getExtension(); + is( + await callFetch(requestUrlForServer, [ + extension.awaitMessage("start"), + extension.awaitMessage("error"), + extension.awaitMessage("stop"), + ]), + requestUrlForServerQueryString, + "Server-supplied response for Fetch completed successfully" + ); + await extension.unload(); +}); + +add_task(async function callFetchExpectServiceWorkerResponse() { + info(`Performing Fetch at ${requestUrlForServiceWorker}...`); + let extension = await getExtension(); + is( + await callFetch(requestUrlForServiceWorker, [ + extension.awaitMessage("start"), + extension.awaitMessage("stop"), + ]), + requestUrlForServiceWorkerQueryString, + "ServiceWorker-supplied response for Fetch completed successfully" + ); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_strict_mode_warning.html b/dom/serviceworkers/test/test_strict_mode_warning.html new file mode 100644 index 0000000000..4df0d1a380 --- /dev/null +++ b/dom/serviceworkers/test/test_strict_mode_warning.html @@ -0,0 +1,42 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1170550 - test registration of service worker scripts with a strict mode warning</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker + .register("strict_mode_warning.js", {scope: "strict_mode_warning"}) + .then((reg) => { + ok(true, "Registration should not fail for warnings"); + return reg.unregister(); + }) + .then(() => { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_third_party_iframes.html b/dom/serviceworkers/test/test_third_party_iframes.html new file mode 100644 index 0000000000..90e9dadfa8 --- /dev/null +++ b/dom/serviceworkers/test/test_third_party_iframes.html @@ -0,0 +1,263 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>Bug 1152899 - Disallow the interception of third-party iframes using service workers when the third-party cookie preference is set</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +var chromeScript; +chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); +}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(2); + +let index = 0; +function next() { + info("Step " + index); + if (index >= steps.length) { + SimpleTest.finish(); + return; + } + try { + let i = index++; + steps[i](); + } catch(ex) { + ok(false, "Caught exception", ex); + } +} + +onload = next; + +let iframe; +let proxyWindow; +let basePath = "/tests/dom/serviceworkers/test/thirdparty/"; +let origin = window.location.protocol + "//" + window.location.host; +let thirdPartyOrigin = "https://example.com"; + +function loadIframe() { + let message = { + source: "parent", + href: origin + basePath + "iframe2.html" + }; + iframe.contentWindow.postMessage(message, "*"); +} + +function loadThirdPartyIframe() { + let message = { + source: "parent", + href: thirdPartyOrigin + basePath + "iframe2.html" + } + iframe.contentWindow.postMessage(message, "*"); +} + +function runTest(aExpectedResponses) { + // Let's use a proxy window to have the new cookie policy applied. + proxyWindow = window.open("window_party_iframes.html"); + proxyWindow.onload = _ => { + iframe = proxyWindow.document.querySelector("iframe"); + iframe.src = thirdPartyOrigin + basePath + "register.html"; + let responsesIndex = 0; + window.onmessage = function(e) { + let status = e.data.status; + let expected = aExpectedResponses[responsesIndex]; + if (status == expected.status) { + ok(true, "Received expected " + expected.status); + if (expected.next) { + expected.next(); + } + } else { + ok(false, "Expected " + expected.status + " got " + status); + } + responsesIndex++; + }; + } +} + +// Verify that we can register and intercept a 3rd party iframe with +// the given cookie policy. +function testShouldIntercept(behavior, done) { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", behavior], + ]}, function() { + runTest([{ + status: "ok" + }, { + status: "registrationdone", + next() { + iframe.src = origin + basePath + "iframe1.html"; + } + }, { + status: "iframeloaded", + next: loadIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next: loadThirdPartyIframe + }, { + status: "swresponse", + }, { + status: "worker-swresponse", + next() { + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }, { + status: "controlled", + }, { + status: "unregistrationdone", + next() { + window.onmessage = null; + proxyWindow.close(); + ok(true, "Test finished successfully"); + done(); + } + }]); + }); +} + +// Verify that we cannot register a service worker in a 3rd party +// iframe with the given cookie policy. +function testShouldNotRegister(behavior, done) { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", behavior], + ]}, function() { + runTest([{ + status: "registrationfailed", + next() { + iframe.src = origin + basePath + "iframe1.html"; + } + }, { + status: "iframeloaded", + next: loadIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next: loadThirdPartyIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next() { + window.onmessage = null; + proxyWindow.close(); + ok(true, "Test finished successfully"); + done(); + } + }]); + }); +} + +// Verify that if a service worker is already registered a 3rd +// party iframe will still not be intercepted with the given cookie +// policy. +function testShouldNotIntercept(behavior, done) { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ]}, function() { + runTest([{ + status: "ok" + }, { + status: "registrationdone", + next() { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", behavior], + ]}, function() { + proxyWindow.close(); + proxyWindow = window.open("window_party_iframes.html"); + proxyWindow.onload = _ => { + iframe = proxyWindow.document.querySelector("iframe"); + iframe.src = origin + basePath + "iframe1.html"; + } + }); + } + }, { + status: "iframeloaded", + next: loadIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next: loadThirdPartyIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next() { + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }, { + status: "uncontrolled", + }, { + status: "getregistrationfailed", + next() { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ]}, function() { + proxyWindow.close(); + proxyWindow = window.open("window_party_iframes.html"); + proxyWindow.onload = _ => { + iframe = proxyWindow.document.querySelector("iframe"); + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }); + } + }, { + status: "controlled", + }, { + status: "unregistrationdone", + next() { + window.onmessage = null; + proxyWindow.close(); + ok(true, "Test finished successfully"); + done(); + } + }]); + }); +} + +const BEHAVIOR_ACCEPT = 0; +const BEHAVIOR_REJECTFOREIGN = 1; +const BEHAVIOR_REJECT = 2; +const BEHAVIOR_LIMITFOREIGN = 3; + +let steps = [() => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.dom.window.dump.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ]}, next); +}, () => { + testShouldNotRegister(BEHAVIOR_REJECTFOREIGN, next); +}, () => { + testShouldNotIntercept(BEHAVIOR_REJECTFOREIGN, next); +}, () => { + testShouldNotRegister(BEHAVIOR_REJECT, next); +}, () => { + testShouldNotIntercept(BEHAVIOR_REJECT, next); +}, () => { + testShouldNotRegister(BEHAVIOR_LIMITFOREIGN, next); +}, () => { + testShouldNotIntercept(BEHAVIOR_LIMITFOREIGN, next); +}, () => { + testShouldIntercept(BEHAVIOR_ACCEPT, next); +}]; + + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_unregister.html b/dom/serviceworkers/test/test_unregister.html new file mode 100644 index 0000000000..959378f03a --- /dev/null +++ b/dom/serviceworkers/test/test_unregister.html @@ -0,0 +1,137 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker.js", { scope: "unregister/" }).then(function(swr) { + if (swr.installing) { + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + if (swr.waiting) { + swr.waiting.onstatechange = function(event) { + if (swr.active) { + resolve(); + } else if (swr.waiting && swr.waiting.state == "redundant") { + reject("Should not go into redundant"); + } + } + } else { + if (swr.active) { + resolve(); + } else { + reject("No waiting and no active!"); + } + } + } + }); + } else { + return Promise.reject("Installing should be non-null"); + } + }); + } + + function testControlled() { + var testPromise = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (!("controlled" in e.data)) { + ok(false, "Something went wrong."); + rej(); + return; + } + + ok(e.data.controlled, "New window should be controlled."); + res(); + } + }) + + var div = document.getElementById("content"); + ok(div, "Parent exists"); + + var ifr = document.createElement("iframe"); + ifr.setAttribute('src', "unregister/index.html"); + div.appendChild(ifr); + + return testPromise.then(function() { + div.removeChild(ifr); + }); + } + + function unregister() { + return navigator.serviceWorker.getRegistration("unregister/") + .then(function(reg) { + if (!reg) { + info("Registration already removed"); + return; + } + + info("getRegistration() succeeded " + reg.scope); + return reg.unregister().then(function(v) { + ok(v, "Unregister should resolve to true"); + }, function(e) { + ok(false, "Unregister failed with " + e.name); + }); + }); + } + + function testUncontrolled() { + var testPromise = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (!("controlled" in e.data)) { + ok(false, "Something went wrong."); + rej(); + return; + } + + ok(!e.data.controlled, "New window should not be controlled."); + res(); + } + }); + + var div = document.getElementById("content"); + ok(div, "Parent exists"); + + var ifr = document.createElement("iframe"); + ifr.setAttribute('src', "unregister/index.html"); + div.appendChild(ifr); + + return testPromise.then(function() { + div.removeChild(ifr); + }); + } + + function runTest() { + simpleRegister() + .then(testControlled) + .then(unregister) + .then(testUncontrolled) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_unresolved_fetch_interception.html b/dom/serviceworkers/test/test_unresolved_fetch_interception.html new file mode 100644 index 0000000000..7182b0fb86 --- /dev/null +++ b/dom/serviceworkers/test/test_unresolved_fetch_interception.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test that an unresolved respondWith promise will reset the channel when + the service worker is terminated due to idling, and that appropriate error + messages are logged for both the termination of the serice worker and the + resetting of the channel. + --> +<head> + <title>Test for Bug 1188545</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(async function grace_timeout_termination_with_interrupted_intercept() { + // Setup timeouts so that the service worker will go into grace timeout after + // a zero-length idle timeout. + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + let registration = await navigator.serviceWorker.register( + "unresolved_fetch_worker.js", { scope: "./"} ); + await waitForState(registration.installing, "activated"); + ok(navigator.serviceWorker.controller, "Controlled"); // double check! + + // We want to make sure the SW is active and processing the fetch before we + // try and kill it. It sends us a message when it has done so. + let waitForFetchActive = new Promise((resolve) => { + navigator.serviceWorker.onmessage = resolve; + }); + + // Issue a fetch which the SW will respondWith() a never resolved promise. + // The fetch, however, will terminate when the SW is killed, so check that. + let hangingFetch = fetch("does_not_exist.html") + .then(() => { ok(false, "should have rejected "); }, + () => { ok(true, "hung fetch terminates when worker dies"); }); + + await waitForFetchActive; + + let expectedMessage = expect_console_message( + // Termination error + "ServiceWorkerGraceTimeoutTermination", + [make_absolute_url("./")], + // The interception failure error generated by the RespondWithHandler + // destructor when it notices it didn't get a response before being + // destroyed. It logs via the intercepted channel nsIConsoleReportCollector + // that is eventually flushed to our document and its console. + "InterceptionFailedWithURL", + [make_absolute_url("does_not_exist.html")] + ); + + // Zero out the grace timeout too so the worker will get terminated after two + // zero-length timer firings. Note that we need to do something to get the + // SW to renew its keepalive for this to actually cause the timers to be + // rescheduled... + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_extended_timeout", 0]]}); + // ...which we do by postMessaging it. + navigator.serviceWorker.controller.postMessage("doomity doom doom"); + + // Now wait for signs that the worker was terminated by the fetch failing. + await hangingFetch; + + // The worker should now be dead and the error logged, wait/assert. + await wait_for_expected_message(expectedMessage); + + // roll back all of our test case specific preferences and otherwise cleanup + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + await registration.unregister(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_workerUnregister.html b/dom/serviceworkers/test/test_workerUnregister.html new file mode 100644 index 0000000000..d0bc1d6ce4 --- /dev/null +++ b/dom/serviceworkers/test/test_workerUnregister.html @@ -0,0 +1,81 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982728 - Test ServiceWorkerGlobalScope.unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="container"></div> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker_unregister.js", { scope: "unregister/" }).then(function(swr) { + if (swr.installing) { + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + if (swr.waiting) { + swr.waiting.onstatechange = function(event) { + if (swr.active) { + resolve(); + } else if (swr.waiting && swr.waiting.state == "redundant") { + reject("Should not go into redundant"); + } + } + } else { + if (swr.active) { + resolve(); + } else { + reject("No waiting and no active!"); + } + } + } + }); + } else { + return Promise.reject("Installing should be non-null"); + } + }); + } + + function waitForMessages(sw) { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "DONE") { + ok(true, "The worker has unregistered itself"); + } else if (e.data === "ERROR") { + ok(false, "The worker has unregistered itself"); + } else if (e.data === "FINISH") { + resolve(); + } + } + }); + + var frame = document.createElement("iframe"); + frame.setAttribute("src", "unregister/unregister.html"); + document.body.appendChild(frame); + + return p; + } + + function runTest() { + simpleRegister().then(waitForMessages).catch(function(e) { + ok(false, "Something went wrong."); + }).then(function() { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_workerUpdate.html b/dom/serviceworkers/test/test_workerUpdate.html new file mode 100644 index 0000000000..015e6bb4ae --- /dev/null +++ b/dom/serviceworkers/test/test_workerUpdate.html @@ -0,0 +1,63 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1065366 - Test ServiceWorkerGlobalScope.update</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="container"></div> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker_update.js", { scope: "workerUpdate/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)); + } + + var registration; + function waitForMessages(sw) { + registration = sw; + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "FINISH") { + ok(true, "The worker has updated itself"); + resolve(); + } else if (e.data === "FAIL") { + ok(false, "The worker failed to update itself"); + resolve(); + } + } + }); + + var frame = document.createElement("iframe"); + frame.setAttribute("src", "workerUpdate/update.html"); + document.body.appendChild(frame); + + return p; + } + + function runTest() { + simpleRegister().then(waitForMessages).catch(function(e) { + ok(false, "Something went wrong."); + }).then(function() { + return registration.unregister(); + }).then(function() { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_worker_reference_gc_timeout.html b/dom/serviceworkers/test/test_worker_reference_gc_timeout.html new file mode 100644 index 0000000000..cf04e13f2e --- /dev/null +++ b/dom/serviceworkers/test/test_worker_reference_gc_timeout.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- + --> +<head> + <title>Test for Bug 1317266</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1317266">Mozilla Bug 1317266</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="text/javascript"> +SimpleTest.requestFlakyTimeout("Forcing a race with the cycle collector."); + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(async function test_worker_ref_gc() { + let registration = await navigator.serviceWorker.register( + "lazy_worker.js", { scope: "./lazy_worker_scope_timeout"} ) + .then(function(reg) { + SpecialPowers.exactGC(); + var worker = reg.installing; + return new Promise(function(resolve) { + worker.addEventListener('statechange', function() { + info("state is " + worker.state + "\n"); + SpecialPowers.exactGC(); + if (worker.state === 'activated') { + resolve(reg); + } + }); + }); + }); + ok(true, "Got activated event!"); + + await registration.unregister(); +}); + +add_task(async function test_worker_ref_gc_ready_promise() { + let wait_active = navigator.serviceWorker.ready.then(function(reg) { + SpecialPowers.exactGC(); + ok(reg.active, "Got active worker."); + ok(reg.active.state === "activating", "Worker is in activating state"); + return new Promise(function(res) { + reg.active.onstatechange = function(e) { + reg.active.onstatechange = null; + ok(reg.active.state === "activated", "Worker was activated"); + res(); + } + }); + }); + + let registration = await navigator.serviceWorker.register( + "lazy_worker.js", { scope: "."} ); + await wait_active; + await registration.unregister(); +}); + +add_task(async function cleanup() { + await SpecialPowers.popPrefEnv(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_workerupdatefoundevent.html b/dom/serviceworkers/test/test_workerupdatefoundevent.html new file mode 100644 index 0000000000..1c3ced13bd --- /dev/null +++ b/dom/serviceworkers/test/test_workerupdatefoundevent.html @@ -0,0 +1,91 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var promise; + + async function start() { + registration = await navigator.serviceWorker.register("worker_updatefoundevent.js", + { scope: "./updatefoundevent.html" }) + await waitForState(registration.installing, 'activated'); + + content = document.getElementById("content"); + iframe = document.createElement("iframe"); + content.appendChild(iframe); + iframe.setAttribute("src", "./updatefoundevent.html"); + + await new Promise(function(resolve) { iframe.onload = resolve; }); + ok(iframe.contentWindow.navigator.serviceWorker.controller, "Controlled client."); + + return Promise.resolve(); + + } + + function startWaitForUpdateFound() { + registration.onupdatefound = function(e) { + } + + promise = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + + if (e.data == "finish") { + ok(true, "Received updatefound"); + resolve(); + } + } + }); + + return Promise.resolve(); + } + + function registerNext() { + return navigator.serviceWorker.register("worker_updatefoundevent2.js", + { scope: "./updatefoundevent.html" }); + } + + function waitForUpdateFound() { + return promise; + } + + function unregister() { + window.onmessage = null; + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function runTest() { + start() + .then(startWaitForUpdateFound) + .then(registerNext) + .then(waitForUpdateFound) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_xslt.html b/dom/serviceworkers/test/test_xslt.html new file mode 100644 index 0000000000..a955c843ac --- /dev/null +++ b/dom/serviceworkers/test/test_xslt.html @@ -0,0 +1,117 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182113 - Test service worker XSLT interception</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var worker; + + function start() { + return navigator.serviceWorker.register("xslt_worker.js", + { scope: "./" }) + .then((swr) => { + registration = swr; + + // Ensure the registration is active before continuing + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getXmlString(xmlObject) { + serializer = new XMLSerializer(); + return serializer.serializeToString(iframe.contentDocument); + } + + function synthetic() { + content = document.getElementById("content"); + ok(content, "parent exists."); + + iframe = document.createElement("iframe"); + content.appendChild(iframe); + + iframe.setAttribute('src', "xslt/test.xml"); + + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + dump("Set request mode\n"); + registration.active.postMessage("synthetic"); + xmlString = getXmlString(iframe.contentDocument); + ok(!xmlString.includes("Error"), "Load synthetic cross origin XSLT should be allowed"); + res(); + }; + }); + + return p; + } + + function cors() { + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + xmlString = getXmlString(iframe.contentDocument); + ok(!xmlString.includes("Error"), "Load CORS cross origin XSLT should be allowed"); + res(); + }; + }); + + registration.active.postMessage("cors"); + iframe.setAttribute('src', "xslt/test.xml"); + + return p; + } + + function opaque() { + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + xmlString = getXmlString(iframe.contentDocument); + ok(xmlString.includes("Error"), "Load opaque cross origin XSLT should not be allowed"); + res(); + }; + }); + + registration.active.postMessage("opaque"); + iframe.setAttribute('src', "xslt/test.xml"); + + return p; + } + + function runTest() { + start() + .then(synthetic) + .then(opaque) + .then(cors) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/thirdparty/iframe1.html b/dom/serviceworkers/test/thirdparty/iframe1.html new file mode 100644 index 0000000000..e8982d306a --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/iframe1.html @@ -0,0 +1,42 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>SW third party iframe test</title> + + <script type="text/javascript"> + function messageListener(event) { + let message = event.data; + + dump("got message " + JSON.stringify(message) + "\n"); + if (message.source == "parent") { + document.getElementById("iframe2").src = message.href; + } + else if (message.source == "iframe") { + parent.postMessage(event.data, "*"); + } else if (message.source == "worker") { + parent.postMessage(event.data, "*"); + } + } + </script> + +</head> + +<body> + <script> + onload = function() { + window.addEventListener('message', messageListener); + let message = { + source: "iframe", + status: "iframeloaded", + } + parent.postMessage(message, "*"); + } + </script> + <iframe id="iframe2"></iframe> +</body> + +</html> diff --git a/dom/serviceworkers/test/thirdparty/iframe2.html b/dom/serviceworkers/test/thirdparty/iframe2.html new file mode 100644 index 0000000000..8013899195 --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/iframe2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({ + source: "iframe", + status: "networkresponse" + }, "*"); + var w = new Worker('worker.js'); + w.onmessage = function(evt) { + window.parent.postMessage({ + source: 'worker', + status: evt.data, + }, '*'); + }; +</script> diff --git a/dom/serviceworkers/test/thirdparty/register.html b/dom/serviceworkers/test/thirdparty/register.html new file mode 100644 index 0000000000..b166acb8a4 --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/register.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + var isDone = false; + function done(reg) { + if (!isDone) { + ok(reg.waiting || reg.active, + "Either active or waiting worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + isDone = true; + } + } + + navigator.serviceWorker.register("sw.js", {scope: "."}) + .then(function(registration) { + if (registration.installing) { + registration.installing.onstatechange = function(e) { + done(registration); + }; + } else { + done(registration); + } + }).catch(function(e) { + window.parent.postMessage({status: "registrationfailed"}, "*"); + }); +</script> diff --git a/dom/serviceworkers/test/thirdparty/sw.js b/dom/serviceworkers/test/thirdparty/sw.js new file mode 100644 index 0000000000..ea844e8f2b --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/sw.js @@ -0,0 +1,33 @@ +self.addEventListener("fetch", function(event) { + dump("fetch " + event.request.url + "\n"); + if (event.request.url.includes("iframe2.html")) { + var body = + "<script>" + + "window.parent.postMessage({" + + "source: 'iframe', status: 'swresponse'" + + "}, '*');" + + "var w = new Worker('worker.js');" + + "w.onmessage = function(evt) {" + + "window.parent.postMessage({" + + "source: 'worker'," + + "status: evt.data," + + "}, '*');" + + "};" + + "</script>"; + event.respondWith( + new Response(body, { + headers: { "Content-Type": "text/html" }, + }) + ); + return; + } + if (event.request.url.includes("worker.js")) { + var body = "self.postMessage('worker-swresponse');"; + event.respondWith( + new Response(body, { + headers: { "Content-Type": "application/javascript" }, + }) + ); + return; + } +}); diff --git a/dom/serviceworkers/test/thirdparty/unregister.html b/dom/serviceworkers/test/thirdparty/unregister.html new file mode 100644 index 0000000000..65b29d5648 --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/unregister.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script> + if (navigator.serviceWorker.controller) { + window.parent.postMessage({status: "controlled"}, "*"); + } else { + window.parent.postMessage({status: "uncontrolled"}, "*"); + } + + navigator.serviceWorker.getRegistration(".").then(function(registration) { + if(!registration) { + return; + } + registration.unregister().then(() => { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + }); + }).catch(function(e) { + window.parent.postMessage({status: "getregistrationfailed"}, "*"); + }); +</script> diff --git a/dom/serviceworkers/test/thirdparty/worker.js b/dom/serviceworkers/test/thirdparty/worker.js new file mode 100644 index 0000000000..bbdc608cde --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/worker.js @@ -0,0 +1 @@ +self.postMessage("worker-networkresponse"); diff --git a/dom/serviceworkers/test/unregister/index.html b/dom/serviceworkers/test/unregister/index.html new file mode 100644 index 0000000000..36cac9fcf6 --- /dev/null +++ b/dom/serviceworkers/test/unregister/index.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("unregister/index.html should not to be launched directly!"); + } + + parent.postMessage({ controlled: !!navigator.serviceWorker.controller }, "*"); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/unregister/unregister.html b/dom/serviceworkers/test/unregister/unregister.html new file mode 100644 index 0000000000..42633ca343 --- /dev/null +++ b/dom/serviceworkers/test/unregister/unregister.html @@ -0,0 +1,21 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test worker::unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + + navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); } + navigator.serviceWorker.controller.postMessage("GO"); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/unresolved_fetch_worker.js b/dom/serviceworkers/test/unresolved_fetch_worker.js new file mode 100644 index 0000000000..80d39b037a --- /dev/null +++ b/dom/serviceworkers/test/unresolved_fetch_worker.js @@ -0,0 +1,18 @@ +var keepPromiseAlive; +onfetch = function(event) { + event.waitUntil( + clients.matchAll().then(clients => { + clients.forEach(client => { + client.postMessage("continue"); + }); + }) + ); + + // Never resolve, and keep it alive on our global so it can't get GC'ed and + // make this test weird and intermittent. + event.respondWith((keepPromiseAlive = new Promise(function(res, rej) {}))); +}; + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/update_worker.sjs b/dom/serviceworkers/test/update_worker.sjs new file mode 100644 index 0000000000..44782a2732 --- /dev/null +++ b/dom/serviceworkers/test/update_worker.sjs @@ -0,0 +1,12 @@ +/* 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/. */ +"use strict"; + +function handleRequest(request, response) { + // This header is necessary for making this script able to be loaded. + response.setHeader("Content-Type", "application/javascript"); + + var body = "/* " + Date.now() + " */"; + response.write(body); +} diff --git a/dom/serviceworkers/test/updatefoundevent.html b/dom/serviceworkers/test/updatefoundevent.html new file mode 100644 index 0000000000..78088c7cd0 --- /dev/null +++ b/dom/serviceworkers/test/updatefoundevent.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title> +</head> +<body> +<script> + navigator.serviceWorker.onmessage = function(e) { + dump("NSM iframe got message " + e.data + "\n"); + window.parent.postMessage(e.data, "*"); + }; +</script> +</body> diff --git a/dom/serviceworkers/test/utils.js b/dom/serviceworkers/test/utils.js new file mode 100644 index 0000000000..28be239593 --- /dev/null +++ b/dom/serviceworkers/test/utils.js @@ -0,0 +1,136 @@ +function waitForState(worker, state, context) { + return new Promise(resolve => { + function onStateChange() { + if (worker.state === state) { + worker.removeEventListener("statechange", onStateChange); + resolve(context); + } + } + + // First add an event listener, so we won't miss any change that happens + // before we check the current state. + worker.addEventListener("statechange", onStateChange); + + // Now check if the worker is already in the desired state. + onStateChange(); + }); +} + +/** + * Helper for browser tests to issue register calls from the content global and + * wait for the SW to progress to the active state, as most tests desire. + * From the ContentTask.spawn, use via + * `content.wrappedJSObject.registerAndWaitForActive`. + */ +async function registerAndWaitForActive(script, maybeScope) { + console.log("...calling register"); + let opts = undefined; + if (maybeScope) { + opts = { scope: maybeScope }; + } + const reg = await navigator.serviceWorker.register(script, opts); + // Unless registration resurrection happens, the SW should be in the + // installing slot. + console.log("...waiting for activation"); + await waitForState(reg.installing, "activated", reg); + console.log("...activated!"); + return reg; +} + +/** + * Helper to create an iframe with the given URL and return the first + * postMessage payload received. This is intended to be used when creating + * cross-origin iframes. + * + * A promise will be returned that resolves with the payload of the postMessage + * call. + */ +function createIframeAndWaitForMessage(url) { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + return new Promise(resolve => { + window.addEventListener( + "message", + event => { + resolve(event.data); + }, + { once: true } + ); + iframe.src = url; + }); +} + +/** + * Helper to create a nested iframe into the iframe created by + * createIframeAndWaitForMessage(). + * + * A promise will be returned that resolves with the payload of the postMessage + * call. + */ +function createNestedIframeAndWaitForMessage(url) { + const iframe = document.getElementsByTagName("iframe")[0]; + iframe.contentWindow.postMessage("create nested iframe", "*"); + return new Promise(resolve => { + window.addEventListener( + "message", + event => { + resolve(event.data); + }, + { once: true } + ); + }); +} + +async function unregisterAll() { + const registrations = await navigator.serviceWorker.getRegistrations(); + for (const reg of registrations) { + await reg.unregister(); + } +} + +/** + * Make a blob that contains random data and therefore shouldn't compress all + * that well. + */ +function makeRandomBlob(size) { + const arr = new Uint8Array(size); + let offset = 0; + /** + * getRandomValues will only provide a maximum of 64k of data at a time and + * will error if we ask for more, so using a while loop for get a random value + * which much larger than 64k. + * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#exceptions + */ + while (offset < size) { + const nextSize = Math.min(size - offset, 65536); + window.crypto.getRandomValues(new Uint8Array(arr.buffer, offset, nextSize)); + offset += nextSize; + } + return new Blob([arr], { type: "application/octet-stream" }); +} + +async function fillStorage(cacheBytes, idbBytes) { + // ## Fill Cache API Storage + const cache = await caches.open("filler"); + await cache.put("fill", new Response(makeRandomBlob(cacheBytes))); + + // ## Fill IDB + const storeName = "filler"; + let db = await new Promise((resolve, reject) => { + let openReq = indexedDB.open("filler", 1); + openReq.onerror = event => { + reject(event.target.error); + }; + openReq.onsuccess = event => { + resolve(event.target.result); + }; + openReq.onupgradeneeded = event => { + const useDB = event.target.result; + useDB.onerror = error => { + reject(error); + }; + const store = useDB.createObjectStore(storeName); + store.put({ blob: makeRandomBlob(idbBytes) }, "filler-blob"); + }; + }); +} diff --git a/dom/serviceworkers/test/window_party_iframes.html b/dom/serviceworkers/test/window_party_iframes.html new file mode 100644 index 0000000000..abeea4449b --- /dev/null +++ b/dom/serviceworkers/test/window_party_iframes.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> +<iframe></iframe> +<script> +window.onmessage = e => { + opener.postMessage(e.data, "*"); +} +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/worker.js b/dom/serviceworkers/test/worker.js new file mode 100644 index 0000000000..2aba167d18 --- /dev/null +++ b/dom/serviceworkers/test/worker.js @@ -0,0 +1 @@ +// empty worker, always succeed! diff --git a/dom/serviceworkers/test/worker2.js b/dom/serviceworkers/test/worker2.js new file mode 100644 index 0000000000..3072d0817f --- /dev/null +++ b/dom/serviceworkers/test/worker2.js @@ -0,0 +1 @@ +// worker2.js diff --git a/dom/serviceworkers/test/worker3.js b/dom/serviceworkers/test/worker3.js new file mode 100644 index 0000000000..449fc2f976 --- /dev/null +++ b/dom/serviceworkers/test/worker3.js @@ -0,0 +1 @@ +// worker3.js diff --git a/dom/serviceworkers/test/workerUpdate/update.html b/dom/serviceworkers/test/workerUpdate/update.html new file mode 100644 index 0000000000..666e213d14 --- /dev/null +++ b/dom/serviceworkers/test/workerUpdate/update.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test worker::update</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + + navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); } + navigator.serviceWorker.ready.then(function() { + navigator.serviceWorker.controller.postMessage("GO"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/worker_unregister.js b/dom/serviceworkers/test/worker_unregister.js new file mode 100644 index 0000000000..a2b45245b4 --- /dev/null +++ b/dom/serviceworkers/test/worker_unregister.js @@ -0,0 +1,22 @@ +onmessage = function(e) { + clients.matchAll().then(function(c) { + if (c.length === 0) { + // We cannot proceed. + return; + } + + registration + .unregister() + .then( + function() { + c[0].postMessage("DONE"); + }, + function() { + c[0].postMessage("ERROR"); + } + ) + .then(function() { + c[0].postMessage("FINISH"); + }); + }); +}; diff --git a/dom/serviceworkers/test/worker_update.js b/dom/serviceworkers/test/worker_update.js new file mode 100644 index 0000000000..5c83b9cd9e --- /dev/null +++ b/dom/serviceworkers/test/worker_update.js @@ -0,0 +1,25 @@ +// For now this test only calls update to verify that our registration +// job queueing works properly when called from the worker thread. We should +// test actual update scenarios with a SJS test. +onmessage = function(e) { + self.registration + .update() + .then(function(v) { + return v instanceof ServiceWorkerRegistration ? "FINISH" : "FAIL"; + }) + .catch(function(ex) { + return "FAIL"; + }) + .then(function(result) { + clients.matchAll().then(function(c) { + if (!c.length) { + dump( + "!!!!!!!!!!! WORKER HAS NO CLIENTS TO FINISH TEST !!!!!!!!!!!!\n" + ); + return; + } + + c[0].postMessage(result); + }); + }); +}; diff --git a/dom/serviceworkers/test/worker_updatefoundevent.js b/dom/serviceworkers/test/worker_updatefoundevent.js new file mode 100644 index 0000000000..cb6c6fa862 --- /dev/null +++ b/dom/serviceworkers/test/worker_updatefoundevent.js @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +registration.onupdatefound = function(e) { + clients.matchAll().then(function(clients) { + if (!clients.length) { + // We don't control any clients when the first update event is fired + // because we haven't reached the 'activated' state. + return; + } + + if (registration.scope.match(/updatefoundevent\.html$/)) { + clients[0].postMessage("finish"); + } else { + dump("Scope did not match"); + } + }); +}; diff --git a/dom/serviceworkers/test/worker_updatefoundevent2.js b/dom/serviceworkers/test/worker_updatefoundevent2.js new file mode 100644 index 0000000000..da4c592aad --- /dev/null +++ b/dom/serviceworkers/test/worker_updatefoundevent2.js @@ -0,0 +1 @@ +// Not useful. diff --git a/dom/serviceworkers/test/xslt/test.xml b/dom/serviceworkers/test/xslt/test.xml new file mode 100644 index 0000000000..83c7776339 --- /dev/null +++ b/dom/serviceworkers/test/xslt/test.xml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/xsl" href="test.xsl"?> +<result> + <Title>Example</Title> + <Error>Error</Error> +</result> diff --git a/dom/serviceworkers/test/xslt/xslt.sjs b/dom/serviceworkers/test/xslt/xslt.sjs new file mode 100644 index 0000000000..db681ab500 --- /dev/null +++ b/dom/serviceworkers/test/xslt/xslt.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "application/xslt+xml", false); + response.setHeader("Access-Control-Allow-Origin", "*"); + + var body = request.queryString; + if (!body) { + response.setStatusLine(null, 500, "Invalid querystring"); + return; + } + + response.write(unescape(body)); +} diff --git a/dom/serviceworkers/test/xslt_worker.js b/dom/serviceworkers/test/xslt_worker.js new file mode 100644 index 0000000000..d3c4383f22 --- /dev/null +++ b/dom/serviceworkers/test/xslt_worker.js @@ -0,0 +1,58 @@ +var testType = "synthetic"; + +var xslt = + '<?xml version="1.0"?> ' + + '<xsl:stylesheet version="1.0"' + + ' xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' + + ' <xsl:template match="node()|@*">' + + " <xsl:copy>" + + ' <xsl:apply-templates select="node()|@*"/>' + + " </xsl:copy>" + + " </xsl:template>" + + ' <xsl:template match="Error"/>' + + "</xsl:stylesheet>"; + +onfetch = function(event) { + if (event.request.url.includes("test.xsl")) { + if (testType == "synthetic") { + if (event.request.mode != "cors") { + event.respondWith(Response.error()); + return; + } + + event.respondWith( + Promise.resolve( + new Response(xslt, { + headers: { "Content-Type": "application/xslt+xml" }, + }) + ) + ); + } else if (testType == "cors") { + if (event.request.mode != "cors") { + event.respondWith(Response.error()); + return; + } + + var url = + "http://example.com/tests/dom/serviceworkers/test/xslt/xslt.sjs?" + + escape(xslt); + event.respondWith(fetch(url, { mode: "cors" })); + } else if (testType == "opaque") { + if (event.request.mode != "cors") { + event.respondWith(Response.error()); + return; + } + + var url = + "http://example.com/tests/dom/serviceworkers/test/xslt/xslt.sjs?" + + escape(xslt); + event.respondWith(fetch(url, { mode: "no-cors" })); + } else { + event.respondWith(Response.error()); + } + } +}; + +onmessage = function(event) { + testType = event.data; +}; |