/* -*- 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 #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(RequestRedirect::Follow), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert( nsIHttpChannelInternal::REDIRECT_MODE_ERROR == static_cast(RequestRedirect::Error), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert( nsIHttpChannelInternal::REDIRECT_MODE_MANUAL == static_cast(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(RequestCache::Default), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE == static_cast(RequestCache::No_store), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD == static_cast(RequestCache::Reload), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE == static_cast(RequestCache::No_cache), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE == static_cast(RequestCache::Force_cache), "RequestCache enumeration value should match Necko Cache mode value."); static_assert( nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED == static_cast(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(ServiceWorkerUpdateViaCache::Imports) == nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" " should match ServiceWorkerUpdateViaCache enumeration."); static_assert(static_cast(ServiceWorkerUpdateViaCache::All) == nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" " should match ServiceWorkerUpdateViaCache enumeration."); static_assert(static_cast(ServiceWorkerUpdateViaCache::None) == nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE, "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*" " should match ServiceWorkerUpdateViaCache enumeration."); static StaticRefPtr 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 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(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 mActor; }; constexpr char kFinishShutdownTopic[] = "profile-before-change-qm"; already_AddRefed GetAsyncShutdownBarrier() { AssertIsOnMainThread(); nsCOMPtr svc = services::GetAsyncShutdownService(); MOZ_ASSERT(svc); nsCOMPtr barrier; DebugOnly rv = svc->GetProfileChangeTeardown(getter_AddRefs(barrier)); MOZ_ASSERT(NS_SUCCEEDED(rv)); return barrier.forget(); } Result, nsresult> ScopeToPrincipal( nsIURI* aScopeURI, const OriginAttributes& aOriginAttributes) { MOZ_ASSERT(aScopeURI); nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(aScopeURI, aOriginAttributes); if (NS_WARN_IF(!principal)) { return Err(NS_ERROR_FAILURE); } return principal; } Result, nsresult> ScopeToPrincipal( const nsACString& aScope, const OriginAttributes& aOriginAttributes) { MOZ_ASSERT(nsContentUtils::IsAbsoluteURL(aScope)); nsCOMPtr 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 { using Base = nsTArray; 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 MatchScope(const nsACString& aClientUrl) const { Maybe 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 mInfos; // Maps scopes to job queues. nsRefPtrHashtable mJobQueues; // Map scopes to scheduled update timers. nsInterfaceHashtable 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 shutdownBarrier = GetAsyncShutdownBarrier(); if (shutdownBarrier) { mShutdownBlocker = ServiceWorkerShutdownBlocker::CreateAndRegisterOn( *shutdownBarrier, *this); MOZ_ASSERT(mShutdownBlocker); } MOZ_DIAGNOSTIC_ASSERT(aRegistrar); PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!actorChild)) { MaybeStartShutdown(); return; } PServiceWorkerManagerChild* actor = actorChild->SendPServiceWorkerManagerConstructor(); if (!actor) { MaybeStartShutdown(); return; } mActor = static_cast(actor); // mActor must be set before LoadRegistrations is called because it can purge // service workers if preferences are disabled. nsTArray data; aRegistrar->GetRegistrations(data); LoadRegistrations(data); 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 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 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 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 { const RefPtr self = this; const ServiceWorkerDescriptor& active = aRegistrationInfo->GetActive()->Descriptor(); if (entry) { const RefPtr old = std::move(entry.Data()->mRegistrationInfo); const RefPtr 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 = ClientManager::CreateHandle( aClientInfo, GetMainThreadSerialEventTarget()); const RefPtr promise = aControlClientHandle ? clientHandle->Control(active) : GenericErrorResultPromise::CreateAndResolve(false, __func__); aRegistrationInfo->StartControllingClient(); entry.Insert( MakeUnique(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 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 obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(this, kFinishShutdownTopic, false); return; } MaybeFinishShutdown(); } void ServiceWorkerManager::MaybeFinishShutdown() { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, kFinishShutdownTopic); } if (!mActor) { return; } mActor->ManagerShuttingDown(); RefPtr 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 registerJob = static_cast(aJob); RefPtr reg = registerJob->GetRegistration(); mPromiseHolder.Resolve(reg->Descriptor(), __func__); } virtual void JobDiscarded(ErrorResult& aStatus) override { MOZ_ASSERT(NS_IsMainThread()); mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__); } RefPtr Promise() { MOZ_ASSERT(NS_IsMainThread()); return mPromiseHolder.Ensure(__func__); } private: ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() = default; MozPromiseHolder 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 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 = 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 self(this); const nsCOMPtr principal(aPrincipal); regPromise->Then( GetMainThreadSerialEventTarget(), __func__, [self, outer, principal, scope](const ServiceWorkerRegistrationDescriptor& regDesc) { RefPtr 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 ServiceWorkerManager::Register( const ClientInfo& aClientInfo, const nsACString& aScopeURL, const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) { nsCOMPtr 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 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 principal = principalOrErr.unwrap(); nsAutoCString scopeKey; rv = PrincipalToScopeKey(principal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return ServiceWorkerRegistrationPromise::CreateAndReject( CopyableErrorResult(rv), __func__); } RefPtr queue = GetOrCreateJobQueue(scopeKey, aScopeURL); RefPtr cb = new ServiceWorkerResolveWindowPromiseOnRegisterCallback(); RefPtr job = new ServiceWorkerRegisterJob( principal, aScopeURL, aScriptURL, static_cast(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 mPromise; public: explicit GetRegistrationsRunnable(const ClientInfo& aClientInfo) : Runnable("dom::ServiceWorkerManager::GetRegistrationsRunnable"), mClientInfo(aClientInfo), mPromise(new ServiceWorkerRegistrationListPromise::Private(__func__)) {} RefPtr Promise() const { return mPromise; } NS_IMETHOD Run() override { auto scopeExit = MakeScopeExit( [&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { return NS_OK; } auto principalOrErr = mClientInfo.GetPrincipal(); if (NS_WARN_IF(principalOrErr.isErr())) { return NS_OK; } nsCOMPtr principal = principalOrErr.unwrap(); nsTArray 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 info = data->mInfos.GetWeak(data->mScopeContainer[i]); NS_ConvertUTF8toUTF16 scope(data->mScopeContainer[i]); nsCOMPtr 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 ServiceWorkerManager::GetRegistrations(const ClientInfo& aClientInfo) const { RefPtr 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 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 Promise() const { return mPromise; } NS_IMETHOD Run() override { RefPtr 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 principal = principalOrErr.unwrap(); nsCOMPtr 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 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 ServiceWorkerManager::GetRegistration( const ClientInfo& aClientInfo, const nsACString& aURL) const { MOZ_ASSERT(NS_IsMainThread()); RefPtr 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& 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>&&, 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>& aData) { OriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } nsCOMPtr 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 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 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 reg = GetServiceWorkerRegistrationInfo(aClientInfo); if (reg && reg->GetActive()) { return ServiceWorkerRegistrationPromise::CreateAndResolve(reg->Descriptor(), __func__); } nsCOMPtr target = GetMainThreadSerialEventTarget(); RefPtr handle = ClientManager::CreateHandle(aClientInfo, target); mPendingReadyList.AppendElement(MakeUnique(handle)); RefPtr self(this); handle->OnDetach()->Then(target, __func__, [self = std::move(self), aClientInfo] { self->RemovePendingReadyPromise(aClientInfo); }); return mPendingReadyList.LastElement()->mPromise; } void ServiceWorkerManager::CheckPendingReadyPromises() { nsTArray> pendingReadyList = std::move(mPendingReadyList); for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) { UniquePtr prd(std::move(pendingReadyList[i])); RefPtr 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> pendingReadyList = std::move(mPendingReadyList); for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) { UniquePtr 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 principal = principalOrErr.unwrap(); nsCOMPtr scope; nsresult rv = NS_NewURI(getter_AddRefs(scope), aController.Scope()); NS_ENSURE_SUCCESS_VOID(rv); RefPtr 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 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 registration = GetServiceWorkerRegistrationInfo(principal, scopeURI); if (!registration) { return nullptr; } return registration->GetActive(); } namespace { class UnregisterJobCallback final : public ServiceWorkerJob::Callback { nsCOMPtr 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 unregisterJob = static_cast(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 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 queue = GetOrCreateJobQueue(scopeKey, scope); RefPtr job = new ServiceWorkerUnregisterJob(aPrincipal, scope); if (aCallback) { RefPtr 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 reg = GetRegistration(aWorker->Principal(), aWorker->Scope()); if (!reg) { return; } if (reg->GetActive() != aWorker) { return; } reg->TryToActivateAsync(); } already_AddRefed 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()) .get(); } RefPtr queue = data->mJobQueues.GetOrInsertNew(aScope); return queue.forget(); } /* static */ already_AddRefed ServiceWorkerManager::GetInstance() { if (!gInstance) { RefPtr 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 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& aParamArray, uint32_t aFlags, const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, uint32_t aColumnNumber) { RefPtr 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::PurgeServiceWorker( const ServiceWorkerRegistrationData& aRegistration, nsIPrincipal* aPrincipal) { MOZ_ASSERT(mActor); serviceWorkerScriptCache::PurgeCache(aPrincipal, aRegistration.cacheName()); MaybeSendUnregister(aPrincipal, aRegistration.scope()); } void ServiceWorkerManager::LoadRegistration( const ServiceWorkerRegistrationData& aRegistration) { MOZ_ASSERT(NS_IsMainThread()); auto principalOrErr = PrincipalInfoToPrincipal(aRegistration.principal()); if (NS_WARN_IF(principalOrErr.isErr())) { return; } nsCOMPtr principal = principalOrErr.unwrap(); if (!StaticPrefs::dom_serviceWorkers_enabled()) { // If service workers are disabled, remove the registration from disk // instead of loading. PurgeServiceWorker(aRegistration, principal); return; } // Purge extensions registrations if they are disabled by prefs. if (!StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) { nsCOMPtr 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")) { PurgeServiceWorker(aRegistration, principal); return; } } RefPtr registration = GetRegistration(principal, aRegistration.scope()); if (!registration) { registration = CreateNewRegistration(aRegistration.scope(), principal, static_cast( 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(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& 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 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 ServiceWorkerManager::GetServiceWorkerRegistrationInfo( const ClientInfo& aClientInfo) const { auto principalOrErr = aClientInfo.GetPrincipal(); if (NS_WARN_IF(principalOrErr.isErr())) { return nullptr; } nsCOMPtr principal = principalOrErr.unwrap(); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aClientInfo.URL()); NS_ENSURE_SUCCESS(rv, nullptr); return GetServiceWorkerRegistrationInfo(principal, uri); } already_AddRefed 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 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 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 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 swm = ServiceWorkerManager::GetInstance(); if (!swm || !swm->mRegistrationInfos.Get(aScopeKey, aData)) { return false; } Maybe 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 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 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 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 principal = principalOrErr.unwrap(); nsCOMPtr scope; nsresult rv = NS_NewURI(getter_AddRefs(scope), aServiceWorker.Scope()); NS_ENSURE_SUCCESS(rv, false); RefPtr 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 uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } RefPtr r = GetServiceWorkerRegistrationInfo(aPrincipal, uri); if (!r) { return NS_ERROR_FAILURE; } CopyUTF8toUTF16(r->Scope(), aScope); return NS_OK; } namespace { class ContinueDispatchFetchEventRunnable : public Runnable { RefPtr mServiceWorkerPrivate; nsCOMPtr mChannel; nsCOMPtr 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 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 loadInfo = channel->LoadInfo(); Maybe 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 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 internalChannel; aRv = aChannel->GetChannel(getter_AddRefs(internalChannel)); if (NS_WARN_IF(aRv.Failed())) { return; } nsCOMPtr loadGroup; aRv = internalChannel->GetLoadGroup(getter_AddRefs(loadGroup)); if (NS_WARN_IF(aRv.Failed())) { return; } nsCOMPtr loadInfo = internalChannel->LoadInfo(); RefPtr serviceWorker; if (!nsContentUtils::IsNonSubresourceRequest(internalChannel)) { const Maybe& controller = loadInfo->GetController(); if (NS_WARN_IF(controller.isNothing())) { aRv.Throw(NS_ERROR_FAILURE); return; } RefPtr 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 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 principal = BasePrincipal::CreateContentPrincipal(uri, attrs); RefPtr 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 = 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 clientPrincipal; if (clientPrincipalOrErr.isOk()) { clientPrincipal = clientPrincipalOrErr.unwrap(); } if (!clientPrincipal || !clientPrincipal->Equals(principal)) { UniquePtr reservedClient = loadInfo->TakeReservedClientSource(); nsCOMPtr 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 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 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 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 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 loadInfo = aChannel->LoadInfo(); if (storageAccess != StorageAccess::eAllow) { if (!StaticPrefs::privacy_partition_serviceWorkers()) { return false; } nsCOMPtr 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 = loadInfo->GetReservedClientInfo(); if (clientInfo.isNothing()) { clientInfo = loadInfo->GetInitialClientInfo(); } if (clientInfo.isSome()) { StartControllingClient(clientInfo.ref(), registration); } uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL; nsCOMPtr 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 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 info; data->mInfos.Get(aScope, getter_AddRefs(info)); MOZ_ASSERT(info); RefPtr 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 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 updateJob = static_cast(aJob); RefPtr 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 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 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 queue = GetOrCreateJobQueue(scopeKey, aScope); RefPtr job = new ServiceWorkerUpdateJob( principal, registration->Scope(), newest->ScriptSpec(), registration->GetUpdateViaCache()); if (aCallback) { RefPtr 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 registration = GetRegistration(scopeKey, aScope); if (NS_WARN_IF(!registration)) { ErrorResult error; error.ThrowTypeError(aScope, "uninstalled"); aCallback->UpdateFailed(error); // In case the callback does not consume the exception error.SuppressException(); return; } RefPtr 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 job = new ServiceWorkerUpdateJob( aPrincipal, registration->Scope(), std::move(aNewestWorkerScriptUrl), registration->GetUpdateViaCache()); RefPtr cb = new UpdateJobCallback(aCallback); job->AppendResultCallback(cb); // "Invoke Schedule Job with job." queue->ScheduleJob(job); } RefPtr 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 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 matchingRegistration = GetServiceWorkerRegistrationInfo(aClientInfo); // The registration currently controlling the client RefPtr controllingRegistration; GetClientRegistration(aClientInfo, getter_AddRefs(controllingRegistration)); if (aWorkerRegistration != matchingRegistration || aWorkerRegistration == controllingRegistration) { return GenericErrorResultPromise::CreateAndResolve(true, __func__); } return StartControllingClient(aClientInfo, aWorkerRegistration); } RefPtr ServiceWorkerManager::MaybeClaimClient( const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aServiceWorker) { auto principalOrErr = aServiceWorker.GetPrincipal(); if (NS_WARN_IF(principalOrErr.isErr())) { return GenericErrorResultPromise::CreateAndResolve(false, __func__); } nsCOMPtr principal = principalOrErr.unwrap(); RefPtr 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 activeWorker = aRegistration->GetActive(); MOZ_DIAGNOSTIC_ASSERT(activeWorker); AutoTArray, 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 p = handle->Control(activeWorker->Descriptor()); RefPtr 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 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 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 data; RefPtr 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 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 = 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 self(this); const nsCOMPtr principal(aPrincipal); regPromise->Then( GetMainThreadSerialEventTarget(), __func__, [self, outer, principal, scope](const ServiceWorkerRegistrationDescriptor& regDesc) { RefPtr 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 scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } RefPtr 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 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 scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aExtensionBaseURL); if (NS_FAILED(rv)) { outer->MaybeReject(rv); outer.forget(aPromise); return NS_OK; } nsCOMPtr 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 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 scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } RefPtr info = GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI); if (!info) { return NS_ERROR_FAILURE; } info.forget(aInfo); return NS_OK; } already_AddRefed ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey, const nsACString& aScope) const { RefPtr reg; RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aScopeKey, &data)) { return reg.forget(); } data->mInfos.Get(aScope, getter_AddRefs(reg)); return reg.forget(); } already_AddRefed ServiceWorkerManager::CreateNewRegistration( const nsCString& aScope, nsIPrincipal* aPrincipal, ServiceWorkerUpdateViaCache aUpdateViaCache, IPCNavigationPreloadState aNavigationPreloadState) { #ifdef DEBUG MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); MOZ_ASSERT(NS_SUCCEEDED(rv)); RefPtr tmp = GetRegistration(aPrincipal, aScope); MOZ_ASSERT(!tmp); #endif RefPtr 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 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 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 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> listeners( mListeners.Clone()); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnRegister(aInfo); } } void ServiceWorkerManager::NotifyListenersOnUnregister( nsIServiceWorkerRegistrationInfo* aInfo) { nsTArray> listeners( mListeners.Clone()); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnUnregister(aInfo); } } void ServiceWorkerManager::NotifyListenersOnQuotaUsageCheckFinish( nsIServiceWorkerRegistrationInfo* aRegistration) { nsTArray> listeners( mListeners.Clone()); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnQuotaUsageCheckFinish(aRegistration); } } class UpdateTimerCallback final : public nsITimerCallback, public nsINamed { nsCOMPtr 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 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 callback = new UpdateTimerCallback(aPrincipal, aScope); const uint32_t UPDATE_DELAY_MS = 1000; nsCOMPtr 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 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