/* 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_SessionStorageManager_h
#define mozilla_dom_SessionStorageManager_h

#include "StorageObserver.h"
#include "ipc/EnumSerializer.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundParent.h"
#include "nsClassHashtable.h"
#include "nsCycleCollectionParticipant.h"
#include "nsHashKeys.h"
#include "nsIDOMStorageManager.h"

class nsIPrincipal;
class nsITimer;

namespace mozilla {
class OriginAttributesPattern;

namespace dom {

class SSCacheCopy;

bool RecvShutdownBackgroundSessionStorageManagers();
void RecvPropagateBackgroundSessionStorageManager(uint64_t aCurrentTopContextId,
                                                  uint64_t aTargetTopContextId);
bool RecvRemoveBackgroundSessionStorageManager(uint64_t aTopContextId);

bool RecvGetSessionStorageData(
    uint64_t aTopContextId, uint32_t aSizeLimit, bool aCancelSessionStoreTimer,
    ::mozilla::ipc::PBackgroundParent::GetSessionStorageManagerDataResolver&&
        aResolver);

bool RecvLoadSessionStorageData(
    uint64_t aTopContextId,
    nsTArray<mozilla::dom::SSCacheCopy>&& aCacheCopyList);

bool RecvClearStoragesForOrigin(const nsACString& aOriginAttrs,
                                const nsACString& aOriginKey);

// DomainMatchingMode is used to allow ClearStorages to mimic one of
// the LSNG behaviours - exact domain match.
// When ClearStorages is invoked with EXACT_MATCH, it clears only the
// data for a given domain, and would not affect any subdomains.
// Currently EXACT_MATCH is only passed when browser:purge-sessionStorage event
// is triggered. By default, ClearStorages is called with PREFIX_MATCH, which
// clears data for a given domain and all the subdomains. This ensures that the
// behaviour of other events that call ClearStorages remain unchanged.
enum class DomainMatchingMode { PREFIX_MATCH, EXACT_MATCH };

class BrowsingContext;
class ContentParent;
class SSSetItemInfo;
class SSWriteInfo;
class SessionStorageCache;
class SessionStorageCacheChild;
class SessionStorageManagerChild;
class SessionStorageManagerParent;
class SessionStorageObserver;
struct OriginRecord;

// sessionStorage is a data store that's unique to each tab (i.e. top-level
// browsing context) and origin. Before the advent of Fission all the data
// for a given tab could be stored in a single content process; now each
// site-specific process stores only its portion of the data. As content
// processes terminate, their sessionStorage data needs to be saved in the
// parent process, in case the same origin appears again in the tab (e.g.
// by navigating an iframe element). Therefore SessionStorageManager
// objects exist in both the parent and content processes.
//
// Whenever a write operation for SessionStorage executes, the content process
// sends the changes to the parent process at the next stable state. Whenever a
// content process navigates to an origin for the first time in a given tab, the
// parent process sends it the saved data. SessionStorageCache has a flag
// (mLoadedOrCloned) to ensure that it's only be loaded or cloned once.
//
// Note: the current implementation is expected to be replaced by a new
// implementation using LSNG.
class SessionStorageManagerBase {
 public:
  SessionStorageManagerBase() = default;

 protected:
  ~SessionStorageManagerBase() = default;

  struct OriginRecord {
    OriginRecord() = default;
    OriginRecord(OriginRecord&&) = default;
    OriginRecord& operator=(OriginRecord&&) = default;
    ~OriginRecord();

    RefPtr<SessionStorageCache> mCache;

    // A flag to ensure that cache is only loaded once.
    FlippedOnce<false> mLoaded;
  };

  void ClearStoragesInternal(
      const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
      DomainMatchingMode aMode = DomainMatchingMode::PREFIX_MATCH);

  void ClearStoragesForOriginInternal(const nsACString& aOriginAttrs,
                                      const nsACString& aOriginKey);

  OriginRecord* GetOriginRecord(const nsACString& aOriginAttrs,
                                const nsACString& aOriginKey,
                                bool aMakeIfNeeded,
                                SessionStorageCache* aCloneFrom);

  using OriginKeyHashTable = nsClassHashtable<nsCStringHashKey, OriginRecord>;
  nsClassHashtable<nsCStringHashKey, OriginKeyHashTable> mOATable;
};

class SessionStorageManager final : public SessionStorageManagerBase,
                                    public nsIDOMSessionStorageManager,
                                    public StorageObserverSink {
 public:
  explicit SessionStorageManager(RefPtr<BrowsingContext> aBrowsingContext);

  NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL
  NS_DECL_NSIDOMSTORAGEMANAGER
  NS_DECL_NSIDOMSESSIONSTORAGEMANAGER

  NS_DECL_CYCLE_COLLECTION_CLASS(SessionStorageManager)

  bool CanLoadData();

  void SetActor(SessionStorageManagerChild* aActor);

  bool ActorExists() const;

  void ClearActor();

  nsresult EnsureManager();

  nsresult LoadData(nsIPrincipal& aPrincipal, SessionStorageCache& aCache);

  void CheckpointData(nsIPrincipal& aPrincipal, SessionStorageCache& aCache);

  nsresult ClearStoragesForOrigin(const nsACString& aOriginAttrs,
                                  const nsACString& aOriginKey);

 private:
  ~SessionStorageManager();

  // StorageObserverSink, handler to various chrome clearing notification
  nsresult Observe(const char* aTopic,
                   const nsAString& aOriginAttributesPattern,
                   const nsACString& aOriginScope) override;

  nsresult GetSessionStorageCacheHelper(nsIPrincipal* aPrincipal,
                                        bool aMakeIfNeeded,
                                        SessionStorageCache* aCloneFrom,
                                        RefPtr<SessionStorageCache>* aRetVal);

  nsresult GetSessionStorageCacheHelper(const nsACString& aOriginAttrs,
                                        const nsACString& aOriginKey,
                                        bool aMakeIfNeeded,
                                        SessionStorageCache* aCloneFrom,
                                        RefPtr<SessionStorageCache>* aRetVal);

  void ClearStorages(
      const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
      DomainMatchingMode aMode = DomainMatchingMode::PREFIX_MATCH);

  SessionStorageCacheChild* EnsureCache(nsIPrincipal& aPrincipal,
                                        const nsACString& aOriginKey,
                                        SessionStorageCache& aCache);

  void CheckpointDataInternal(nsIPrincipal& aPrincipal,
                              const nsACString& aOriginKey,
                              SessionStorageCache& aCache);

  RefPtr<SessionStorageObserver> mObserver;

  RefPtr<BrowsingContext> mBrowsingContext;

  SessionStorageManagerChild* mActor;
};

/**
 * A specialized SessionStorageManager class that lives on the parent process
 * background thread. It is a shadow copy of SessionStorageManager and it's used
 * to preserve SessionStorageCaches for the other SessionStorageManagers.
 */
