diff options
Diffstat (limited to 'dom/serviceworkers/ServiceWorkerManager.cpp')
-rw-r--r-- | dom/serviceworkers/ServiceWorkerManager.cpp | 3364 |
1 files changed, 3364 insertions, 0 deletions
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 |