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

#ifndef mozilla_dom_workers_serviceworkermanager_h
#define mozilla_dom_workers_serviceworkermanager_h

#include "nsIServiceWorkerManager.h"
#include "nsCOMPtr.h"

#include "ipc/IPCMessageUtils.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/ConsoleReportCollector.h"
#include "mozilla/LinkedList.h"
#include "mozilla/Preferences.h"
#include "mozilla/TypedEnumBits.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/WeakPtr.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/ServiceWorkerCommon.h"
#include "mozilla/dom/ServiceWorkerRegistrar.h"
#include "mozilla/dom/ServiceWorkerRegistrarTypes.h"
#include "mozilla/dom/workers/ServiceWorkerRegistrationInfo.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "nsClassHashtable.h"
#include "nsDataHashtable.h"
#include "nsIIPCBackgroundChildCreateCallback.h"
#include "nsRefPtrHashtable.h"
#include "nsTArrayForwardDeclare.h"
#include "nsTObserverArray.h"

class mozIApplicationClearPrivateDataParams;
class nsIConsoleReportCollector;

namespace mozilla {

class PrincipalOriginAttributes;

namespace dom {

class ServiceWorkerRegistrar;
class ServiceWorkerRegistrationListener;

namespace workers {

class ServiceWorkerClientInfo;
class ServiceWorkerInfo;
class ServiceWorkerJobQueue;
class ServiceWorkerManagerChild;
class ServiceWorkerPrivate;

class ServiceWorkerUpdateFinishCallback
{
protected:
  virtual ~ServiceWorkerUpdateFinishCallback()
  {}

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 }     \
}

/*
 * 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.
 */
class ServiceWorkerManager final
  : public nsIServiceWorkerManager
  , public nsIIPCBackgroundChildCreateCallback
  , public nsIObserver
{
  friend class GetReadyPromiseRunnable;
  friend class GetRegistrationsRunnable;
  friend class GetRegistrationRunnable;
  friend class ServiceWorkerJob;
  friend class ServiceWorkerRegistrationInfo;
  friend class ServiceWorkerUnregisterJob;
  friend class ServiceWorkerUpdateJob;
  friend class UpdateTimerCallback;

public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSISERVICEWORKERMANAGER
  NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK
  NS_DECL_NSIOBSERVER

  struct RegistrationDataPerPrincipal;
  nsClassHashtable<nsCStringHashKey, RegistrationDataPerPrincipal> mRegistrationInfos;

  nsTObserverArray<ServiceWorkerRegistrationListener*> mServiceWorkerRegistrationListeners;

  nsRefPtrHashtable<nsISupportsHashKey, ServiceWorkerRegistrationInfo> mControlledDocuments;

  // Track all documents that have attempted to register a service worker for a
  // given scope.
  typedef nsTArray<nsCOMPtr<nsIWeakReference>> WeakDocumentList;
  nsClassHashtable<nsCStringHashKey, WeakDocumentList> mRegisteringDocuments;

  // Track all intercepted navigation channels for a given scope.  Channels are
  // placed in the appropriate list before dispatch the FetchEvent to the worker
  // thread and removed once FetchEvent processing dispatches back to the main
  // thread.
  //
  // Note: Its safe to use weak references here because a RAII-style callback
  //       is registered with the channel before its added to this list.  We
  //       are guaranteed the callback will fire before and remove the ref
  //       from this list before the channel is destroyed.
  typedef nsTArray<nsIInterceptedChannel*> InterceptionList;
  nsClassHashtable<nsCStringHashKey, InterceptionList> mNavigationInterceptions;

  bool
  IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI);

  bool
  IsControlled(nsIDocument* aDocument, ErrorResult& aRv);

