/* 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_net_EarlyHintPreloader_h
#define mozilla_net_EarlyHintPreloader_h

#include "mozilla/dom/ipc/IdType.h"
#include "mozilla/Maybe.h"
#include "mozilla/PreloadHashKey.h"
#include "NeckoCommon.h"
#include "mozilla/net/NeckoChannelParams.h"
#include "nsHashtablesFwd.h"
#include "nsIChannelEventSink.h"
#include "nsIInterfaceRequestor.h"
#include "nsIMultiPartChannel.h"
#include "nsIRedirectResultListener.h"
#include "nsIStreamListener.h"
#include "nsITimer.h"
#include "nsNetUtil.h"

class nsAttrValue;
class nsICookieJarSettings;
class nsILoadContext;
class nsIPrincipal;
class nsIReferrerInfo;

namespace mozilla::dom {
class CanonicalBrowsingContext;
}

namespace mozilla::net {

class EarlyHintPreloader;
class EarlyHintConnectArgs;
class ParentChannelListener;
struct LinkHeader;

// class keeping track of all ongoing early hints
class OngoingEarlyHints final {
 public:
  NS_INLINE_DECL_REFCOUNTING(OngoingEarlyHints)

  OngoingEarlyHints() = default;

  // returns whether a preload with that key already existed
  bool Contains(const PreloadHashKey& aKey);
  bool Add(const PreloadHashKey& aKey, RefPtr<EarlyHintPreloader> aPreloader);

  void CancelAll(const nsACString& aReason);

  // registers all channels and returns the ids
  void RegisterLinksAndGetConnectArgs(
      dom::ContentParentId aCpId, nsTArray<EarlyHintConnectArgs>& aOutLinks);

 private:
  ~OngoingEarlyHints() = default;

  // We need to do two things requiring two separate variables to keep track of
  // preloads:
  //  - deduplicate Link headers when starting preloads, therefore we store them
  //    hashset with PreloadHashKey to look up whether we started the preload
  //    already
  //  - pass link headers in order they were received when passing all started
  //    preloads to the content process, therefore we store them in a nsTArray
  nsTHashSet<PreloadHashKey> mStartedPreloads;
  nsTArray<RefPtr<EarlyHintPreloader>> mPreloaders;
};

class EarlyHintPreloader final : public nsIStreamListener,
                                 public nsIChannelEventSink,
                                 public nsIRedirectResultListener,
                                 public nsIInterfaceRequestor,
                                 public nsIMultiPartChannelListener,
                                 public nsINamed,
                                 public nsITimerCallback {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIREQUESTOBSERVER
  NS_DECL_NSISTREAMLISTENER
  NS_DECL_NSICHANNELEVENTSINK
  NS_DECL_NSIREDIRECTRESULTLISTENER
  NS_DECL_NSIINTERFACEREQUESTOR
  NS_DECL_NSIMULTIPARTCHANNELLISTENER
  // required by NS_DECL_NSITIMERCALLBACK
  NS_DECL_NSINAMED
  NS_DECL_NSITIMERCALLBACK

 public:
  // Create and insert a preload into OngoingEarlyHints if the same preload
  // wasn't already issued and the LinkHeader can be parsed correctly.
  static void MaybeCreateAndInsertPreload(
      OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aHeader,
      nsIURI* aBaseURI, nsIPrincipal* aPrincipal,
      nsICookieJarSettings* aCookieJarSettings,
      const nsACString& aReferrerPolicy, const nsACString& aCSPHeader,
      uint64_t aBrowsingContextID,
      dom::CanonicalBrowsingContext* aLoadingBrowsingContext,
      bool aIsModulepreload);

  // register Channel to EarlyHintRegistrar. Returns true and sets connect args
  // if successful
  bool Register(dom::ContentParentId aCpId, EarlyHintConnectArgs& aOut);

  // Allows EarlyHintRegistrar to check if the correct content process accesses
  // this preload. Preventing compromised content processes to access Early Hint
  // preloads from other origins
  bool IsFromContentParent(dom::ContentParentId aCpId) const;

  // Should be called by the preloader service when the preload is not
  // needed after all, because the final response returns a non-2xx status
  // code. If aDeleteEntry is false, the calling function MUST make sure that
  // the EarlyHintPreloader is not in the EarlyHintRegistrar anymore. Because
  // after this function, the EarlyHintPreloader can't connect back to the
  // parent anymore.
  nsresult CancelChannel(nsresult aStatus, const nsACString& aReason,
                         bool aDeleteEntry);

  void OnParentReady(nsIParentChannel* aParent);

 private:
  void SetParentChannel();
  void InvokeStreamListenerFunctions();

  EarlyHintPreloader();
  ~EarlyHintPreloader();

  static Maybe<PreloadHashKey> GenerateHashKey(ASDestination aAs, nsIURI* aURI,
                                               nsIPrincipal* aPrincipal,
                                               CORSMode corsMode,
                                               bool aIsModulepreload);

  static nsSecurityFlags ComputeSecurityFlags(CORSMode aCORSMode,
                                              ASDestination aAs);

  // call to start the preload
  nsresult OpenChannel(nsIURI* aURI, nsIPrincipal* aPrincipal,
                       nsSecurityFlags aSecurityFlags,
                       nsContentPolicyType aContentPolicyType,
                       nsIReferrerInfo* aReferrerInfo,
                       nsICookieJarSettings* aCookieJarSettings,
                       uint64_t aBrowsingContextID);
  void PriorizeAsPreload();
  void SetLinkHeader(const LinkHeader& aLinkHeader);

  nsCOMPtr<nsIChannel> mChannel;
  nsCOMPtr<nsIChannel> mRedirectChannel;

  dom::ContentParentId mCpId;
  EarlyHintConnectArgs mConnectArgs;

  // Copy behavior from DocumentLoadListener.h:
  // https://searchfox.org/mozilla-central/rev/c0bed29d643393af6ebe77aa31455f283f169202/netwerk/ipc/DocumentLoadListener.h#487-512
  // The set of nsIStreamListener functions that got called on this
  // listener, so that we can replay them onto the replacement channel's
  // listener. This should generally only be OnStartRequest, since we
  // Suspend() the channel at that point, but it can fail sometimes
  // so we have to support holding a list.
  nsTArray<StreamListenerFunction> mStreamListenerFunctions;

  // Set to true once OnStartRequest is called
  bool mOnStartRequestCalled = false;
  // Set to true if we suspended mChannel in the OnStartRequest call
  bool mSuspended = false;
  nsCOMPtr<nsIParentChannel> mParent;
  // Set to true after we've received the last OnStopRequest, and shouldn't
  // setup a reference from the ParentChannelListener to the replacement
  // channel.
  bool mIsFinished = false;

  RefPtr<ParentChannelListener> mParentListener;
  nsCOMPtr<nsITimer> mTimer;

  // Hold the load context to provide data to web extension and anti tracking.
  // See Bug 1836289 and Bug 1875268
  nsCOMPtr<nsILoadContext> mLoadContext;

 private:
  // IMPORTANT: when adding new values, always add them to the end, otherwise
  // it will mess up telemetry.
  enum EHPreloaderState : uint32_t {
    ePreloaderCreated = 0,
    ePreloaderOpened,
    ePreloaderUsed,
    ePreloaderCancelled,
    ePreloaderTimeout,
  };
  EHPreloaderState mState = ePreloaderCreated;
  void SetState(EHPreloaderState aState) { mState = aState; }
};

inline nsISupports* ToSupports(EarlyHintPreloader* aObj) {
  return static_cast<nsIInterfaceRequestor*>(aObj);
}

}  // namespace mozilla::net

#endif  // mozilla_net_EarlyHintPreloader_h
