/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef mozilla_dom_workers_serviceworkermanager_h
#define mozilla_dom_workers_serviceworkermanager_h

#include <cstdint>

#include "ErrorList.h"
#include "ServiceWorkerDescriptor.h"
#include "ServiceWorkerShutdownState.h"
#include "js/ErrorReport.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Assertions.h"
#include "mozilla/HashTable.h"
#include "mozilla/MozPromise.h"
#include "mozilla/RefPtr.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/ClientHandle.h"
#include "mozilla/dom/ClientOpPromise.h"
#include "mozilla/dom/ServiceWorkerLifetimeExtension.h"
#include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
#include "mozilla/dom/ServiceWorkerRegistrationInfo.h"
#include "mozilla/dom/ServiceWorkerUtils.h"
#include "mozilla/mozalloc.h"
#include "nsClassHashtable.h"
#include "nsContentUtils.h"
#include "nsHashKeys.h"
#include "nsIObserver.h"
#include "nsIServiceWorkerManager.h"
#include "nsISupports.h"
#include "nsStringFwd.h"
#include "nsTArray.h"

class nsIConsoleReportCollector;
class nsIServiceWorkerUnregisterCallback;

namespace mozilla {

class OriginAttributes;

namespace ipc {
class PrincipalInfo;
}  // namespace ipc

namespace net {
class CookieStruct;
}

namespace dom {

class ContentParent;
class ServiceWorkerInfo;
class ServiceWorkerJobQueue;
class ServiceWorkerManagerChild;
class ServiceWorkerPrivate;
class ServiceWorkerRegistrar;
class ServiceWorkerShutdownBlocker;
struct CookieListItem;

class ServiceWorkerUpdateFinishCallback {
 protected:
  virtual ~ServiceWorkerUpdateFinishCallback() = default;

 public:
  NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateFinishCallback)

  virtual void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) = 0;

  virtual void UpdateFailed(ErrorResult& aStatus) = 0;
};

#define NS_SERVICEWORKERMANAGER_IMPL_IID      \
  {/* f4f8755a-69ca-46e8-a65d-775745535990 */ \
   0xf4f8755a,                                \
   0x69ca,                                    \
   0x46e8,                                    \
   {0xa6, 0x5d, 0x77, 0x57, 0x45, 0x53, 0x59, 0x90}}

class ETPPermissionObserver final : public nsIObserver {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER

  ETPPermissionObserver();

 private:
  ~ETPPermissionObserver();

  void RegisterObserverEvents();
  void UnregisterObserverEvents();
};

/*
 * The ServiceWorkerManager is a per-process global that deals with the
 * installation, querying and event dispatch of ServiceWorkers for all the
 * origins in the process.
 *
 * NOTE: the following documentation is a WIP:
 *
 * The ServiceWorkerManager (SWM) is a main-thread, parent-process singleton
 * that encapsulates the browser-global state of service workers. This state
 * includes, but is not limited to, all service worker registrations and all
 * controlled service worker clients. The SWM also provides methods to read and
 * mutate this state and to dispatch operations (e.g. DOM events such as a
 * FetchEvent) to service workers.
 *
 * Example usage:
 *
 * MOZ_ASSERT(NS_IsMainThread(), "SWM is main-thread only");
 *
 * RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
 *
 * // Nullness must be checked by code that possibly executes during browser
 * // shutdown, which is when the SWM is destroyed.
 * if (swm) {
 *   // Do something with the SWM.
 * }
 */