class BackgroundSessionStorageManager final : public SessionStorageManagerBase {
 public:
  // Parent process getter function.
  static BackgroundSessionStorageManager* GetOrCreate(uint64_t aTopContextId);

  NS_INLINE_DECL_REFCOUNTING(BackgroundSessionStorageManager);

  // Only called by CanonicalBrowsingContext::ReplaceBy on the parent process.
  static void PropagateManager(uint64_t aCurrentTopContextId,
                               uint64_t aTargetTopContextId);

  // Only called by CanonicalBrowsingContext::CanonicalDiscard on parent
  // process.
  static void RemoveManager(uint64_t aTopContextId);

  static void LoadData(
      uint64_t aTopContextId,
      const nsTArray<mozilla::dom::SSCacheCopy>& aCacheCopyList);

  using DataPromise =
      ::mozilla::ipc::PBackgroundChild::GetSessionStorageManagerDataPromise;
  static RefPtr<DataPromise> GetData(BrowsingContext* aContext,
                                     uint32_t aSizeLimit,
                                     bool aClearSessionStoreTimer = false);

  void GetData(uint32_t aSizeLimit, nsTArray<SSCacheCopy>& aCacheCopyList);

  void CopyDataToContentProcess(const nsACString& aOriginAttrs,
                                const nsACString& aOriginKey,
                                nsTArray<SSSetItemInfo>& aData);

