summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/ServiceWorkerManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/serviceworkers/ServiceWorkerManager.cpp')
-rw-r--r--dom/serviceworkers/ServiceWorkerManager.cpp3380
1 files changed, 3380 insertions, 0 deletions
diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp
new file mode 100644
index 0000000000..b0dcfca893
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -0,0 +1,3380 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerManager.h"
+
+#include <algorithm>
+
+#include "nsCOMPtr.h"
+#include "nsICookieJarSettings.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsINamed.h"
+#include "nsINetworkInterceptController.h"
+#include "nsIMutableArray.h"
+#include "nsIPrincipal.h"
+#include "nsITimer.h"
+#include "nsIUploadChannel2.h"
+#include "nsServiceManagerUtils.h"
+#include "nsDebug.h"
+#include "nsIPermissionManager.h"
+#include "nsXULAppAPI.h"
+
+#include "jsapi.h"
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/LoadContext.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ClientHandle.h"
+#include "mozilla/dom/ClientManager.h"
+#include "mozilla/dom/ClientSource.h"
+#include "mozilla/dom/ConsoleUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ErrorEvent.h"
+#include "mozilla/dom/Headers.h"
+#include "mozilla/dom/InternalHeaders.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/NotificationEvent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/Request.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/dom/SharedWorker.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_extensions.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/Unused.h"
+#include "mozilla/EnumSet.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsIDUtils.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsQueryObject.h"
+#include "nsTArray.h"
+
+#include "ServiceWorker.h"
+#include "ServiceWorkerContainer.h"
+#include "ServiceWorkerInfo.h"
+#include "ServiceWorkerJobQueue.h"
+#include "ServiceWorkerManagerChild.h"
+#include "ServiceWorkerPrivate.h"
+#include "ServiceWorkerRegisterJob.h"
+#include "ServiceWorkerRegistrar.h"
+#include "ServiceWorkerRegistration.h"
+#include "ServiceWorkerScriptCache.h"
+#include "ServiceWorkerShutdownBlocker.h"
+#include "ServiceWorkerEvents.h"
+#include "ServiceWorkerUnregisterJob.h"
+#include "ServiceWorkerUpdateJob.h"
+#include "ServiceWorkerUtils.h"
+#include "ServiceWorkerQuotaUtils.h"
+
+#ifdef PostMessage
+# undef PostMessage
+#endif
+
+mozilla::LazyLogModule sWorkerTelemetryLog("WorkerTelemetry");
+
+#ifdef LOG
+# undef LOG
+#endif
+#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args);
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+namespace mozilla::dom {
+
+// Counts the number of registered ServiceWorkers, and the number that
+// handle Fetch, for reporting in Telemetry
+uint32_t gServiceWorkersRegistered = 0;
+uint32_t gServiceWorkersRegisteredFetch = 0;
+
+static_assert(
+ nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW ==
+ static_cast<uint32_t>(RequestRedirect::Follow),
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+static_assert(
+ nsIHttpChannelInternal::REDIRECT_MODE_ERROR ==
+ static_cast<uint32_t>(RequestRedirect::Error),
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+static_assert(
+ nsIHttpChannelInternal::REDIRECT_MODE_MANUAL ==
+ static_cast<uint32_t>(RequestRedirect::Manual),
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+static_assert(
+ 3 == RequestRedirectValues::Count,
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT ==
+ static_cast<uint32_t>(RequestCache::Default),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE ==
+ static_cast<uint32_t>(RequestCache::No_store),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD ==
+ static_cast<uint32_t>(RequestCache::Reload),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE ==
+ static_cast<uint32_t>(RequestCache::No_cache),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE ==
+ static_cast<uint32_t>(RequestCache::Force_cache),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED ==
+ static_cast<uint32_t>(RequestCache::Only_if_cached),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ 6 == RequestCacheValues::Count,
+ "RequestCache enumeration value should match Necko Cache mode value.");
+
+static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::Imports) ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
+ " should match ServiceWorkerUpdateViaCache enumeration.");
+static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::All) ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL,
+ "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
+ " should match ServiceWorkerUpdateViaCache enumeration.");
+static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None) ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE,
+ "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
+ " should match ServiceWorkerUpdateViaCache enumeration.");
+
+static StaticRefPtr<ServiceWorkerManager> gInstance;
+
+namespace {
+
+nsresult PopulateRegistrationData(
+ nsIPrincipal* aPrincipal,
+ const ServiceWorkerRegistrationInfo* aRegistration,
+ ServiceWorkerRegistrationData& aData) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aRegistration);
+
+ if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aData.scope() = aRegistration->Scope();
+
+ // TODO: When bug 1426401 is implemented we will need to handle more
+ // than just the active worker here.
+ RefPtr<ServiceWorkerInfo> active = aRegistration->GetActive();
+ MOZ_ASSERT(active);
+ if (NS_WARN_IF(!active)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aData.currentWorkerURL() = active->ScriptSpec();
+ aData.cacheName() = active->CacheName();
+ aData.currentWorkerHandlesFetch() = active->HandlesFetch();
+
+ aData.currentWorkerInstalledTime() = active->GetInstalledTime();
+ aData.currentWorkerActivatedTime() = active->GetActivatedTime();
+
+ aData.updateViaCache() =
+ static_cast<uint32_t>(aRegistration->GetUpdateViaCache());
+
+ aData.lastUpdateTime() = aRegistration->GetLastUpdateTime();
+
+ aData.navigationPreloadState() = aRegistration->GetNavigationPreloadState();
+
+ MOZ_ASSERT(ServiceWorkerRegistrationDataIsValid(aData));
+
+ return NS_OK;
+}
+
+class TeardownRunnable final : public Runnable {
+ public:
+ explicit TeardownRunnable(ServiceWorkerManagerChild* aActor)
+ : Runnable("dom::ServiceWorkerManager::TeardownRunnable"),
+ mActor(aActor) {
+ MOZ_ASSERT(mActor);
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(mActor);
+ PServiceWorkerManagerChild::Send__delete__(mActor);
+ return NS_OK;
+ }
+
+ private:
+ ~TeardownRunnable() = default;
+
+ RefPtr<ServiceWorkerManagerChild> mActor;
+};
+
+constexpr char kFinishShutdownTopic[] = "profile-before-change-qm";
+
+already_AddRefed<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() {
+ AssertIsOnMainThread();
+
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ MOZ_ASSERT(svc);
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier;
+ DebugOnly<nsresult> rv =
+ svc->GetProfileChangeTeardown(getter_AddRefs(barrier));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ return barrier.forget();
+}
+
+Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal(
+ nsIURI* aScopeURI, const OriginAttributes& aOriginAttributes) {
+ MOZ_ASSERT(aScopeURI);
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(aScopeURI, aOriginAttributes);
+ if (NS_WARN_IF(!principal)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return principal;
+}
+
+Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal(
+ const nsACString& aScope, const OriginAttributes& aOriginAttributes) {
+ MOZ_ASSERT(nsContentUtils::IsAbsoluteURL(aScope));
+
+ nsCOMPtr<nsIURI> scopeURI;
+ MOZ_TRY(NS_NewURI(getter_AddRefs(scopeURI), aScope));
+
+ return ScopeToPrincipal(scopeURI, aOriginAttributes);
+}
+
+} // namespace
+
+struct ServiceWorkerManager::RegistrationDataPerPrincipal final {
+ // Implements a container of keys for the "scope to registration map":
+ // https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map
+ //
+ // where each key is an absolute URL.
+ //
+ // The properties of this map that the spec uses are
+ // 1) insertion,
+ // 2) removal,
+ // 3) iteration of scopes in FIFO order (excluding removed scopes),
+ // 4) and finding, for a given path, the maximal length scope which is a
+ // prefix of the path.
+ //
+ // Additionally, because this is a container of keys for a map, there
+ // shouldn't be duplicate scopes.
+ //
+ // The current implementation uses a dynamic array as the underlying
+ // container, which is not optimal for unbounded container sizes (all
+ // supported operations are in linear time) but may be superior for small
+ // container sizes.
+ //
+ // If this is proven to be too slow, the underlying storage should be replaced
+ // with a linked list of scopes in combination with an ordered map that maps
+ // scopes to linked list elements/iterators. This would reduce all of the
+ // above operations besides iteration (necessarily linear) to logarithmic
+ // time.
+ class ScopeContainer final : private nsTArray<nsCString> {
+ using Base = nsTArray<nsCString>;
+
+ public:
+ using Base::Contains;
+ using Base::IsEmpty;
+ using Base::Length;
+
+ // No using-declaration to avoid importing the non-const overload.
+ decltype(auto) operator[](Base::index_type aIndex) const {
+ return Base::operator[](aIndex);
+ }
+
+ void InsertScope(const nsACString& aScope) {
+ MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::IsAbsoluteURL(aScope));
+
+ if (Contains(aScope)) {
+ return;
+ }
+
+ AppendElement(aScope);
+ }
+
+ void RemoveScope(const nsACString& aScope) {
+ MOZ_ALWAYS_TRUE(RemoveElement(aScope));
+ }
+
+ // Implements most of "Match Service Worker Registration":
+ // https://w3c.github.io/ServiceWorker/#scope-match-algorithm
+ Maybe<nsCString> MatchScope(const nsACString& aClientUrl) const {
+ Maybe<nsCString> match;
+
+ for (const nsCString& scope : *this) {
+ if (StringBeginsWith(aClientUrl, scope)) {
+ if (!match || scope.Length() > match->Length()) {
+ match = Some(scope);
+ }
+ }
+ }
+
+ // Step 7.2:
+ // "Assert: matchingScope’s origin and clientURL’s origin are same
+ // origin."
+ MOZ_DIAGNOSTIC_ASSERT_IF(match, IsSameOrigin(*match, aClientUrl));
+
+ return match;
+ }
+
+ private:
+ bool IsSameOrigin(const nsACString& aMatchingScope,
+ const nsACString& aClientUrl) const {
+ auto parseResult = ScopeToPrincipal(aMatchingScope, OriginAttributes());
+
+ if (NS_WARN_IF(parseResult.isErr())) {
+ return false;
+ }
+
+ auto scopePrincipal = parseResult.unwrap();
+
+ parseResult = ScopeToPrincipal(aClientUrl, OriginAttributes());
+
+ if (NS_WARN_IF(parseResult.isErr())) {
+ return false;
+ }
+
+ auto clientPrincipal = parseResult.unwrap();
+
+ bool equals = false;
+
+ if (NS_WARN_IF(
+ NS_FAILED(scopePrincipal->Equals(clientPrincipal, &equals)))) {
+ return false;
+ }
+
+ return equals;
+ }
+ };
+
+ ScopeContainer mScopeContainer;
+
+ // Scope to registration.
+ // The scope should be a fully qualified valid URL.
+ nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo> mInfos;
+
+ // Maps scopes to job queues.
+ nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerJobQueue> mJobQueues;
+
+ // Map scopes to scheduled update timers.
+ nsInterfaceHashtable<nsCStringHashKey, nsITimer> mUpdateTimers;
+
+ // The number of times we have done a quota usage check for this origin for
+ // mitigation purposes. See the docs on nsIServiceWorkerRegistrationInfo,
+ // where this value is exposed.
+ int32_t mQuotaUsageCheckCount = 0;
+};
+
+//////////////////////////
+// ServiceWorkerManager //
+//////////////////////////
+
+NS_IMPL_ADDREF(ServiceWorkerManager)
+NS_IMPL_RELEASE(ServiceWorkerManager)
+
+NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager)
+ NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager)
+NS_INTERFACE_MAP_END
+
+ServiceWorkerManager::ServiceWorkerManager()
+ : mActor(nullptr), mShuttingDown(false) {}
+
+ServiceWorkerManager::~ServiceWorkerManager() {
+ // The map will assert if it is not empty when destroyed.
+ mRegistrationInfos.Clear();
+
+ // This can happen if the browser is started up in ProfileManager mode, in
+ // which case XPCOM will startup and shutdown, but there won't be any
+ // profile-* topic notifications. The shutdown blocker expects to be in a
+ // NotAcceptingPromises state when it's destroyed, and this transition
+ // normally happens in the "profile-change-teardown" notification callback
+ // (which won't be called in ProfileManager mode).
+ if (!mShuttingDown && mShutdownBlocker) {
+ mShutdownBlocker->StopAcceptingPromises();
+ }
+}
+
+void ServiceWorkerManager::BlockShutdownOn(GenericNonExclusivePromise* aPromise,
+ uint32_t aShutdownStateId) {
+ AssertIsOnMainThread();
+
+ MOZ_ASSERT(mShutdownBlocker);
+ MOZ_ASSERT(aPromise);
+
+ mShutdownBlocker->WaitOnPromise(aPromise, aShutdownStateId);
+}
+
+void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) {
+ // ServiceWorkers now only support parent intercept. In parent intercept
+ // mode, only the parent process ServiceWorkerManager has any state or does
+ // anything.
+ //
+ // It is our goal to completely eliminate support for content process
+ // ServiceWorkerManager instances and make getting a SWM instance trigger a
+ // fatal assertion. But until we've reached that point, we make
+ // initialization a no-op so that content process ServiceWorkerManager
+ // instances will simply have no state and no registrations.
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier = GetAsyncShutdownBarrier();
+
+ if (shutdownBarrier) {
+ mShutdownBlocker = ServiceWorkerShutdownBlocker::CreateAndRegisterOn(
+ *shutdownBarrier, *this);
+ MOZ_ASSERT(mShutdownBlocker);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aRegistrar);
+
+ PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!actorChild)) {
+ MaybeStartShutdown();
+ return;
+ }
+
+ PServiceWorkerManagerChild* actor =
+ actorChild->SendPServiceWorkerManagerConstructor();
+ if (!actor) {
+ MaybeStartShutdown();
+ return;
+ }
+
+ mActor = static_cast<ServiceWorkerManagerChild*>(actor);
+
+ // mActor must be set before LoadRegistrations is called because it can purge
+ // service workers if preferences are disabled.
+ nsTArray<ServiceWorkerRegistrationData> 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<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "ServiceWorkerTelemetryRunnable", [aNumber, aFetch, repeats]() {
+ LOG(("ServiceWorkers running: %u samples of %u/%u", repeats, aNumber,
+ aFetch));
+ // Don't allocate infinitely huge arrays if someone visits a SW site
+ // after a few months running. 1 month is about 500K repeats @ 5s
+ // sampling
+ uint32_t num_repeats = std::min(repeats, 1000000U); // 4MB max
+ nsTArray<uint32_t> values;
+
+ uint32_t* array = values.AppendElements(num_repeats);
+ for (uint32_t i = 0; i < num_repeats; i++) {
+ array[i] = aNumber;
+ }
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "All"_ns,
+ values);
+
+ for (uint32_t i = 0; i < num_repeats; i++) {
+ array[i] = aFetch;
+ }
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "Fetch"_ns,
+ values);
+ });
+ NS_DispatchBackgroundTask(runnable.forget(), nsIEventTarget::DISPATCH_NORMAL);
+}
+
+RefPtr<GenericErrorResultPromise> ServiceWorkerManager::StartControllingClient(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo* aRegistrationInfo,
+ bool aControlClientHandle) {
+ MOZ_DIAGNOSTIC_ASSERT(aRegistrationInfo->GetActive());
+
+ // XXX We can't use a generic lambda (accepting auto&& entry) like elsewhere
+ // with WithEntryHandle, since we get linker errors then using clang+lld. This
+ // might be a toolchain issue?
+ return mControlledClients.WithEntryHandle(
+ aClientInfo.Id(),
+ [&](decltype(mControlledClients)::EntryHandle&& entry)
+ -> RefPtr<GenericErrorResultPromise> {
+ const RefPtr<ServiceWorkerManager> self = this;
+
+ const ServiceWorkerDescriptor& active =
+ aRegistrationInfo->GetActive()->Descriptor();
+
+ if (entry) {
+ const RefPtr<ServiceWorkerRegistrationInfo> old =
+ std::move(entry.Data()->mRegistrationInfo);
+
+ const RefPtr<GenericErrorResultPromise> promise =
+ aControlClientHandle
+ ? entry.Data()->mClientHandle->Control(active)
+ : GenericErrorResultPromise::CreateAndResolve(false,
+ __func__);
+
+ entry.Data()->mRegistrationInfo = aRegistrationInfo;
+
+ if (old != aRegistrationInfo) {
+ StopControllingRegistration(old);
+ aRegistrationInfo->StartControllingClient();
+ }
+
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS,
+ 1);
+
+ // Always check to see if we failed to actually control the client. In
+ // that case remove the client from our list of controlled clients.
+ return promise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](bool) {
+ // do nothing on success
+ return GenericErrorResultPromise::CreateAndResolve(true,
+ __func__);
+ },
+ [self, aClientInfo](const CopyableErrorResult& aRv) {
+ // failed to control, forget about this client
+ self->StopControllingClient(aClientInfo);
+ return GenericErrorResultPromise::CreateAndReject(aRv,
+ __func__);
+ });
+ }
+
+ RefPtr<ClientHandle> clientHandle = ClientManager::CreateHandle(
+ aClientInfo, GetMainThreadSerialEventTarget());
+
+ const RefPtr<GenericErrorResultPromise> promise =
+ aControlClientHandle
+ ? clientHandle->Control(active)
+ : GenericErrorResultPromise::CreateAndResolve(false, __func__);
+
+ aRegistrationInfo->StartControllingClient();
+
+ entry.Insert(
+ MakeUnique<ControlledClientData>(clientHandle, aRegistrationInfo));
+
+ clientHandle->OnDetach()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, aClientInfo] { self->StopControllingClient(aClientInfo); });
+
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS,
+ 1);
+
+ // Always check to see if we failed to actually control the client. In
+ // that case removed the client from our list of controlled clients.
+ return promise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](bool) {
+ // do nothing on success
+ return GenericErrorResultPromise::CreateAndResolve(true,
+ __func__);
+ },
+ [self, aClientInfo](const CopyableErrorResult& aRv) {
+ // failed to control, forget about this client
+ self->StopControllingClient(aClientInfo);
+ return GenericErrorResultPromise::CreateAndReject(aRv, __func__);
+ });
+ });
+}
+
+void ServiceWorkerManager::StopControllingClient(
+ const ClientInfo& aClientInfo) {
+ auto entry = mControlledClients.Lookup(aClientInfo.Id());
+ if (!entry) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ std::move(entry.Data()->mRegistrationInfo);
+
+ entry.Remove();
+
+ StopControllingRegistration(reg);
+}
+
+void ServiceWorkerManager::MaybeStartShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ mShuttingDown = true;
+
+ for (const auto& dataPtr : mRegistrationInfos.Values()) {
+ for (const auto& timerEntry : dataPtr->mUpdateTimers.Values()) {
+ timerEntry->Cancel();
+ }
+ dataPtr->mUpdateTimers.Clear();
+
+ for (const auto& queueEntry : dataPtr->mJobQueues.Values()) {
+ queueEntry->CancelAll();
+ }
+ dataPtr->mJobQueues.Clear();
+
+ for (const auto& registrationEntry : dataPtr->mInfos.Values()) {
+ registrationEntry->ShutdownWorkers();
+ }
+
+ // ServiceWorkerCleanup may try to unregister registrations, so don't clear
+ // mInfos.
+ }
+
+ for (const auto& entry : mControlledClients.Values()) {
+ entry->mRegistrationInfo->ShutdownWorkers();
+ }
+
+ for (auto iter = mOrphanedRegistrations.iter(); !iter.done(); iter.next()) {
+ iter.get()->ShutdownWorkers();
+ }
+
+ if (mShutdownBlocker) {
+ mShutdownBlocker->StopAcceptingPromises();
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, kFinishShutdownTopic, false);
+ return;
+ }
+
+ MaybeFinishShutdown();
+}
+
+void ServiceWorkerManager::MaybeFinishShutdown() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, kFinishShutdownTopic);
+ }
+
+ if (!mActor) {
+ return;
+ }
+
+ mActor->ManagerShuttingDown();
+
+ RefPtr<TeardownRunnable> runnable = new TeardownRunnable(mActor);
+ nsresult rv = NS_DispatchToMainThread(runnable);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ mActor = nullptr;
+
+ // This also submits final telemetry
+ ServiceWorkerPrivate::RunningShutdown();
+}
+
+class ServiceWorkerResolveWindowPromiseOnRegisterCallback final
+ : public ServiceWorkerJob::Callback {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(
+ ServiceWorkerResolveWindowPromiseOnRegisterCallback, override)
+
+ virtual void JobFinished(ServiceWorkerJob* aJob,
+ ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+
+ if (aStatus.Failed()) {
+ mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__);
+ return;
+ }
+
+ MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register);
+ RefPtr<ServiceWorkerRegisterJob> registerJob =
+ static_cast<ServiceWorkerRegisterJob*>(aJob);
+ RefPtr<ServiceWorkerRegistrationInfo> reg = registerJob->GetRegistration();
+
+ mPromiseHolder.Resolve(reg->Descriptor(), __func__);
+ }
+
+ virtual void JobDiscarded(ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__);
+ }
+
+ RefPtr<ServiceWorkerRegistrationPromise> Promise() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mPromiseHolder.Ensure(__func__);
+ }
+
+ private:
+ ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() = default;
+
+ MozPromiseHolder<ServiceWorkerRegistrationPromise> mPromiseHolder;
+};
+
+NS_IMETHODIMP
+ServiceWorkerManager::RegisterForTest(nsIPrincipal* aPrincipal,
+ const nsAString& aScopeURL,
+ const nsAString& aScriptURL,
+ JSContext* aCx,
+ mozilla::dom::Promise** aPromise) {
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<Promise> outer = Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ if (!StaticPrefs::dom_serviceWorkers_testing_enabled()) {
+ outer->MaybeRejectWithAbortError(
+ "registerForTest only allowed when dom.serviceWorkers.testing.enabled "
+ "is true");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ if (aPrincipal == nullptr) {
+ outer->MaybeRejectWithAbortError("Missing principal");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ if (aScriptURL.IsEmpty()) {
+ outer->MaybeRejectWithAbortError("Missing script url");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ if (aScopeURL.IsEmpty()) {
+ outer->MaybeRejectWithAbortError("Missing scope url");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ // The ClientType isn't really used here, but ClientType::Window
+ // is the least bad choice since this is happening on the main thread.
+ Maybe<ClientInfo> clientInfo =
+ dom::ClientManager::CreateInfo(ClientType::Window, aPrincipal);
+
+ if (!clientInfo.isSome()) {
+ outer->MaybeRejectWithUnknownError("Error creating clientInfo");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ auto scope = NS_ConvertUTF16toUTF8(aScopeURL);
+ auto scriptURL = NS_ConvertUTF16toUTF8(aScriptURL);
+
+ auto regPromise = Register(clientInfo.ref(), scope, scriptURL,
+ dom::ServiceWorkerUpdateViaCache::Imports);
+ const RefPtr<ServiceWorkerManager> self(this);
+ const nsCOMPtr<nsIPrincipal> principal(aPrincipal);
+ regPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, outer, principal,
+ scope](const ServiceWorkerRegistrationDescriptor& regDesc) {
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ self->GetRegistration(principal, NS_ConvertUTF16toUTF8(scope));
+ if (registration) {
+ outer->MaybeResolve(registration);
+ } else {
+ outer->MaybeRejectWithUnknownError(
+ "Failed to retrieve ServiceWorkerRegistrationInfo");
+ }
+ },
+ [outer](const mozilla::CopyableErrorResult& err) {
+ CopyableErrorResult result(err);
+ outer->MaybeReject(std::move(result));
+ });
+
+ outer.forget(aPromise);
+
+ return NS_OK;
+}
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::Register(
+ const ClientInfo& aClientInfo, const nsACString& aScopeURL,
+ const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) {
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScopeURL);
+ if (NS_FAILED(rv)) {
+ // Odd, since it was serialiazed from an nsIURI.
+ CopyableErrorResult err;
+ err.ThrowInvalidStateError("Scope URL cannot be parsed");
+ return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__);
+ }
+
+ nsCOMPtr<nsIURI> scriptURI;
+ rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL);
+ if (NS_FAILED(rv)) {
+ // Odd, since it was serialiazed from an nsIURI.
+ CopyableErrorResult err;
+ err.ThrowInvalidStateError("Script URL cannot be parsed");
+ return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__);
+ }
+
+ IgnoredErrorResult err;
+ ServiceWorkerScopeAndScriptAreValid(aClientInfo, scopeURI, scriptURI, err);
+ if (err.Failed()) {
+ return ServiceWorkerRegistrationPromise::CreateAndReject(
+ CopyableErrorResult(std::move(err)), __func__);
+ }
+
+ // If the previous validation step passed then we must have a principal.
+ auto principalOrErr = aClientInfo.GetPrincipal();
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return ServiceWorkerRegistrationPromise::CreateAndReject(
+ CopyableErrorResult(principalOrErr.unwrapErr()), __func__);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsAutoCString scopeKey;
+ rv = PrincipalToScopeKey(principal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return ServiceWorkerRegistrationPromise::CreateAndReject(
+ CopyableErrorResult(rv), __func__);
+ }
+
+ RefPtr<ServiceWorkerJobQueue> queue =
+ GetOrCreateJobQueue(scopeKey, aScopeURL);
+
+ RefPtr<ServiceWorkerResolveWindowPromiseOnRegisterCallback> cb =
+ new ServiceWorkerResolveWindowPromiseOnRegisterCallback();
+
+ RefPtr<ServiceWorkerRegisterJob> job = new ServiceWorkerRegisterJob(
+ principal, aScopeURL, aScriptURL,
+ static_cast<ServiceWorkerUpdateViaCache>(aUpdateViaCache));
+
+ job->AppendResultCallback(cb);
+ queue->ScheduleJob(job);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return cb->Promise();
+}
+
+/*
+ * Implements the async aspects of the getRegistrations algorithm.
+ */
+class GetRegistrationsRunnable final : public Runnable {
+ const ClientInfo mClientInfo;
+ RefPtr<ServiceWorkerRegistrationListPromise::Private> mPromise;
+
+ public:
+ explicit GetRegistrationsRunnable(const ClientInfo& aClientInfo)
+ : Runnable("dom::ServiceWorkerManager::GetRegistrationsRunnable"),
+ mClientInfo(aClientInfo),
+ mPromise(new ServiceWorkerRegistrationListPromise::Private(__func__)) {}
+
+ RefPtr<ServiceWorkerRegistrationListPromise> Promise() const {
+ return mPromise;
+ }
+
+ NS_IMETHOD
+ Run() override {
+ auto scopeExit = MakeScopeExit(
+ [&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return NS_OK;
+ }
+
+ auto principalOrErr = mClientInfo.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ nsTArray<ServiceWorkerRegistrationDescriptor> array;
+
+ if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsContentPrincipal())) {
+ return NS_OK;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = swm->PrincipalToScopeKey(principal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ ServiceWorkerManager::RegistrationDataPerPrincipal* data;
+ if (!swm->mRegistrationInfos.Get(scopeKey, &data)) {
+ scopeExit.release();
+ mPromise->Resolve(array, __func__);
+ return NS_OK;
+ }
+
+ for (uint32_t i = 0; i < data->mScopeContainer.Length(); ++i) {
+ RefPtr<ServiceWorkerRegistrationInfo> info =
+ data->mInfos.GetWeak(data->mScopeContainer[i]);
+
+ NS_ConvertUTF8toUTF16 scope(data->mScopeContainer[i]);
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ break;
+ }
+
+ // Unfortunately we don't seem to have an obvious window id here; in
+ // particular ClientInfo does not have one, and neither do service worker
+ // registrations, as far as I can tell.
+ rv = principal->CheckMayLoadWithReporting(
+ scopeURI, false /* allowIfInheritsPrincipal */,
+ 0 /* innerWindowID */);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ array.AppendElement(info->Descriptor());
+ }
+
+ scopeExit.release();
+ mPromise->Resolve(array, __func__);
+
+ return NS_OK;
+ }
+};
+
+RefPtr<ServiceWorkerRegistrationListPromise>
+ServiceWorkerManager::GetRegistrations(const ClientInfo& aClientInfo) const {
+ RefPtr<GetRegistrationsRunnable> runnable =
+ new GetRegistrationsRunnable(aClientInfo);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable));
+ return runnable->Promise();
+}
+
+/*
+ * Implements the async aspects of the getRegistration algorithm.
+ */
+class GetRegistrationRunnable final : public Runnable {
+ const ClientInfo mClientInfo;
+ RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise;
+ nsCString mURL;
+
+ public:
+ GetRegistrationRunnable(const ClientInfo& aClientInfo, const nsACString& aURL)
+ : Runnable("dom::ServiceWorkerManager::GetRegistrationRunnable"),
+ mClientInfo(aClientInfo),
+ mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)),
+ mURL(aURL) {}
+
+ RefPtr<ServiceWorkerRegistrationPromise> Promise() const { return mPromise; }
+
+ NS_IMETHOD
+ Run() override {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__);
+ return NS_OK;
+ }
+
+ auto principalOrErr = mClientInfo.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mPromise->Reject(rv, __func__);
+ return NS_OK;
+ }
+
+ // Unfortunately we don't seem to have an obvious window id here; in
+ // particular ClientInfo does not have one, and neither do service worker
+ // registrations, as far as I can tell.
+ rv = principal->CheckMayLoadWithReporting(
+ uri, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */);
+ if (NS_FAILED(rv)) {
+ mPromise->Reject(NS_ERROR_DOM_SECURITY_ERR, __func__);
+ return NS_OK;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetServiceWorkerRegistrationInfo(principal, uri);
+
+ if (!registration) {
+ // Reject with NS_OK means "not found".
+ mPromise->Reject(NS_OK, __func__);
+ return NS_OK;
+ }
+
+ mPromise->Resolve(registration->Descriptor(), __func__);
+
+ return NS_OK;
+ }
+};
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::GetRegistration(
+ const ClientInfo& aClientInfo, const nsACString& aURL) const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<GetRegistrationRunnable> runnable =
+ new GetRegistrationRunnable(aClientInfo, aURL);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable));
+
+ return runnable->Promise();
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes,
+ const nsACString& aScope,
+ const nsTArray<uint8_t>& aDataBytes,
+ uint8_t optional_argc) {
+ if (optional_argc == 1) {
+ // This does one copy here (while constructing the Maybe) and another when
+ // we end up copying into the SendPushEventRunnable. We could fix that to
+ // only do one copy by making things between here and there take
+ // Maybe<nsTArray<uint8_t>>&&, but then we'd need to copy before we know
+ // whether we really need to in PushMessageDispatcher::NotifyWorkers. Since
+ // in practice this only affects JS callers that pass data, and we don't
+ // have any right now, let's not worry about it.
+ return SendPushEvent(aOriginAttributes, aScope, u""_ns,
+ Some(aDataBytes.Clone()));
+ }
+ MOZ_ASSERT(optional_argc == 0);
+ return SendPushEvent(aOriginAttributes, aScope, u""_ns, Nothing());
+}
+
+nsresult ServiceWorkerManager::SendPushEvent(
+ const nsACString& aOriginAttributes, const nsACString& aScope,
+ const nsAString& aMessageId, const Maybe<nsTArray<uint8_t>>& aData) {
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY_VAR(principal, ScopeToPrincipal(aScope, attrs));
+
+ // The registration handling a push notification must have an exact scope
+ // match. This will try to find an exact match, unlike how fetch may find the
+ // registration with the longest scope that's a prefix of the fetched URL.
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(principal, aScope);
+ if (NS_WARN_IF(!registration)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(registration->Scope().Equals(aScope));
+
+ ServiceWorkerInfo* serviceWorker = registration->GetActive();
+ if (NS_WARN_IF(!serviceWorker)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData,
+ registration);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendPushSubscriptionChangeEvent(
+ const nsACString& aOriginAttributes, const nsACString& aScope) {
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope);
+ if (!info) {
+ return NS_ERROR_FAILURE;
+ }
+ return info->WorkerPrivate()->SendPushSubscriptionChangeEvent();
+}
+
+nsresult ServiceWorkerManager::SendNotificationEvent(
+ const nsAString& aEventName, const nsACString& aOriginSuffix,
+ const nsACString& aScope, const nsAString& aID, const nsAString& aTitle,
+ const nsAString& aDir, const nsAString& aLang, const nsAString& aBody,
+ const nsAString& aTag, const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior) {
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(aOriginSuffix)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope);
+ if (!info) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate();
+ return workerPrivate->SendNotificationEvent(
+ aEventName, aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData,
+ aBehavior, NS_ConvertUTF8toUTF16(aScope));
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendNotificationClickEvent(
+ const nsACString& aOriginSuffix, const nsACString& aScope,
+ const nsAString& aID, const nsAString& aTitle, const nsAString& aDir,
+ const nsAString& aLang, const nsAString& aBody, const nsAString& aTag,
+ const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior) {
+ return SendNotificationEvent(nsLiteralString(NOTIFICATION_CLICK_EVENT_NAME),
+ aOriginSuffix, aScope, aID, aTitle, aDir, aLang,
+ aBody, aTag, aIcon, aData, aBehavior);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendNotificationCloseEvent(
+ const nsACString& aOriginSuffix, const nsACString& aScope,
+ const nsAString& aID, const nsAString& aTitle, const nsAString& aDir,
+ const nsAString& aLang, const nsAString& aBody, const nsAString& aTag,
+ const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior) {
+ return SendNotificationEvent(nsLiteralString(NOTIFICATION_CLOSE_EVENT_NAME),
+ aOriginSuffix, aScope, aID, aTitle, aDir, aLang,
+ aBody, aTag, aIcon, aData, aBehavior);
+}
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::WhenReady(
+ const ClientInfo& aClientInfo) {
+ AssertIsOnMainThread();
+
+ for (auto& prd : mPendingReadyList) {
+ if (prd->mClientHandle->Info().Id() == aClientInfo.Id() &&
+ prd->mClientHandle->Info().PrincipalInfo() ==
+ aClientInfo.PrincipalInfo()) {
+ return prd->mPromise;
+ }
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ GetServiceWorkerRegistrationInfo(aClientInfo);
+ if (reg && reg->GetActive()) {
+ return ServiceWorkerRegistrationPromise::CreateAndResolve(reg->Descriptor(),
+ __func__);
+ }
+
+ nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget();
+
+ RefPtr<ClientHandle> handle =
+ ClientManager::CreateHandle(aClientInfo, target);
+ mPendingReadyList.AppendElement(MakeUnique<PendingReadyData>(handle));
+
+ RefPtr<ServiceWorkerManager> self(this);
+ handle->OnDetach()->Then(target, __func__,
+ [self = std::move(self), aClientInfo] {
+ self->RemovePendingReadyPromise(aClientInfo);
+ });
+
+ return mPendingReadyList.LastElement()->mPromise;
+}
+
+void ServiceWorkerManager::CheckPendingReadyPromises() {
+ nsTArray<UniquePtr<PendingReadyData>> pendingReadyList =
+ std::move(mPendingReadyList);
+ for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) {
+ UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i]));
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ GetServiceWorkerRegistrationInfo(prd->mClientHandle->Info());
+
+ if (reg && reg->GetActive()) {
+ prd->mPromise->Resolve(reg->Descriptor(), __func__);
+ } else {
+ mPendingReadyList.AppendElement(std::move(prd));
+ }
+ }
+}
+
+void ServiceWorkerManager::RemovePendingReadyPromise(
+ const ClientInfo& aClientInfo) {
+ nsTArray<UniquePtr<PendingReadyData>> pendingReadyList =
+ std::move(mPendingReadyList);
+ for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) {
+ UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i]));
+
+ if (prd->mClientHandle->Info().Id() == aClientInfo.Id() &&
+ prd->mClientHandle->Info().PrincipalInfo() ==
+ aClientInfo.PrincipalInfo()) {
+ prd->mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__);
+ } else {
+ mPendingReadyList.AppendElement(std::move(prd));
+ }
+ }
+}
+
+void ServiceWorkerManager::NoteInheritedController(
+ const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aController) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto principalOrErr = PrincipalInfoToPrincipal(aController.PrincipalInfo());
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsCOMPtr<nsIURI> scope;
+ nsresult rv = NS_NewURI(getter_AddRefs(scope), aController.Scope());
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, scope);
+ NS_ENSURE_TRUE_VOID(registration);
+ NS_ENSURE_TRUE_VOID(registration->GetActive());
+
+ StartControllingClient(aClientInfo, registration,
+ false /* aControlClientHandle */);
+}
+
+ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForScope(
+ const OriginAttributes& aOriginAttributes, const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ auto result = ScopeToPrincipal(scopeURI, aOriginAttributes);
+ if (NS_WARN_IF(result.isErr())) {
+ return nullptr;
+ }
+
+ auto principal = result.unwrap();
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, scopeURI);
+ if (!registration) {
+ return nullptr;
+ }
+
+ return registration->GetActive();
+}
+
+namespace {
+
+class UnregisterJobCallback final : public ServiceWorkerJob::Callback {
+ nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback;
+
+ ~UnregisterJobCallback() { MOZ_ASSERT(!mCallback); }
+
+ public:
+ explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback)
+ : mCallback(aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+ }
+
+ void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+ MOZ_ASSERT(mCallback);
+
+ auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; });
+
+ if (aStatus.Failed()) {
+ mCallback->UnregisterFailed();
+ return;
+ }
+
+ MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister);
+ RefPtr<ServiceWorkerUnregisterJob> unregisterJob =
+ static_cast<ServiceWorkerUnregisterJob*>(aJob);
+ mCallback->UnregisterSucceeded(unregisterJob->GetResult());
+ }
+
+ void JobDiscarded(ErrorResult&) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+
+ mCallback->UnregisterFailed();
+ mCallback = nullptr;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback, override)
+};
+
+} // anonymous namespace
+
+NS_IMETHODIMP
+ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal,
+ nsIServiceWorkerUnregisterCallback* aCallback,
+ const nsAString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aPrincipal) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+
+// This is not accessible by content, and callers should always ensure scope is
+// a correct URI, so this is wrapped in DEBUG
+#ifdef DEBUG
+ nsCOMPtr<nsIURI> scopeURI;
+ rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+#endif
+
+ nsAutoCString scopeKey;
+ rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ NS_ConvertUTF16toUTF8 scope(aScope);
+ RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope);
+
+ RefPtr<ServiceWorkerUnregisterJob> job =
+ new ServiceWorkerUnregisterJob(aPrincipal, scope);
+
+ if (aCallback) {
+ RefPtr<UnregisterJobCallback> cb = new UnregisterJobCallback(aCallback);
+ job->AppendResultCallback(cb);
+ }
+
+ queue->ScheduleJob(job);
+ return NS_OK;
+}
+
+void ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(aWorker);
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ GetRegistration(aWorker->Principal(), aWorker->Scope());
+ if (!reg) {
+ return;
+ }
+
+ if (reg->GetActive() != aWorker) {
+ return;
+ }
+
+ reg->TryToActivateAsync();
+}
+
+already_AddRefed<ServiceWorkerJobQueue>
+ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey,
+ const nsACString& aScope) {
+ MOZ_ASSERT(!aKey.IsEmpty());
+ ServiceWorkerManager::RegistrationDataPerPrincipal* data;
+ // XXX we could use WithEntryHandle here to avoid a hashtable lookup, except
+ // that leads to a false positive assertion, see bug 1370674 comment 7.
+ if (!mRegistrationInfos.Get(aKey, &data)) {
+ data = mRegistrationInfos
+ .InsertOrUpdate(aKey, MakeUnique<RegistrationDataPerPrincipal>())
+ .get();
+ }
+
+ RefPtr queue = data->mJobQueues.GetOrInsertNew(aScope);
+ return queue.forget();
+}
+
+/* static */
+already_AddRefed<ServiceWorkerManager> ServiceWorkerManager::GetInstance() {
+ if (!gInstance) {
+ RefPtr<ServiceWorkerRegistrar> swr;
+
+ // XXX: Substitute this with an assertion. See comment in Init.
+ if (XRE_IsParentProcess()) {
+ // Don't (re-)create the ServiceWorkerManager if we are already shutting
+ // down.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return nullptr;
+ }
+ // Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar
+ // is initialized.
+ swr = ServiceWorkerRegistrar::Get();
+ if (!swr) {
+ return nullptr;
+ }
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ gInstance = new ServiceWorkerManager();
+ gInstance->Init(swr);
+ ClearOnShutdown(&gInstance);
+ }
+ RefPtr<ServiceWorkerManager> copy = gInstance.get();
+ return copy.forget();
+}
+
+void ServiceWorkerManager::ReportToAllClients(
+ const nsCString& aScope, const nsString& aMessage,
+ const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags) {
+ ConsoleUtils::ReportForServiceWorkerScope(
+ NS_ConvertUTF8toUTF16(aScope), aMessage, aFilename, aLineNumber,
+ aColumnNumber, ConsoleUtils::eError);
+}
+
+/* static */
+void ServiceWorkerManager::LocalizeAndReportToAllClients(
+ const nsCString& aScope, const char* aStringKey,
+ const nsTArray<nsString>& aParamArray, uint32_t aFlags,
+ const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return;
+ }
+
+ nsresult rv;
+ nsAutoString message;
+ rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
+ aStringKey, aParamArray, message);
+ if (NS_SUCCEEDED(rv)) {
+ swm->ReportToAllClients(aScope, message, aFilename, aLine, aLineNumber,
+ aColumnNumber, aFlags);
+ } else {
+ NS_WARNING("Failed to format and therefore report localized error.");
+ }
+}
+
+void ServiceWorkerManager::HandleError(
+ JSContext* aCx, nsIPrincipal* aPrincipal, const nsCString& aScope,
+ const nsString& aWorkerURL, const nsString& aMessage,
+ const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ ServiceWorkerManager::RegistrationDataPerPrincipal* data;
+ if (NS_WARN_IF(!mRegistrationInfos.Get(scopeKey, &data))) {
+ return;
+ }
+
+ // Always report any uncaught exceptions or errors to the console of
+ // each client.
+ ReportToAllClients(aScope, aMessage, aFilename, aLine, aLineNumber,
+ aColumnNumber, aFlags);
+}
+
+void ServiceWorkerManager::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<nsIPrincipal> 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<nsIURI> uri = principal->GetURI();
+
+ // We do check the URI scheme here because when this is going to run
+ // the extension may not have been loaded yet and the WebExtensionPolicy
+ // may not exist yet.
+ if (uri->SchemeIs("moz-extension")) {
+ PurgeServiceWorker(aRegistration, principal);
+ return;
+ }
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(principal, aRegistration.scope());
+ if (!registration) {
+ registration =
+ CreateNewRegistration(aRegistration.scope(), principal,
+ static_cast<ServiceWorkerUpdateViaCache>(
+ aRegistration.updateViaCache()),
+ aRegistration.navigationPreloadState());
+ } else {
+ // If active worker script matches our expectations for a "current worker",
+ // then we are done. Since scripts with the same URL might have different
+ // contents such as updated scripts or scripts with different LoadFlags, we
+ // use the CacheName to judge whether the two scripts are identical, where
+ // the CacheName is an UUID generated when a new script is found.
+ if (registration->GetActive() &&
+ registration->GetActive()->CacheName() == aRegistration.cacheName()) {
+ // No needs for updates.
+ return;
+ }
+ }
+
+ registration->SetLastUpdateTime(aRegistration.lastUpdateTime());
+
+ nsLoadFlags importsLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
+ if (aRegistration.updateViaCache() !=
+ static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None)) {
+ importsLoadFlags |= nsIRequest::VALIDATE_ALWAYS;
+ }
+
+ const nsCString& currentWorkerURL = aRegistration.currentWorkerURL();
+ if (!currentWorkerURL.IsEmpty()) {
+ registration->SetActive(new ServiceWorkerInfo(
+ registration->Principal(), registration->Scope(), registration->Id(),
+ registration->Version(), currentWorkerURL, aRegistration.cacheName(),
+ importsLoadFlags));
+ registration->GetActive()->SetHandlesFetch(
+ aRegistration.currentWorkerHandlesFetch());
+ registration->GetActive()->SetInstalledTime(
+ aRegistration.currentWorkerInstalledTime());
+ registration->GetActive()->SetActivatedTime(
+ aRegistration.currentWorkerActivatedTime());
+ }
+}
+
+void ServiceWorkerManager::LoadRegistrations(
+ const nsTArray<ServiceWorkerRegistrationData>& aRegistrations) {
+ MOZ_ASSERT(NS_IsMainThread());
+ uint32_t fetch = 0;
+ for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) {
+ LoadRegistration(aRegistrations[i]);
+ if (aRegistrations[i].currentWorkerHandlesFetch()) {
+ fetch++;
+ }
+ }
+ gServiceWorkersRegistered = aRegistrations.Length();
+ gServiceWorkersRegisteredFetch = fetch;
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"All"_ns, gServiceWorkersRegistered);
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"Fetch"_ns, gServiceWorkersRegisteredFetch);
+ LOG(("LoadRegistrations: %u, fetch %u\n", gServiceWorkersRegistered,
+ gServiceWorkersRegisteredFetch));
+}
+
+void ServiceWorkerManager::StoreRegistration(
+ nsIPrincipal* aPrincipal, ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aRegistration);
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ // Do not store a registration for addons that are not installed, not enabled
+ // or installed temporarily.
+ //
+ // If the dom.serviceWorkers.testing.persistTemporaryInstalledAddons is set
+ // to true, the registration for a temporary installed addon will still be
+ // persisted (only meant to be used to make it easier to test some particular
+ // scenario with a temporary installed addon which doesn't need to be signed
+ // to be installed on release channel builds).
+ if (aPrincipal->SchemeIs("moz-extension")) {
+ RefPtr<extensions::WebExtensionPolicy> addonPolicy =
+ BasePrincipal::Cast(aPrincipal)->AddonPolicy();
+ if (!addonPolicy || !addonPolicy->Active() ||
+ (addonPolicy->TemporarilyInstalled() &&
+ !StaticPrefs::
+ dom_serviceWorkers_testing_persistTemporarilyInstalledAddons())) {
+ return;
+ }
+ }
+
+ ServiceWorkerRegistrationData data;
+ nsresult rv = PopulateRegistrationData(aPrincipal, aRegistration, data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ PrincipalInfo principalInfo;
+ if (NS_WARN_IF(
+ NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) {
+ return;
+ }
+
+ mActor->SendRegister(data);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetServiceWorkerRegistrationInfo(
+ const ClientInfo& aClientInfo) const {
+ auto principalOrErr = aClientInfo.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aClientInfo.URL());
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return GetServiceWorkerRegistrationInfo(principal, uri);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal,
+ nsIURI* aURI) const {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aURI);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ return GetServiceWorkerRegistrationInfo(scopeKey, aURI);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetServiceWorkerRegistrationInfo(
+ const nsACString& aScopeKey, nsIURI* aURI) const {
+ MOZ_ASSERT(aURI);
+
+ nsAutoCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ nsAutoCString scope;
+ RegistrationDataPerPrincipal* data;
+ if (!FindScopeForPath(aScopeKey, spec, &data, scope)) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(data);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration;
+ data->mInfos.Get(scope, getter_AddRefs(registration));
+ // ordered scopes and registrations better be in sync.
+ MOZ_ASSERT(registration);
+
+#ifdef DEBUG
+ nsAutoCString origin;
+ rv = registration->Principal()->GetOrigin(origin);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(origin.Equals(aScopeKey));
+#endif
+
+ return registration.forget();
+}
+
+/* static */
+nsresult ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal,
+ nsACString& aKey) {
+ MOZ_ASSERT(aPrincipal);
+
+ if (!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = aPrincipal->GetOrigin(aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+/* static */
+nsresult ServiceWorkerManager::PrincipalInfoToScopeKey(
+ const PrincipalInfo& aPrincipalInfo, nsACString& aKey) {
+ if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto content = aPrincipalInfo.get_ContentPrincipalInfo();
+
+ nsAutoCString suffix;
+ content.attrs().CreateSuffix(suffix);
+
+ aKey = content.originNoSuffix();
+ aKey.Append(suffix);
+
+ return NS_OK;
+}
+
+/* static */
+void ServiceWorkerManager::AddScopeAndRegistration(
+ const nsACString& aScope, ServiceWorkerRegistrationInfo* aInfo) {
+ MOZ_ASSERT(aInfo);
+ MOZ_ASSERT(aInfo->Principal());
+ MOZ_ASSERT(!aInfo->IsUnregistered());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // browser shutdown
+ return;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = swm->PrincipalToScopeKey(aInfo->Principal(), scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ MOZ_ASSERT(!scopeKey.IsEmpty());
+
+ auto* const data = swm->mRegistrationInfos.GetOrInsertNew(scopeKey);
+ data->mScopeContainer.InsertScope(aScope);
+ data->mInfos.InsertOrUpdate(aScope, RefPtr{aInfo});
+ swm->NotifyListenersOnRegister(aInfo);
+}
+
+/* static */
+bool ServiceWorkerManager::FindScopeForPath(
+ const nsACString& aScopeKey, const nsACString& aPath,
+ RegistrationDataPerPrincipal** aData, nsACString& aMatch) {
+ MOZ_ASSERT(aData);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+ if (!swm || !swm->mRegistrationInfos.Get(aScopeKey, aData)) {
+ return false;
+ }
+
+ Maybe<nsCString> scope = (*aData)->mScopeContainer.MatchScope(aPath);
+
+ if (scope) {
+ // scope.isSome() will still truen true after this; we are just moving the
+ // string inside the Maybe, so the Maybe will contain an empty string.
+ aMatch = std::move(*scope);
+ }
+
+ return scope.isSome();
+}
+
+/* static */
+bool ServiceWorkerManager::HasScope(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return false;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!swm->mRegistrationInfos.Get(scopeKey, &data)) {
+ return false;
+ }
+
+ return data->mScopeContainer.Contains(aScope);
+}
+
+/* static */
+void ServiceWorkerManager::RemoveScopeAndRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = swm->PrincipalToScopeKey(aRegistration->Principal(), scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!swm->mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ if (auto entry = data->mUpdateTimers.Lookup(aRegistration->Scope())) {
+ entry.Data()->Cancel();
+ entry.Remove();
+ }
+
+ // Verify there are no controlled clients for the purged registration.
+ for (auto iter = swm->mControlledClients.Iter(); !iter.Done(); iter.Next()) {
+ auto& reg = iter.UserData()->mRegistrationInfo;
+ if (reg->Scope().Equals(aRegistration->Scope()) &&
+ reg->Principal()->Equals(aRegistration->Principal()) &&
+ reg->IsCorrupt()) {
+ iter.Remove();
+ }
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> info;
+ data->mInfos.Remove(aRegistration->Scope(), getter_AddRefs(info));
+ aRegistration->SetUnregistered();
+ data->mScopeContainer.RemoveScope(aRegistration->Scope());
+ swm->NotifyListenersOnUnregister(info);
+
+ swm->MaybeRemoveRegistrationInfo(scopeKey);
+}
+
+void ServiceWorkerManager::MaybeRemoveRegistrationInfo(
+ const nsACString& aScopeKey) {
+ if (auto entry = mRegistrationInfos.Lookup(aScopeKey)) {
+ if (entry.Data()->mScopeContainer.IsEmpty() &&
+ entry.Data()->mJobQueues.Count() == 0) {
+ entry.Remove();
+
+ // Need to reset the mQuotaUsageCheckCount, if
+ // RegistrationDataPerPrincipal:: mScopeContainer is empty. This
+ // RegistrationDataPerPrincipal might be reused, such that quota usage
+ // mitigation can be triggered for the new added registration.
+ } else if (entry.Data()->mScopeContainer.IsEmpty() &&
+ entry.Data()->mQuotaUsageCheckCount) {
+ entry.Data()->mQuotaUsageCheckCount = 0;
+ }
+ }
+}
+
+bool ServiceWorkerManager::StartControlling(
+ const ClientInfo& aClientInfo,
+ const ServiceWorkerDescriptor& aServiceWorker) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto principalOrErr =
+ PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo());
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return false;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ nsCOMPtr<nsIURI> scope;
+ nsresult rv = NS_NewURI(getter_AddRefs(scope), aServiceWorker.Scope());
+ NS_ENSURE_SUCCESS(rv, false);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, scope);
+ NS_ENSURE_TRUE(registration, false);
+ NS_ENSURE_TRUE(registration->GetActive(), false);
+
+ StartControllingClient(aClientInfo, registration);
+
+ return true;
+}
+
+void ServiceWorkerManager::MaybeCheckNavigationUpdate(
+ const ClientInfo& aClientInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // We perform these success path navigation update steps when the
+ // document tells us its more or less done loading. This avoids
+ // slowing down page load and also lets pages consistently get
+ // updatefound events when they fire.
+ //
+ // 9.8.20 If respondWithEntered is false, then:
+ // 9.8.22 Else: (respondWith was entered and succeeded)
+ // If request is a non-subresource request, then: Invoke Soft Update
+ // algorithm.
+ ControlledClientData* data = mControlledClients.Get(aClientInfo.Id());
+ if (data && data->mRegistrationInfo) {
+ data->mRegistrationInfo->MaybeScheduleUpdate();
+ }
+}
+
+void ServiceWorkerManager::StopControllingRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ aRegistration->StopControllingClient();
+ if (aRegistration->IsControllingClients()) {
+ return;
+ }
+
+ if (aRegistration->IsUnregistered()) {
+ if (aRegistration->IsIdle()) {
+ aRegistration->Clear();
+ } else {
+ aRegistration->ClearWhenIdle();
+ }
+ return;
+ }
+
+ // We use to aggressively terminate the worker at this point, but it
+ // caused problems. There are more uses for a service worker than actively
+ // controlled documents. We need to let the worker naturally terminate
+ // in case its handling push events, message events, etc.
+ aRegistration->TryToActivateAsync();
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetScopeForUrl(nsIPrincipal* aPrincipal,
+ const nsAString& aUrl, nsAString& aScope) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> r =
+ GetServiceWorkerRegistrationInfo(aPrincipal, uri);
+ if (!r) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CopyUTF8toUTF16(r->Scope(), aScope);
+ return NS_OK;
+}
+
+namespace {
+
+class ContinueDispatchFetchEventRunnable : public Runnable {
+ RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
+ nsCOMPtr<nsIInterceptedChannel> mChannel;
+ nsCOMPtr<nsILoadGroup> mLoadGroup;
+
+ public:
+ ContinueDispatchFetchEventRunnable(
+ ServiceWorkerPrivate* aServiceWorkerPrivate,
+ nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup)
+ : Runnable(
+ "dom::ServiceWorkerManager::ContinueDispatchFetchEventRunnable"),
+ mServiceWorkerPrivate(aServiceWorkerPrivate),
+ mChannel(aChannel),
+ mLoadGroup(aLoadGroup) {
+ MOZ_ASSERT(aServiceWorkerPrivate);
+ MOZ_ASSERT(aChannel);
+ }
+
+ void HandleError() {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_WARNING("Unexpected error while dispatching fetch event!");
+ nsresult rv = mChannel->ResetInterception(false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to resume intercepted network request");
+ mChannel->CancelInterception(rv);
+ }
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIChannel> channel;
+ nsresult rv = mChannel->GetChannel(getter_AddRefs(channel));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleError();
+ return NS_OK;
+ }
+
+ // The channel might have encountered an unexpected error while ensuring
+ // the upload stream is cloneable. Check here and reset the interception
+ // if that happens.
+ nsresult status;
+ rv = channel->GetStatus(&status);
+ if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) {
+ HandleError();
+ return NS_OK;
+ }
+
+ nsString clientId;
+ nsString resultingClientId;
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
+ if (clientInfo.isSome()) {
+ clientId = NSID_TrimBracketsUTF16(clientInfo->Id());
+ }
+
+ // Having an initial or reserved client are mutually exclusive events:
+ // either an initial client is used upon navigating an about:blank
+ // iframe, or a new, reserved environment/client is created (e.g.
+ // upon a top-level navigation). See step 4 of
+ // https://html.spec.whatwg.org/#process-a-navigate-fetch as well as
+ // https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444
+ Maybe<ClientInfo> resulting = loadInfo->GetInitialClientInfo();
+
+ if (resulting.isNothing()) {
+ resulting = loadInfo->GetReservedClientInfo();
+ } else {
+ MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing());
+ }
+
+ if (resulting.isSome()) {
+ resultingClientId = NSID_TrimBracketsUTF16(resulting->Id());
+ }
+
+ rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId,
+ resultingClientId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleError();
+ }
+
+ return NS_OK;
+ }
+};
+
+} // anonymous namespace
+
+void ServiceWorkerManager::DispatchFetchEvent(nsIInterceptedChannel* aChannel,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsIChannel> internalChannel;
+ aRv = aChannel->GetChannel(getter_AddRefs(internalChannel));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ aRv = internalChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = internalChannel->LoadInfo();
+ RefPtr<ServiceWorkerInfo> serviceWorker;
+
+ if (!nsContentUtils::IsNonSubresourceRequest(internalChannel)) {
+ const Maybe<ServiceWorkerDescriptor>& controller =
+ loadInfo->GetController();
+ if (NS_WARN_IF(controller.isNothing())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration;
+ nsresult rv = GetClientRegistration(loadInfo->GetClientInfo().ref(),
+ getter_AddRefs(registration));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ serviceWorker = registration->GetActive();
+ if (NS_WARN_IF(!serviceWorker) ||
+ NS_WARN_IF(serviceWorker->Descriptor().Id() != controller.ref().Id())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ } else {
+ nsCOMPtr<nsIURI> uri;
+ aRv = aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // non-subresource request means the URI contains the principal
+ OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ if (StaticPrefs::privacy_partition_serviceWorkers()) {
+ StoragePrincipalHelper::GetOriginAttributes(
+ internalChannel, attrs,
+ StoragePrincipalHelper::eForeignPartitionedPrincipal);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, attrs);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, uri);
+ if (NS_WARN_IF(!registration)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // While we only enter this method if IsAvailable() previously saw
+ // an active worker, it is possible for that worker to be removed
+ // before we get to this point. Therefore we must handle a nullptr
+ // active worker here.
+ serviceWorker = registration->GetActive();
+ if (NS_WARN_IF(!serviceWorker)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // If there is a reserved client it should be marked as controlled before
+ // the FetchEvent is dispatched.
+ Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo();
+
+ // Also override the initial about:blank controller since the real
+ // network load may be intercepted by a different service worker. If
+ // the intial about:blank has a controller here its simply been
+ // inherited from its parent.
+ if (clientInfo.isNothing()) {
+ clientInfo = loadInfo->GetInitialClientInfo();
+
+ // TODO: We need to handle the case where the initial about:blank is
+ // controlled, but the final document load is not. Right now
+ // the spec does not really say what to do. There currently
+ // is no way for the controller to be cleared from a client in
+ // the spec or our implementation. We may want to force a
+ // new inner window to be created instead of reusing the
+ // initial about:blank global. See bug 1419620 and the spec
+ // issue here: https://github.com/w3c/ServiceWorker/issues/1232
+ }
+
+ if (clientInfo.isSome()) {
+ // ClientChannelHelper is not called for STS upgrades that get
+ // intercepted by a service worker when interception occurs in
+ // the content process. Therefore the reserved client is not
+ // properly cleared in that case leading to a situation where
+ // a ClientSource with an http:// principal is controlled by
+ // a ServiceWorker with an https:// principal.
+ //
+ // This does not occur when interception is handled by the
+ // simpler InterceptedHttpChannel approach in the parent.
+ //
+ // As a temporary work around check for this principal mismatch
+ // here and perform the ClientChannelHelper's replacement of
+ // reserved client automatically.
+ if (!XRE_IsParentProcess()) {
+ auto clientPrincipalOrErr = clientInfo.ref().GetPrincipal();
+
+ nsCOMPtr<nsIPrincipal> clientPrincipal;
+ if (clientPrincipalOrErr.isOk()) {
+ clientPrincipal = clientPrincipalOrErr.unwrap();
+ }
+
+ if (!clientPrincipal || !clientPrincipal->Equals(principal)) {
+ UniquePtr<ClientSource> reservedClient =
+ loadInfo->TakeReservedClientSource();
+
+ nsCOMPtr<nsISerialEventTarget> target =
+ reservedClient ? reservedClient->EventTarget()
+ : GetMainThreadSerialEventTarget();
+
+ reservedClient.reset();
+ reservedClient = ClientManager::CreateSource(ClientType::Window,
+ target, principal);
+
+ loadInfo->GiveReservedClientSource(std::move(reservedClient));
+
+ clientInfo = loadInfo->GetReservedClientInfo();
+ }
+ }
+
+ // First, attempt to mark the reserved client controlled directly. This
+ // will update the controlled status in the ClientManagerService in the
+ // parent. It will also eventually propagate back to the ClientSource.
+ StartControllingClient(clientInfo.ref(), registration);
+ }
+
+ uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL;
+ nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(internalChannel);
+ MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode));
+
+ // Synthetic redirects for non-subresource requests with a "follow"
+ // redirect mode may switch controllers. This is basically worker
+ // scripts right now. In this case we need to explicitly clear the
+ // controller to avoid assertions on the SetController() below.
+ if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) {
+ loadInfo->ClearController();
+ }
+
+ // But we also note the reserved state on the LoadInfo. This allows the
+ // ClientSource to be updated immediately after the nsIChannel starts.
+ // This is necessary to have the correct controller in place for immediate
+ // follow-on requests.
+ loadInfo->SetController(serviceWorker->Descriptor());
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(serviceWorker);
+
+ RefPtr<ContinueDispatchFetchEventRunnable> continueRunnable =
+ new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(),
+ aChannel, loadGroup);
+
+ // When this service worker was registered, we also sent down the permissions
+ // for the runnable. They should have arrived by now, but we still need to
+ // wait for them if they have not.
+ RefPtr<PermissionManager> permMgr = PermissionManager::GetInstance();
+ if (permMgr) {
+ permMgr->WhenPermissionsAvailable(serviceWorker->Principal(),
+ continueRunnable);
+ } else {
+ continueRunnable->HandleError();
+ }
+}
+
+bool ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI,
+ nsIChannel* aChannel) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aURI);
+ MOZ_ASSERT(aChannel);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(aPrincipal, aURI);
+
+ if (!registration || !registration->GetActive()) {
+ return false;
+ }
+
+ // Checking if the matched service worker handles fetch events or not.
+ // If it does, directly return true and handle the client controlling logic
+ // in DispatchFetchEvent(). otherwise, do followings then return false.
+ // 1. Set the matched service worker as the controller of LoadInfo and
+ // correspoinding ClinetInfo
+ // 2. Maybe schedule a soft update
+ if (!registration->GetActive()->HandlesFetch()) {
+ // Checkin if the channel is not allowed for the service worker.
+ auto storageAccess = StorageAllowedForChannel(aChannel);
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ if (storageAccess != StorageAccess::eAllow) {
+ if (!StaticPrefs::privacy_partition_serviceWorkers()) {
+ return false;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+
+ if (!StoragePartitioningEnabled(storageAccess, cookieJarSettings)) {
+ return false;
+ }
+ }
+
+ // ServiceWorkerInterceptController::ShouldPrepareForIntercept() handles the
+ // subresource cases. Must be non-subresource case here.
+ MOZ_ASSERT(nsContentUtils::IsNonSubresourceRequest(aChannel));
+
+ Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo();
+ if (clientInfo.isNothing()) {
+ clientInfo = loadInfo->GetInitialClientInfo();
+ }
+
+ if (clientInfo.isSome()) {
+ StartControllingClient(clientInfo.ref(), registration);
+ }
+
+ uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL;
+ nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(aChannel);
+ MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode));
+
+ // Synthetic redirects for non-subresource requests with a "follow"
+ // redirect mode may switch controllers. This is basically worker
+ // scripts right now. In this case we need to explicitly clear the
+ // controller to avoid assertions on the SetController() below.
+ if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) {
+ loadInfo->ClearController();
+ }
+
+ loadInfo->SetController(registration->GetActive()->Descriptor());
+
+ // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm 17.1
+ // try schedule a soft-update for non-subresource case.
+ registration->MaybeScheduleUpdate();
+ return false;
+ }
+ // Found a matching service worker which handles fetch events, return true.
+ return true;
+}
+
+nsresult ServiceWorkerManager::GetClientRegistration(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo** aRegistrationInfo) {
+ ControlledClientData* data = mControlledClients.Get(aClientInfo.Id());
+ if (!data || !data->mRegistrationInfo) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // If the document is controlled, the current worker MUST be non-null.
+ if (!data->mRegistrationInfo->GetActive()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> ref = data->mRegistrationInfo;
+ ref.forget(aRegistrationInfo);
+ return NS_OK;
+}
+
+int32_t ServiceWorkerManager::GetPrincipalQuotaUsageCheckCount(
+ nsIPrincipal* aPrincipal) {
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return -1;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return -1;
+ }
+
+ return data->mQuotaUsageCheckCount;
+}
+
+void ServiceWorkerManager::CheckPrincipalQuotaUsage(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ // Had already schedule a quota usage check.
+ if (data->mQuotaUsageCheckCount != 0) {
+ return;
+ }
+
+ ++data->mQuotaUsageCheckCount;
+
+ // Get the corresponding ServiceWorkerRegistrationInfo here. Unregisteration
+ // might be triggered later, should get it here before it be removed from
+ // data.mInfos, such that NotifyListenersOnQuotaCheckFinish() can notify the
+ // corresponding ServiceWorkerRegistrationInfo after asynchronous quota
+ // checking finish.
+ RefPtr<ServiceWorkerRegistrationInfo> info;
+ data->mInfos.Get(aScope, getter_AddRefs(info));
+ MOZ_ASSERT(info);
+
+ RefPtr<ServiceWorkerManager> self = this;
+
+ ClearQuotaUsageIfNeeded(aPrincipal, [self, info](bool aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ self->NotifyListenersOnQuotaUsageCheckFinish(info);
+ });
+}
+
+void ServiceWorkerManager::SoftUpdate(const OriginAttributes& aOriginAttributes,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ SoftUpdateInternal(aOriginAttributes, aScope, nullptr);
+}
+
+namespace {
+
+class UpdateJobCallback final : public ServiceWorkerJob::Callback {
+ RefPtr<ServiceWorkerUpdateFinishCallback> mCallback;
+
+ ~UpdateJobCallback() { MOZ_ASSERT(!mCallback); }
+
+ public:
+ explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback)
+ : mCallback(aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+ }
+
+ void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+ MOZ_ASSERT(mCallback);
+
+ auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; });
+
+ if (aStatus.Failed()) {
+ mCallback->UpdateFailed(aStatus);
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update);
+ RefPtr<ServiceWorkerUpdateJob> updateJob =
+ static_cast<ServiceWorkerUpdateJob*>(aJob);
+ RefPtr<ServiceWorkerRegistrationInfo> reg = updateJob->GetRegistration();
+ mCallback->UpdateSucceeded(reg);
+ }
+
+ void JobDiscarded(ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+
+ mCallback->UpdateFailed(aStatus);
+ mCallback = nullptr;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback, override)
+};
+
+} // anonymous namespace
+
+void ServiceWorkerManager::SoftUpdateInternal(
+ const OriginAttributes& aOriginAttributes, const nsACString& aScope,
+ ServiceWorkerUpdateFinishCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ auto result = ScopeToPrincipal(aScope, aOriginAttributes);
+ if (NS_WARN_IF(result.isErr())) {
+ return;
+ }
+
+ auto principal = result.unwrap();
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(principal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(scopeKey, aScope);
+ if (NS_WARN_IF(!registration)) {
+ return;
+ }
+
+ // "If registration's installing worker is not null, abort these steps."
+ if (registration->GetInstalling()) {
+ return;
+ }
+
+ // "Let newestWorker be the result of running Get Newest Worker algorithm
+ // passing registration as its argument.
+ // If newestWorker is null, abort these steps."
+ RefPtr<ServiceWorkerInfo> newest = registration->Newest();
+ if (!newest) {
+ return;
+ }
+
+ // "If the registration queue for registration is empty, invoke Update
+ // algorithm, or its equivalent, with client, registration as its argument."
+ // TODO(catalinb): We don't implement the force bypass cache flag.
+ // See: https://github.com/slightlyoff/ServiceWorker/issues/759
+ RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope);
+
+ RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob(
+ principal, registration->Scope(), newest->ScriptSpec(),
+ registration->GetUpdateViaCache());
+
+ if (aCallback) {
+ RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback);
+ job->AppendResultCallback(cb);
+ }
+
+ queue->ScheduleJob(job);
+}
+
+void ServiceWorkerManager::Update(
+ nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString aNewestWorkerScriptUrl,
+ ServiceWorkerUpdateFinishCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty());
+
+ UpdateInternal(aPrincipal, aScope, std::move(aNewestWorkerScriptUrl),
+ aCallback);
+}
+
+void ServiceWorkerManager::UpdateInternal(
+ nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString&& aNewestWorkerScriptUrl,
+ ServiceWorkerUpdateFinishCallback* aCallback) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty());
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(scopeKey, aScope);
+ if (NS_WARN_IF(!registration)) {
+ ErrorResult error;
+ error.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(aScope, "uninstalled");
+ aCallback->UpdateFailed(error);
+
+ // In case the callback does not consume the exception
+ error.SuppressException();
+ return;
+ }
+
+ RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope);
+
+ // "Let job be the result of running Create Job with update, registration’s
+ // scope url, newestWorker’s script url, promise, and the context object’s
+ // relevant settings object."
+ RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob(
+ aPrincipal, registration->Scope(), std::move(aNewestWorkerScriptUrl),
+ registration->GetUpdateViaCache());
+
+ RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback);
+ job->AppendResultCallback(cb);
+
+ // "Invoke Schedule Job with job."
+ queue->ScheduleJob(job);
+}
+
+RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo* aWorkerRegistration) {
+ MOZ_DIAGNOSTIC_ASSERT(aWorkerRegistration);
+
+ if (!aWorkerRegistration->GetActive()) {
+ CopyableErrorResult rv;
+ rv.ThrowInvalidStateError("Worker is not active");
+ return GenericErrorResultPromise::CreateAndReject(rv, __func__);
+ }
+
+ // Same origin check
+ auto principalOrErr = aClientInfo.GetPrincipal();
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ CopyableErrorResult rv;
+ rv.ThrowSecurityError("Could not extract client's principal");
+ return GenericErrorResultPromise::CreateAndReject(rv, __func__);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ if (!aWorkerRegistration->Principal()->Equals(principal)) {
+ CopyableErrorResult rv;
+ rv.ThrowSecurityError("Worker is for a different origin");
+ return GenericErrorResultPromise::CreateAndReject(rv, __func__);
+ }
+
+ // The registration that should be controlling the client
+ RefPtr<ServiceWorkerRegistrationInfo> matchingRegistration =
+ GetServiceWorkerRegistrationInfo(aClientInfo);
+
+ // The registration currently controlling the client
+ RefPtr<ServiceWorkerRegistrationInfo> controllingRegistration;
+ GetClientRegistration(aClientInfo, getter_AddRefs(controllingRegistration));
+
+ if (aWorkerRegistration != matchingRegistration ||
+ aWorkerRegistration == controllingRegistration) {
+ return GenericErrorResultPromise::CreateAndResolve(true, __func__);
+ }
+
+ return StartControllingClient(aClientInfo, aWorkerRegistration);
+}
+
+RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient(
+ const ClientInfo& aClientInfo,
+ const ServiceWorkerDescriptor& aServiceWorker) {
+ auto principalOrErr = aServiceWorker.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return GenericErrorResultPromise::CreateAndResolve(false, __func__);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(principal, aServiceWorker.Scope());
+
+ // While ServiceWorkerManager is distributed across child processes its
+ // possible for us to sometimes get a claim for a new worker that has
+ // not propagated to this process yet. For now, simply note that we
+ // are done. The fix for this is to move the SWM to the parent process
+ // so there are no consistency errors.
+ if (NS_WARN_IF(!registration) || NS_WARN_IF(!registration->GetActive())) {
+ return GenericErrorResultPromise::CreateAndResolve(false, __func__);
+ }
+
+ return MaybeClaimClient(aClientInfo, registration);
+}
+
+void ServiceWorkerManager::UpdateClientControllers(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerInfo> activeWorker = aRegistration->GetActive();
+ MOZ_DIAGNOSTIC_ASSERT(activeWorker);
+
+ AutoTArray<RefPtr<ClientHandle>, 16> handleList;
+ for (const auto& client : mControlledClients.Values()) {
+ if (client->mRegistrationInfo != aRegistration) {
+ continue;
+ }
+
+ handleList.AppendElement(client->mClientHandle);
+ }
+
+ // Fire event after iterating mControlledClients is done to prevent
+ // modification by reentering from the event handlers during iteration.
+ for (auto& handle : handleList) {
+ RefPtr<GenericErrorResultPromise> p =
+ handle->Control(activeWorker->Descriptor());
+
+ RefPtr<ServiceWorkerManager> self = this;
+
+ // If we fail to control the client, then automatically remove it
+ // from our list of controlled clients.
+ p->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](bool) {
+ // do nothing on success
+ },
+ [self, clientInfo = handle->Info()](const CopyableErrorResult& aRv) {
+ // failed to control, forget about this client
+ self->StopControllingClient(clientInfo);
+ });
+ }
+}
+
+void ServiceWorkerManager::EvictFromBFCache(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (const auto& client : mControlledClients.Values()) {
+ if (client->mRegistrationInfo == aRegistration) {
+ client->mClientHandle->EvictFromBFCache();
+ }
+ }
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetRegistration(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) const {
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return GetRegistration(scopeKey, aScope);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetRegistration(const PrincipalInfo& aPrincipalInfo,
+ const nsACString& aScope) const {
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalInfoToScopeKey(aPrincipalInfo, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return GetRegistration(scopeKey, aScope);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::ReloadRegistrationsForTest() {
+ if (NS_WARN_IF(!StaticPrefs::dom_serviceWorkers_testing_enabled())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Let's keep it simple and fail if there are any controlled client,
+ // the test case can take care of making sure there is none when this
+ // method will be called.
+ if (NS_WARN_IF(!mControlledClients.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& info : mRegistrationInfos.Values()) {
+ for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) {
+ MOZ_ASSERT(reg);
+ reg->ForceShutdown();
+ }
+ }
+
+ mRegistrationInfos.Clear();
+
+ nsTArray<ServiceWorkerRegistrationData> data;
+ RefPtr<ServiceWorkerRegistrar> swr = ServiceWorkerRegistrar::Get();
+ if (NS_WARN_IF(!swr->ReloadDataForTest())) {
+ return NS_ERROR_FAILURE;
+ }
+ swr->GetRegistrations(data);
+ LoadRegistrations(data);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::RegisterForAddonPrincipal(nsIPrincipal* aPrincipal,
+ JSContext* aCx,
+ dom::Promise** aPromise) {
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<Promise> outer = Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ auto enabled =
+ StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup();
+ if (!enabled) {
+ outer->MaybeRejectWithNotAllowedError(
+ "Disabled. extensions.backgroundServiceWorker.enabled is false");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aPrincipal);
+ auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy();
+ if (!addonPolicy) {
+ outer->MaybeRejectWithNotAllowedError("Not an extension principal");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsCString scope;
+ auto result = addonPolicy->GetURL(u""_ns);
+ if (result.isOk()) {
+ scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap()));
+ } else {
+ outer->MaybeRejectWithUnknownError("Unable to resolve addon scope URL");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsString scriptURL;
+ addonPolicy->GetBackgroundWorker(scriptURL);
+
+ if (scriptURL.IsEmpty()) {
+ outer->MaybeRejectWithNotFoundError("Missing background worker script url");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ Maybe<ClientInfo> clientInfo =
+ dom::ClientManager::CreateInfo(ClientType::All, aPrincipal);
+
+ if (!clientInfo.isSome()) {
+ outer->MaybeRejectWithUnknownError("Error creating clientInfo");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ auto regPromise =
+ Register(clientInfo.ref(), scope, NS_ConvertUTF16toUTF8(scriptURL),
+ dom::ServiceWorkerUpdateViaCache::Imports);
+ const RefPtr<ServiceWorkerManager> self(this);
+ const nsCOMPtr<nsIPrincipal> principal(aPrincipal);
+ regPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, outer, principal,
+ scope](const ServiceWorkerRegistrationDescriptor& regDesc) {
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ self->GetRegistration(principal, scope);
+ if (registration) {
+ outer->MaybeResolve(registration);
+ } else {
+ outer->MaybeRejectWithUnknownError(
+ "Failed to retrieve ServiceWorkerRegistrationInfo");
+ }
+ },
+ [outer](const mozilla::CopyableErrorResult& err) {
+ CopyableErrorResult result(err);
+ outer->MaybeReject(std::move(result));
+ });
+
+ outer.forget(aPromise);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetRegistrationForAddonPrincipal(
+ nsIPrincipal* aPrincipal, nsIServiceWorkerRegistrationInfo** aInfo) {
+ MOZ_ASSERT(aPrincipal);
+
+ MOZ_ASSERT(aPrincipal);
+ auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy();
+ if (!addonPolicy) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCString scope;
+ auto result = addonPolicy->GetURL(u""_ns);
+ if (result.isOk()) {
+ scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap()));
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> info =
+ GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI);
+ if (!info) {
+ aInfo = nullptr;
+ return NS_OK;
+ }
+ info.forget(aInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::WakeForExtensionAPIEvent(
+ const nsAString& aExtensionBaseURL, const nsAString& aAPINamespace,
+ const nsAString& aAPIEventName, JSContext* aCx, dom::Promise** aPromise) {
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<Promise> outer = Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ auto enabled =
+ StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup();
+ if (!enabled) {
+ outer->MaybeRejectWithNotAllowedError(
+ "Disabled. extensions.backgroundServiceWorker.enabled is false");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aExtensionBaseURL);
+ if (NS_FAILED(rv)) {
+ outer->MaybeReject(rv);
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY_VAR(principal, ScopeToPrincipal(scopeURI, {}));
+
+ auto* addonPolicy = BasePrincipal::Cast(principal)->AddonPolicy();
+ if (NS_WARN_IF(!addonPolicy)) {
+ outer->MaybeRejectWithNotAllowedError(
+ "Not an extension principal or extension disabled");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ OriginAttributes attrs;
+ ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(
+ attrs, NS_ConvertUTF16toUTF8(aExtensionBaseURL));
+ if (NS_WARN_IF(!info)) {
+ outer->MaybeRejectWithInvalidStateError(
+ "No active worker for the extension background service worker");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate();
+ auto result =
+ workerPrivate->WakeForExtensionAPIEvent(aAPINamespace, aAPIEventName);
+ if (result.isErr()) {
+ outer->MaybeReject(result.propagateErr());
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ RefPtr<ServiceWorkerPrivate::PromiseExtensionWorkerHasListener> innerPromise =
+ result.unwrap();
+
+ innerPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [outer](bool aSubscribedEvent) { outer->MaybeResolve(aSubscribedEvent); },
+ [outer](nsresult aErrorResult) { outer->MaybeReject(aErrorResult); });
+
+ outer.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetRegistrationByPrincipal(
+ nsIPrincipal* aPrincipal, const nsAString& aScope,
+ nsIServiceWorkerRegistrationInfo** aInfo) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aInfo);
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> info =
+ GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI);
+ if (!info) {
+ return NS_ERROR_FAILURE;
+ }
+ info.forget(aInfo);
+
+ return NS_OK;
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey,
+ const nsACString& aScope) const {
+ RefPtr<ServiceWorkerRegistrationInfo> reg;
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(aScopeKey, &data)) {
+ return reg.forget();
+ }
+
+ data->mInfos.Get(aScope, getter_AddRefs(reg));
+ return reg.forget();
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::CreateNewRegistration(
+ const nsCString& aScope, nsIPrincipal* aPrincipal,
+ ServiceWorkerUpdateViaCache aUpdateViaCache,
+ IPCNavigationPreloadState aNavigationPreloadState) {
+#ifdef DEBUG
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ RefPtr<ServiceWorkerRegistrationInfo> tmp =
+ GetRegistration(aPrincipal, aScope);
+ MOZ_ASSERT(!tmp);
+#endif
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ new ServiceWorkerRegistrationInfo(aScope, aPrincipal, aUpdateViaCache,
+ std::move(aNavigationPreloadState));
+
+ // From now on ownership of registration is with
+ // mServiceWorkerRegistrationInfos.
+ AddScopeAndRegistration(aScope, registration);
+ return registration.forget();
+}
+
+void ServiceWorkerManager::MaybeRemoveRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(aRegistration);
+ RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest();
+ if (!newest && HasScope(aRegistration->Principal(), aRegistration->Scope())) {
+ RemoveRegistration(aRegistration);
+ }
+}
+
+void ServiceWorkerManager::RemoveRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ // Note, we do not need to call mActor->SendUnregister() here. There are a
+ // few ways we can get here: 1) Through a normal unregister which calls
+ // SendUnregister() in the
+ // unregister job Start() method.
+ // 2) Through origin storage being purged. These result in ForceUnregister()
+ // starting unregister jobs which in turn call SendUnregister().
+ // 3) Through the failure to install a new service worker. Since we don't
+ // store the registration until install succeeds, we do not need to call
+ // SendUnregister here.
+ MOZ_ASSERT(HasScope(aRegistration->Principal(), aRegistration->Scope()));
+
+ RemoveScopeAndRegistration(aRegistration);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID));
+ if (!array) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (const auto& info : mRegistrationInfos.Values()) {
+ for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) {
+ MOZ_ASSERT(reg);
+
+ array->AppendElement(reg);
+ }
+ }
+
+ array.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::RemoveRegistrationsByOriginAttributes(
+ const nsAString& aPattern) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(!aPattern.IsEmpty());
+
+ OriginAttributesPattern pattern;
+ MOZ_ALWAYS_TRUE(pattern.Init(aPattern));
+
+ for (const auto& data : mRegistrationInfos.Values()) {
+ // We can use iteration because ForceUnregister (and Unregister) are
+ // async. Otherwise doing some R/W operations on an hashtable during
+ // iteration will crash.
+ for (ServiceWorkerRegistrationInfo* reg : data->mInfos.Values()) {
+ MOZ_ASSERT(reg);
+ MOZ_ASSERT(reg->Principal());
+
+ bool matches = pattern.Matches(reg->Principal()->OriginAttributesRef());
+ if (!matches) {
+ continue;
+ }
+
+ ForceUnregister(data.get(), reg);
+ }
+ }
+
+ return NS_OK;
+}
+
+void ServiceWorkerManager::ForceUnregister(
+ RegistrationDataPerPrincipal* aRegistrationData,
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(aRegistrationData);
+ MOZ_ASSERT(aRegistration);
+
+ RefPtr<ServiceWorkerJobQueue> queue;
+ aRegistrationData->mJobQueues.Get(aRegistration->Scope(),
+ getter_AddRefs(queue));
+ if (queue) {
+ queue->CancelAll();
+ }
+
+ if (auto entry =
+ aRegistrationData->mUpdateTimers.Lookup(aRegistration->Scope())) {
+ entry.Data()->Cancel();
+ entry.Remove();
+ }
+
+ // Since Unregister is async, it is ok to call it in an enumeration.
+ Unregister(aRegistration->Principal(), nullptr,
+ NS_ConvertUTF8toUTF16(aRegistration->Scope()));
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aListener || mListeners.Contains(aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mListeners.AppendElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::RemoveListener(
+ nsIServiceWorkerManagerListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aListener || !mListeners.Contains(aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mListeners.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (strcmp(aTopic, kFinishShutdownTopic) == 0) {
+ MaybeFinishShutdown();
+ return NS_OK;
+ }
+
+ MOZ_CRASH("Received message we aren't supposed to be registered for!");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::PropagateUnregister(
+ nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback,
+ const nsAString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ // Return earlier with an explicit failure if this xpcom method is called
+ // when the ServiceWorkerManager is not initialized yet or it is already
+ // shutting down.
+ if (NS_WARN_IF(!mActor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ PrincipalInfo principalInfo;
+ if (NS_WARN_IF(
+ NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mActor->SendPropagateUnregister(principalInfo, aScope);
+
+ nsresult rv = Unregister(aPrincipal, aCallback, aScope);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void ServiceWorkerManager::NotifyListenersOnRegister(
+ nsIServiceWorkerRegistrationInfo* aInfo) {
+ nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
+ mListeners.Clone());
+ for (size_t index = 0; index < listeners.Length(); ++index) {
+ listeners[index]->OnRegister(aInfo);
+ }
+}
+
+void ServiceWorkerManager::NotifyListenersOnUnregister(
+ nsIServiceWorkerRegistrationInfo* aInfo) {
+ nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
+ mListeners.Clone());
+ for (size_t index = 0; index < listeners.Length(); ++index) {
+ listeners[index]->OnUnregister(aInfo);
+ }
+}
+
+void ServiceWorkerManager::NotifyListenersOnQuotaUsageCheckFinish(
+ nsIServiceWorkerRegistrationInfo* aRegistration) {
+ nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
+ mListeners.Clone());
+ for (size_t index = 0; index < listeners.Length(); ++index) {
+ listeners[index]->OnQuotaUsageCheckFinish(aRegistration);
+ }
+}
+
+class UpdateTimerCallback final : public nsITimerCallback, public nsINamed {
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ const nsCString mScope;
+
+ ~UpdateTimerCallback() = default;
+
+ public:
+ UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope)
+ : mPrincipal(aPrincipal), mScope(aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPrincipal);
+ MOZ_ASSERT(!mScope.IsEmpty());
+ }
+
+ NS_IMETHOD
+ Notify(nsITimer* aTimer) override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // shutting down, do nothing
+ return NS_OK;
+ }
+
+ swm->UpdateTimerFired(mPrincipal, mScope);
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ GetName(nsACString& aName) override {
+ aName.AssignLiteral("UpdateTimerCallback");
+ return NS_OK;
+ }
+
+ NS_DECL_ISUPPORTS
+};
+
+NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback, nsINamed)
+
+void ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(!aScope.IsEmpty());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ data->mUpdateTimers.WithEntryHandle(
+ aScope, [&aPrincipal, &aScope](auto&& entry) {
+ if (entry) {
+ // In case there is already a timer scheduled, just use the original
+ // schedule time. We don't want to push it out to a later time since
+ // that could allow updates to be starved forever if events are
+ // continuously fired.
+ return;
+ }
+
+ nsCOMPtr<nsITimerCallback> callback =
+ new UpdateTimerCallback(aPrincipal, aScope);
+
+ const uint32_t UPDATE_DELAY_MS = 1000;
+
+ nsCOMPtr<nsITimer> timer;
+
+ const nsresult rv =
+ NS_NewTimerWithCallback(getter_AddRefs(timer), callback,
+ UPDATE_DELAY_MS, nsITimer::TYPE_ONE_SHOT);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ entry.Insert(std::move(timer));
+ });
+}
+
+void ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(!aScope.IsEmpty());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ // First cleanup the timer.
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ if (auto entry = data->mUpdateTimers.Lookup(aScope)) {
+ entry.Data()->Cancel();
+ entry.Remove();
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration;
+ data->mInfos.Get(aScope, getter_AddRefs(registration));
+ if (!registration) {
+ return;
+ }
+
+ if (!registration->CheckAndClearIfUpdateNeeded()) {
+ return;
+ }
+
+ OriginAttributes attrs = aPrincipal->OriginAttributesRef();
+
+ SoftUpdate(attrs, aScope);
+}
+
+void ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(!aScope.IsEmpty());
+
+ if (!mActor) {
+ return;
+ }
+
+ PrincipalInfo principalInfo;
+ nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ Unused << mActor->SendUnregister(principalInfo,
+ NS_ConvertUTF8toUTF16(aScope));
+}
+
+void ServiceWorkerManager::AddOrphanedRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRegistration);
+ MOZ_ASSERT(aRegistration->IsUnregistered());
+ MOZ_ASSERT(!aRegistration->IsControllingClients());
+ MOZ_ASSERT(!aRegistration->IsIdle());
+ MOZ_ASSERT(!mOrphanedRegistrations.has(aRegistration));
+
+ MOZ_ALWAYS_TRUE(mOrphanedRegistrations.putNew(aRegistration));
+}
+
+void ServiceWorkerManager::RemoveOrphanedRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRegistration);
+ MOZ_ASSERT(aRegistration->IsUnregistered());
+ MOZ_ASSERT(!aRegistration->IsControllingClients());
+ MOZ_ASSERT(aRegistration->IsIdle());
+ MOZ_ASSERT(mOrphanedRegistrations.has(aRegistration));
+
+ mOrphanedRegistrations.remove(aRegistration);
+}
+
+uint32_t ServiceWorkerManager::MaybeInitServiceWorkerShutdownProgress() const {
+ if (!mShutdownBlocker) {
+ return ServiceWorkerShutdownBlocker::kInvalidShutdownStateId;
+ }
+
+ return mShutdownBlocker->CreateShutdownState();
+}
+
+void ServiceWorkerManager::ReportServiceWorkerShutdownProgress(
+ uint32_t aShutdownStateId,
+ ServiceWorkerShutdownState::Progress aProgress) const {
+ MOZ_ASSERT(mShutdownBlocker);
+ mShutdownBlocker->ReportShutdownProgress(aShutdownStateId, aProgress);
+}
+
+} // namespace mozilla::dom