/* -*- 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