class ServiceWorkerManager final : public nsIServiceWorkerManager,
                                   public nsIObserver {
  friend class GetRegistrationsRunnable;
  friend class GetRegistrationRunnable;
  friend class ServiceWorkerInfo;
  friend class ServiceWorkerJob;
  friend class ServiceWorkerRegistrationInfo;
  friend class ServiceWorkerShutdownBlocker;
  friend class ServiceWorkerUnregisterJob;
  friend class ServiceWorkerUpdateJob;
  friend class UpdateTimerCallback;

 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSISERVICEWORKERMANAGER
  NS_DECL_NSIOBSERVER

  // Determine the correct lifetime extension to use for a given client.  This
  // will work for ServiceWorker clients, but ideally you should have a
  // ServiceWorkerDescriptor in that case.
  ServiceWorkerLifetimeExtension DetermineLifetimeForClient(
      const ClientInfo& aClientInfo);

  // Determine the correct lifetime extension to use for a given client.  This
  // will work for ServiceWorker clients, but ideally you should have a
  // ServiceWorkerDescriptor in that case.
  ServiceWorkerLifetimeExtension DetermineLifetimeForServiceWorker(
      const ServiceWorkerDescriptor& aServiceWorker);

  // Return true if the given principal and URI matches a registered service
  // worker which handles fetch event.
  // If there is a matched service worker but doesn't handle fetch events, this
  // method will try to set the matched service worker as the controller of the
  // passed in channel. Then also schedule a soft-update job for the service
  // worker.
  bool IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI,
                   nsIChannel* aChannel);

  void DispatchFetchEvent(nsIInterceptedChannel* aChannel, ErrorResult& aRv);

  void Update(const ClientInfo& aClientInfo, nsIPrincipal* aPrincipal,
              const nsACString& aScope, nsCString aNewestWorkerScriptUrl,
              ServiceWorkerUpdateFinishCallback* aCallback);

  void UpdateInternal(const ClientInfo& aClientInfo, nsIPrincipal* aPrincipal,
                      const nsACString& aScope,
                      nsCString&& aNewestWorkerScriptUrl,
                      ServiceWorkerUpdateFinishCallback* aCallback);

  void SoftUpdate(const OriginAttributes& aOriginAttributes,
                  const nsACString& aScope);

  void SoftUpdateInternal(const OriginAttributes& aOriginAttributes,
                          const nsACString& aScope,
                          ServiceWorkerUpdateFinishCallback* aCallback);

  RefPtr<ServiceWorkerRegistrationPromise> Register(
      const ClientInfo& aClientInfo, const nsACString& aScopeURL,
      const WorkerType& aType, const nsACString& aScriptURL,
      ServiceWorkerUpdateViaCache aUpdateViaCache);

  RefPtr<ServiceWorkerRegistrationPromise> GetRegistration(
      const ClientInfo& aClientInfo, const nsACString& aURL) const;

  RefPtr<ServiceWorkerRegistrationListPromise> GetRegistrations(
      const ClientInfo& aClientInfo) const;

  already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration(
      nsIPrincipal* aPrincipal, const nsACString& aScope) const;

  already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration(
      const mozilla::ipc::PrincipalInfo& aPrincipal,
      const nsACString& aScope) const;

  already_AddRefed<ServiceWorkerRegistrationInfo> CreateNewRegistration(
      const nsCString& aScope, const WorkerType& aType,
      nsIPrincipal* aPrincipal, ServiceWorkerUpdateViaCache aUpdateViaCache,
      IPCNavigationPreloadState aNavigationPreloadState =
          IPCNavigationPreloadState(false, "true"_ns));

  void RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);

  void StoreRegistration(nsIPrincipal* aPrincipal,
                         ServiceWorkerRegistrationInfo* aRegistration);

  /**
   * Report an error for the given scope to any window we think might be
   * interested, failing over to the Browser Console if we couldn't find any.
   *
   * Error messages should be localized, so you probably want to call
   * LocalizeAndReportToAllClients instead, which in turn calls us after
   * localizing the error.
   */
  void ReportToAllClients(const nsCString& aScope, const nsString& aMessage,
                          const nsCString& aFilename, const nsString& aLine,
                          uint32_t aLineNumber, uint32_t aColumnNumber,
                          uint32_t aFlags);

  /**
   * Report a localized error for the given scope to any window we think might
   * be interested.
   *
   * Note that this method takes an nsTArray<nsString> for the parameters, not
   * bare chart16_t*[].  You can use a std::initializer_list constructor inline
   * so that argument might look like: nsTArray<nsString> { some_nsString,
   * PromiseFlatString(some_nsSubString_aka_nsAString),
   * NS_ConvertUTF8toUTF16(some_nsCString_or_nsCSubString),
   * u"some literal"_ns }.  If you have anything else, like a
   * number, you can use an nsAutoString with AppendInt/friends.
   *
   * @param [aFlags]
   *   The nsIScriptError flag, one of errorFlag (0x0), warningFlag (0x1),
   *   infoFlag (0x8).  We default to error if omitted because usually we're
   *   logging exceptional and/or obvious breakage.
   */
  static void LocalizeAndReportToAllClients(
      const nsCString& aScope, const char* aStringKey,
      const nsTArray<nsString>& aParamArray, uint32_t aFlags = 0x0,
      const nsCString& aFilename = ""_ns, const nsString& aLine = u""_ns,
      uint32_t aLineNumber = 0, uint32_t aColumnNumber = 0);

  // Always consumes the error by reporting to consoles of all controlled
  // documents.
  void HandleError(JSContext* aCx, nsIPrincipal* aPrincipal,
                   const nsCString& aScope, const nsCString& aWorkerURL,
                   const nsString& aMessage, const nsCString& aFilename,
                   const nsString& aLine, uint32_t aLineNumber,
                   uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType);

  [[nodiscard]] RefPtr<GenericErrorResultPromise> MaybeClaimClient(
      const ClientInfo& aClientInfo,
      ServiceWorkerRegistrationInfo* aWorkerRegistration);

  [[nodiscard]] RefPtr<GenericErrorResultPromise> MaybeClaimClient(
      const ClientInfo& aClientInfo,
      const ServiceWorkerDescriptor& aServiceWorker);

  static already_AddRefed<ServiceWorkerManager> GetInstance();

  void LoadRegistration(const ServiceWorkerRegistrationData& aRegistration);

  void LoadRegistrations(
      const nsTArray<ServiceWorkerRegistrationData>& aRegistrations);

  void MaybeCheckNavigationUpdate(const ClientInfo& aClientInfo);

  nsresult SendCookieChangeEvent(const OriginAttributes& aOriginAttributes,
                                 const nsACString& aScope,
                                 const net::CookieStruct& aCookie,
                                 bool aCookieDeleted);

  nsresult SendPushEvent(const nsACString& aOriginAttributes,
                         const nsACString& aScope, const nsAString& aMessageId,
                         const Maybe<nsTArray<uint8_t>>& aData);

  void WorkerIsIdle(ServiceWorkerInfo* aWorker);

  RefPtr<ServiceWorkerRegistrationPromise> WhenReady(
      const ClientInfo& aClientInfo);

  void CheckPendingReadyPromises();

  void RemovePendingReadyPromise(const ClientInfo& aClientInfo);

  void NoteInheritedController(const ClientInfo& aClientInfo,
                               const ServiceWorkerDescriptor& aController);

  void BlockShutdownOn(GenericNonExclusivePromise* aPromise,
                       uint32_t aShutdownStateId);

  nsresult GetClientRegistration(
      const ClientInfo& aClientInfo,
      ServiceWorkerRegistrationInfo** aRegistrationInfo);

  int32_t GetPrincipalQuotaUsageCheckCount(nsIPrincipal* aPrincipal);

  void CheckPrincipalQuotaUsage(nsIPrincipal* aPrincipal,
                                const nsACString& aScope);

  // Returns the shutdown state ID (may be an invalid ID if an
  // nsIAsyncShutdownBlocker is not used).
  uint32_t MaybeInitServiceWorkerShutdownProgress() const;

  void ReportServiceWorkerShutdownProgress(
      uint32_t aShutdownStateId,
      ServiceWorkerShutdownState::Progress aProgress) const;

  // Record periodic telemetry on number of running ServiceWorkers.  When
  // the number of running ServiceWorkers changes (or on shutdown),
  // ServiceWorkerPrivateImpl will call RecordTelemetry with the number of
  // running serviceworkers and those supporting Fetch.  We use
  // mTelemetryLastChange to determine how many datapoints to inject into
  // Telemetry, and dispatch a background runnable to call
  // RecordTelemetryGap() and Accumulate them.
  void RecordTelemetry(uint32_t aNumber, uint32_t aFetch);

  void EvictFromBFCache(ServiceWorkerRegistrationInfo* aRegistration);

  nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo>
  GetRegistrations(nsIPrincipal* aPrincipal);

 private:
  struct RegistrationDataPerPrincipal;

  static bool FindScopeForPath(const nsACString& aScopeKey,
                               const nsACString& aPath,
                               RegistrationDataPerPrincipal** aData,
                               nsACString& aMatch);

  ServiceWorkerManager();
  ~ServiceWorkerManager();

  void Init(ServiceWorkerRegistrar* aRegistrar);

  RefPtr<GenericErrorResultPromise> StartControllingClient(
      const ClientInfo& aClientInfo,
      ServiceWorkerRegistrationInfo* aRegistrationInfo,
      bool aControlClientHandle = true);

  void StopControllingClient(const ClientInfo& aClientInfo);

  void MaybeStartShutdown();

  void MaybeFinishShutdown();

  already_AddRefed<ServiceWorkerJobQueue> GetOrCreateJobQueue(
      const nsACString& aOriginSuffix, const nsACString& aScope);

  void MaybeRemoveRegistrationInfo(const nsACString& aScopeKey);

  already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration(
      const nsACString& aScopeKey, const nsACString& aScope) const;

  void AbortCurrentUpdate(ServiceWorkerRegistrationInfo* aRegistration);

  nsresult Update(ServiceWorkerRegistrationInfo* aRegistration);

  ServiceWorkerInfo* GetActiveWorkerInfoForScope(
      const OriginAttributes& aOriginAttributes, const nsACString& aScope);

  // Given the ClientInfo for a ServiceWorker global, return the corresponding
  // ServiceWorkerInfo, nullptr otherwise.  Do not use this for clients for
  // globals that are not ServiceWorkers (and ideally you should be using
  // GetServiceWorkerByDescriptor instead).
  ServiceWorkerInfo* GetServiceWorkerByClientInfo(
      const ClientInfo& aClientInfo) const;

  // Given the ServiceWorkerDescriptor for a ServiceWorker, return the
  // corresponding nullptr otherwise.
  ServiceWorkerInfo* GetServiceWorkerByDescriptor(
      const ServiceWorkerDescriptor& aServiceWorker) const;

  void StopControllingRegistration(
      ServiceWorkerRegistrationInfo* aRegistration);

  // Find the ServiceWorkerRegistration whose scope best matches the URL of the
  // given window or worker client (for the origin of the client based on its
  // principal).  This cannot be used with ServiceWorker ClientInfos.
  already_AddRefed<ServiceWorkerRegistrationInfo>
  GetServiceWorkerRegistrationInfo(const ClientInfo& aClientInfo) const;

  // Find the ServiceWorkerRegistration whose scope best matches the provided
  // URL (for the origin of the given principal).
  //
  // Note that `GetRegistration` should be used in cases where you already have
  // an exact scope.
  already_AddRefed<ServiceWorkerRegistrationInfo>
  GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal,
                                   nsIURI* aURI) const;

  // Find the ServiceWorkerRegistration whose scope best matches the provided
  // URL for the origin encoded as a scope key that has been obtained from
  // PrincipToScopeKey.
  already_AddRefed<ServiceWorkerRegistrationInfo>
  GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey,
                                   nsIURI* aURI) const;

  static nsresult PrincipalToScopeKey(nsIPrincipal* aPrincipal,
                                      nsACString& aKey);

  static nsresult PrincipalInfoToScopeKey(
      const mozilla::ipc::PrincipalInfo& aPrincipalInfo, nsACString& aKey);

  static void AddScopeAndRegistration(
      const nsACString& aScope, ServiceWorkerRegistrationInfo* aRegistation);

  static bool HasScope(nsIPrincipal* aPrincipal, const nsACString& aScope);

  static void RemoveScopeAndRegistration(
      ServiceWorkerRegistrationInfo* aRegistration);

  void QueueFireEventOnServiceWorkerRegistrations(
      ServiceWorkerRegistrationInfo* aRegistration, const nsAString& aName);

  void UpdateClientControllers(ServiceWorkerRegistrationInfo* aRegistration);

  void MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);

  void PurgeServiceWorker(const ServiceWorkerRegistrationData& aRegistration,
                          nsIPrincipal* aPrincipal);

  RefPtr<ServiceWorkerManagerChild> mActor;

  bool mShuttingDown;

  nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> mListeners;

  void NotifyListenersOnRegister(
      nsIServiceWorkerRegistrationInfo* aRegistration);

  void NotifyListenersOnUnregister(
      nsIServiceWorkerRegistrationInfo* aRegistration);

  void NotifyListenersOnQuotaUsageCheckFinish(
      nsIServiceWorkerRegistrationInfo* aRegistration);

  void ScheduleUpdateTimer(nsIPrincipal* aPrincipal, const nsACString& aScope);

  void UpdateTimerFired(nsIPrincipal* aPrincipal, const nsACString& aScope);

  void MaybeSendUnregister(nsIPrincipal* aPrincipal, const nsACString& aScope);

  // Used by remove() and removeAll() when clearing history.
  // MUST ONLY BE CALLED FROM UnregisterIfMatchesHost!
  void ForceUnregister(RegistrationDataPerPrincipal* aRegistrationData,
                       ServiceWorkerRegistrationInfo* aRegistration,
                       nsIServiceWorkerUnregisterCallback* aCallback = nullptr);

  // An "orphaned" registration is one that is unregistered and not controlling
  // clients. The ServiceWorkerManager must know about all orphaned
  // registrations to forcefully shutdown all Service Workers during browser
  // shutdown.
  void AddOrphanedRegistration(ServiceWorkerRegistrationInfo* aRegistration);

  void RemoveOrphanedRegistration(ServiceWorkerRegistrationInfo* aRegistration);

  HashSet<RefPtr<ServiceWorkerRegistrationInfo>,
          PointerHasher<ServiceWorkerRegistrationInfo*>>
      mOrphanedRegistrations;

  RefPtr<ServiceWorkerShutdownBlocker> mShutdownBlocker;

  nsClassHashtable<nsCStringHashKey, RegistrationDataPerPrincipal>
      mRegistrationInfos;

  struct ControlledClientData {
    RefPtr<ClientHandle> mClientHandle;
    RefPtr<ServiceWorkerRegistrationInfo> mRegistrationInfo;

    ControlledClientData(ClientHandle* aClientHandle,
                         ServiceWorkerRegistrationInfo* aRegistrationInfo)
        : mClientHandle(aClientHandle), mRegistrationInfo(aRegistrationInfo) {}
  };

  nsClassHashtable<nsIDHashKey, ControlledClientData> mControlledClients;

  struct PendingReadyData {
    RefPtr<ClientHandle> mClientHandle;
    RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise;

    explicit PendingReadyData(ClientHandle* aClientHandle)
        : mClientHandle(aClientHandle),
          mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)) {}
  };

  nsTArray<UniquePtr<PendingReadyData>> mPendingReadyList;

  const uint32_t mTelemetryPeriodMs = 5 * 1000;
  TimeStamp mTelemetryLastChange;
  RefPtr<ETPPermissionObserver> mETPPermissionObserver;
};

}  // namespace dom
}  // namespace mozilla

#endif  // mozilla_dom_workers_serviceworkermanager_h