  // Return true if the given content process could potentially be executing
  // service worker code with the given principal.  At the current time, this
  // just means that we have any registration for the origin, regardless of
  // scope.  This is a very weak guarantee but is the best we can do when push
  // notifications can currently spin up a service worker in content processes
  // without our involvement in the parent process.
  //
  // In the future when there is only a single ServiceWorkerManager in the
  // parent process that is entirely in control of spawning and running service
  // worker code, we will be able to authoritatively indicate whether there is
  // an activate service worker in the given content process.  At that time we
  // will rename this method HasActiveServiceWorkerInstance and provide
  // semantics that ensure this method returns true until the worker is known to
  // have shut down in order to allow the caller to induce a crash for security
  // reasons without having to worry about shutdown races with the worker.
  bool
  MayHaveActiveServiceWorkerInstance(ContentParent* aContent,
                                     nsIPrincipal* aPrincipal);

  void
  DispatchFetchEvent(const PrincipalOriginAttributes& aOriginAttributes,
                     nsIDocument* aDoc,
                     const nsAString& aDocumentIdForTopLevelNavigation,
                     nsIInterceptedChannel* aChannel,
                     bool aIsReload,
                     bool aIsSubresourceLoad,
                     ErrorResult& aRv);

  void
  Update(nsIPrincipal* aPrincipal,
         const nsACString& aScope,
         ServiceWorkerUpdateFinishCallback* aCallback);

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

  void
  PropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes,
                      const nsAString& aScope);

  void
  PropagateRemove(const nsACString& aHost);

  void
  Remove(const nsACString& aHost);

  void
  PropagateRemoveAll();

  void
  RemoveAll();

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

  already_AddRefed<ServiceWorkerRegistrationInfo>
  CreateNewRegistration(const nsCString& aScope, nsIPrincipal* aPrincipal);

  void
  RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);

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

  void
  FinishFetch(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 nsString& 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),
   * NS_LITERAL_STRING("some literal") }.  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 nsString& aFilename = EmptyString(),
                                const nsString& aLine = EmptyString(),
                                uint32_t aLineNumber = 0,
                                uint32_t aColumnNumber = 0);

  void
  FlushReportsToAllClients(const nsACString& aScope,
                           nsIConsoleReportCollector* aReporter);

  // Always consumes the error by reporting to consoles of all controlled
  // documents.
  void
  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);

  UniquePtr<ServiceWorkerClientInfo>
  GetClient(nsIPrincipal* aPrincipal,
            const nsAString& aClientId,
            ErrorResult& aRv);

  void
  GetAllClients(nsIPrincipal* aPrincipal,
                const nsCString& aScope,
                bool aIncludeUncontrolled,
                nsTArray<ServiceWorkerClientInfo>& aDocuments);

  void
  MaybeClaimClient(nsIDocument* aDocument,
                   ServiceWorkerRegistrationInfo* aWorkerRegistration);

  nsresult
  ClaimClients(nsIPrincipal* aPrincipal, const nsCString& aScope, uint64_t aId);

  void
  SetSkipWaitingFlag(nsIPrincipal* aPrincipal, const nsCString& aScope,
                     uint64_t aServiceWorkerID);

  static already_AddRefed<ServiceWorkerManager>
  GetInstance();

  void
  LoadRegistration(const ServiceWorkerRegistrationData& aRegistration);

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

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

  NS_IMETHOD
  AddRegistrationEventListener(const nsAString& aScope,
                               ServiceWorkerRegistrationListener* aListener);

  NS_IMETHOD
  RemoveRegistrationEventListener(const nsAString& aScope,
                                  ServiceWorkerRegistrationListener* aListener);

  void
  MaybeCheckNavigationUpdate(nsIDocument* aDoc);

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

  nsresult
  NotifyUnregister(nsIPrincipal* aPrincipal, const nsAString& aScope);

  void
  WorkerIsIdle(ServiceWorkerInfo* aWorker);