  void UpdateData(const nsACString& aOriginAttrs, const nsACString& aOriginKey,
                  const nsTArray<SSWriteInfo>& aWriteInfos);

  void UpdateData(const nsACString& aOriginAttrs, const nsACString& aOriginKey,
                  const nsTArray<SSSetItemInfo>& aData);

  void ClearStorages(
      const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
      DomainMatchingMode aMode = DomainMatchingMode::PREFIX_MATCH);

  void ClearStoragesForOrigin(const nsACString& aOriginAttrs,
                              const nsACString& aOriginKey);

  void SetCurrentBrowsingContextId(uint64_t aBrowsingContextId);

  void MaybeDispatchSessionStoreUpdate();

  void CancelSessionStoreUpdate();

  void AddParticipatingActor(SessionStorageManagerParent* aActor);

  void RemoveParticipatingActor(SessionStorageManagerParent* aActor);

 private:
  // Only be called by GetOrCreate() on the parent process.
  explicit BackgroundSessionStorageManager(uint64_t aBrowsingContextId);

  ~BackgroundSessionStorageManager();

  // Sets a timer for notifying main thread that the cache has been
  // updated. May do nothing if we're coalescing notifications.
  void MaybeScheduleSessionStoreUpdate();

  void DispatchSessionStoreUpdate();

  // The most current browsing context using this manager
  uint64_t mCurrentBrowsingContextId;

  // Callback for notifying main thread of calls to `UpdateData`.
  //
  // A timer that is held whenever this manager has dirty state that
  // has not yet been reflected to the main thread. The timer is used
  // to delay notifying the main thread to ask for changes, thereby
  // coalescing/throttling changes. (Note that SessionStorage, like
  // LocalStorage, treats attempts to set a value to its current value
  // as a no-op.)
  //

  // The timer is initialized with a fixed delay as soon as the state
  // becomes dirty; additional mutations to our state will not reset
  // the timer because then we might never flush to the main
  // thread. The timer is cleared only when a new set of data is sent
  // to the main thread and therefore this manager no longer has any
  // dirty state. This means that there is a period of time after the
  // nsITimer fires where this value is non-null but there is no
  // scheduled timer while we wait for the main thread to request the
  // new state. Callers of GetData can also optionally cancel the
  // current timer to reduce the amounts of notifications.
  //
  // When this manager is moved to a new top-level browsing context id
  // via a PropagateBackgroundSessionStorageManager message, the
  // behavior of the timer doesn't change because the main thread knows
  // about the renaming and is initiating it (and any in-flight
  // GetSessionStorageManagerData requests will be unaffected because
  // they use async-returns so the response is inherently matched up via
  // the issued promise).
  nsCOMPtr<nsITimer> mSessionStoreCallbackTimer;

  nsTArray<RefPtr<SessionStorageManagerParent>> mParticipatingActors;
};

}  // namespace dom
}  // namespace mozilla

namespace IPC {

template <>
struct ParamTraits<mozilla::dom::DomainMatchingMode>
    : public ContiguousEnumSerializerInclusive<
          mozilla::dom::DomainMatchingMode,
          mozilla::dom::DomainMatchingMode::PREFIX_MATCH,
          mozilla::dom::DomainMatchingMode::EXACT_MATCH> {};

}  // namespace IPC

#endif  // mozilla_dom_SessionStorageManager_h