private:
  ServiceWorkerManager();
  ~ServiceWorkerManager();

  void
  Init(ServiceWorkerRegistrar* aRegistrar);

  void
  MaybeStartShutdown();

  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);

  nsresult
  GetDocumentRegistration(nsIDocument* aDoc,
                          ServiceWorkerRegistrationInfo** aRegistrationInfo);

  nsresult
  GetServiceWorkerForScope(nsPIDOMWindowInner* aWindow,
                           const nsAString& aScope,
                           WhichServiceWorker aWhichWorker,
                           nsISupports** aServiceWorker);

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

  ServiceWorkerInfo*
  GetActiveWorkerInfoForDocument(nsIDocument* aDocument);

  void
  InvalidateServiceWorkerRegistrationWorker(ServiceWorkerRegistrationInfo* aRegistration,
                                            WhichServiceWorker aWhichOnes);

  void
  NotifyServiceWorkerRegistrationRemoved(ServiceWorkerRegistrationInfo* aRegistration);

  void
  StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration,
                            nsIDocument* aDoc,
                            const nsAString& aDocumentId);

  void
  StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration);

  already_AddRefed<ServiceWorkerRegistrationInfo>
  GetServiceWorkerRegistrationInfo(nsPIDOMWindowInner* aWindow);

  already_AddRefed<ServiceWorkerRegistrationInfo>
  GetServiceWorkerRegistrationInfo(nsIDocument* aDoc);

  already_AddRefed<ServiceWorkerRegistrationInfo>
  GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, nsIURI* aURI);

  already_AddRefed<ServiceWorkerRegistrationInfo>
  GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey,
                                   nsIURI* aURI);

  // This method generates a key using appId and isInElementBrowser from the
  // principal. We don't use the origin because it can change during the
  // loading.
  static nsresult
  PrincipalToScopeKey(nsIPrincipal* aPrincipal, nsACString& aKey);

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

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

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

  static void
  RemoveScopeAndRegistration(ServiceWorkerRegistrationInfo* aRegistration);

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

  void
  FireUpdateFoundOnServiceWorkerRegistrations(ServiceWorkerRegistrationInfo* aRegistration);

  void
  FireControllerChange(ServiceWorkerRegistrationInfo* aRegistration);

  void
  StorePendingReadyPromise(nsPIDOMWindowInner* aWindow, nsIURI* aURI,
                           Promise* aPromise);

  void
  CheckPendingReadyPromises();

  bool
  CheckReadyPromise(nsPIDOMWindowInner* aWindow, nsIURI* aURI,
                    Promise* aPromise);

  struct PendingReadyPromise final
  {
    PendingReadyPromise(nsIURI* aURI, Promise* aPromise)
      : mURI(aURI), mPromise(aPromise)
    {}

    nsCOMPtr<nsIURI> mURI;
    RefPtr<Promise> mPromise;
  };

  void AppendPendingOperation(nsIRunnable* aRunnable);

  bool HasBackgroundActor() const
  {
    return !!mActor;
  }

  nsClassHashtable<nsISupportsHashKey, PendingReadyPromise> mPendingReadyPromises;

  void
  MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);

  // Removes all service worker registrations that matches the given pattern.
  void
  RemoveAllRegistrations(OriginAttributesPattern* aPattern);

  RefPtr<ServiceWorkerManagerChild> mActor;

  nsTArray<nsCOMPtr<nsIRunnable>> mPendingOperations;

  bool mShuttingDown;

  nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> mListeners;

  void
  NotifyListenersOnRegister(nsIServiceWorkerRegistrationInfo* aRegistration);

  void
  NotifyListenersOnUnregister(nsIServiceWorkerRegistrationInfo* aRegistration);

  void
  AddRegisteringDocument(const nsACString& aScope, nsIDocument* aDoc);

  class InterceptionReleaseHandle;

  void
  AddNavigationInterception(const nsACString& aScope,
                            nsIInterceptedChannel* aChannel);

  void
  RemoveNavigationInterception(const nsACString& aScope,
                               nsIInterceptedChannel* aChannel);

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

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

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

  nsresult
  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);
};

} // namespace workers
} // namespace dom
} // namespace mozilla

#endif // mozilla_dom_workers_serviceworkermanager_h