/* 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 "mozilla/net/DocumentChannel.h"

#include <inttypes.h>
#include "mozIDOMWindow.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Assertions.h"
#include "mozilla/LoadInfo.h"
#include "mozilla/Logging.h"
#include "mozilla/RefPtr.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/ParentProcessChannelHandle.h"
#include "mozilla/net/DocumentChannelChild.h"
#include "mozilla/net/ParentProcessDocumentChannel.h"
#include "nsCOMPtr.h"
#include "nsDebug.h"
#include "nsDocShell.h"
#include "nsDocShellLoadState.h"
#include "nsHttpHandler.h"
#include "nsIContentPolicy.h"
#include "nsIInterfaceRequestor.h"
#include "nsILoadContext.h"
#include "nsILoadGroup.h"
#include "nsILoadInfo.h"
#include "nsIStreamListener.h"
#include "nsIURI.h"
#include "nsLoadGroup.h"
#include "nsMimeTypes.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "nsPIDOMWindowInlines.h"
#include "nsStringFwd.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"
#include "nscore.h"

using namespace mozilla::dom;
using namespace mozilla::ipc;

extern mozilla::LazyLogModule gDocumentChannelLog;
#define LOG(fmt) MOZ_LOG(gDocumentChannelLog, mozilla::LogLevel::Verbose, fmt)

namespace mozilla {
namespace net {

//-----------------------------------------------------------------------------
// DocumentChannel::nsISupports

NS_IMPL_ADDREF(DocumentChannel)
NS_IMPL_RELEASE(DocumentChannel)

NS_INTERFACE_MAP_BEGIN(DocumentChannel)
  NS_INTERFACE_MAP_ENTRY(nsIRequest)
  NS_INTERFACE_MAP_ENTRY(nsIChannel)
  NS_INTERFACE_MAP_ENTRY(nsIIdentChannel)
  NS_INTERFACE_MAP_ENTRY_CONCRETE(DocumentChannel)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRequest)
NS_INTERFACE_MAP_END

DocumentChannel::DocumentChannel(nsDocShellLoadState* aLoadState,
                                 net::LoadInfo* aLoadInfo,
                                 nsLoadFlags aLoadFlags, uint32_t aCacheKey,
                                 bool aUriModified,
                                 bool aIsEmbeddingBlockedError)
    : mLoadState(aLoadState),
      mCacheKey(aCacheKey),
      mLoadFlags(aLoadFlags),
      mURI(aLoadState->URI()),
      mLoadInfo(aLoadInfo),
      mUriModified(aUriModified),
      mIsEmbeddingBlockedError(aIsEmbeddingBlockedError) {
  LOG(("DocumentChannel ctor [this=%p, uri=%s]", this,
       aLoadState->URI()->GetSpecOrDefault().get()));
  RefPtr<nsHttpHandler> handler = nsHttpHandler::GetInstance();
  mChannelId = handler->NewChannelId();
}

DocumentChannel::~DocumentChannel() = default;

NS_IMETHODIMP
DocumentChannel::AsyncOpen(nsIStreamListener* aListener) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

void DocumentChannel::ShutdownListeners(nsresult aStatusCode) {
  LOG(("DocumentChannel ShutdownListeners [this=%p, status=%" PRIx32 "]", this,
       static_cast<uint32_t>(aStatusCode)));
  mStatus = aStatusCode;

  nsCOMPtr<nsIStreamListener> listener = mListener;
  if (listener) {
    listener->OnStartRequest(this);
  }

  mIsPending = false;

  listener = mListener;  // it might have changed!
  nsCOMPtr<nsILoadGroup> loadGroup = mLoadGroup;

  mListener = nullptr;
  mLoadGroup = nullptr;
  mCallbacks = nullptr;

  NS_DispatchToMainThread(NS_NewRunnableFunction(
      "DocumentChannel::ShutdownListeners", [=, self = RefPtr{this}] {
        if (listener) {
          listener->OnStopRequest(self, aStatusCode);
        }

        if (loadGroup) {
          loadGroup->RemoveRequest(self, nullptr, aStatusCode);
        }
      }));

  DeleteIPDL();
}

void DocumentChannel::DisconnectChildListeners(
    const nsresult& aStatus, const nsresult& aLoadGroupStatus) {
  MOZ_ASSERT(NS_FAILED(aStatus));

  // In the case where the channel was redirected to be downloaded by the
  // nsExternalHelperAppService in the parent process, we'll be called with a
  // different aLoadGroupStatus and aStatus.
  //
  // Before DocumentChannel was implemented, the channel would have been removed
  // from the load group by the nsExternalHelperAppService. This simulates that
  // behaviour by removing the load group when the channel is no longer in use.
  //
  // We cannot unconditionally remove the channel from the load group here, as
  // that may unblock load events too early if new navigations will be started
  // by channel listeners. See bug 1961008.
  if (aStatus != aLoadGroupStatus) {
    MOZ_ASSERT(aStatus == NS_BINDING_RETARGETED);
    MOZ_ASSERT(NS_SUCCEEDED(aLoadGroupStatus));

    mStatus = aLoadGroupStatus;
    if (mLoadGroup) {
      mLoadGroup->RemoveRequest(this, nullptr, aStatus);
      mLoadGroup = nullptr;
    }
  }

  ShutdownListeners(aStatus);
}

nsDocShell* DocumentChannel::GetDocShell() {
  nsCOMPtr<nsILoadContext> loadContext;
  NS_QueryNotificationCallbacks(this, loadContext);
  if (!loadContext) {
    return nullptr;
  }
  nsCOMPtr<mozIDOMWindowProxy> domWindow;
  loadContext->GetAssociatedWindow(getter_AddRefs(domWindow));
  if (!domWindow) {
    return nullptr;
  }
  auto* pDomWindow = nsPIDOMWindowOuter::From(domWindow);
  nsIDocShell* docshell = pDomWindow->GetDocShell();
  return nsDocShell::Cast(docshell);
}

static bool URIUsesDocChannel(nsIURI* aURI) {
  if (aURI->SchemeIs("javascript")) {
    return false;
  }

  nsCString spec = aURI->GetSpecOrDefault();
  return
#ifdef MOZ_WIDGET_ANDROID
      !spec.EqualsLiteral("about:crashcontentjava") &&
#endif
      !spec.EqualsLiteral("about:crashcontent");
}

bool DocumentChannel::CanUseDocumentChannel(nsIURI* aURI) {
  // We want to use DocumentChannel if we're using a supported scheme.
  return URIUsesDocChannel(aURI);
}

/* static */
already_AddRefed<DocumentChannel> DocumentChannel::CreateForDocument(
    nsDocShellLoadState* aLoadState, class LoadInfo* aLoadInfo,
    nsLoadFlags aLoadFlags, nsIInterfaceRequestor* aNotificationCallbacks,
    uint32_t aCacheKey, bool aUriModified, bool aIsEmbeddingBlockedError) {
  RefPtr<DocumentChannel> channel;
  if (XRE_IsContentProcess()) {
    channel =
        new DocumentChannelChild(aLoadState, aLoadInfo, aLoadFlags, aCacheKey,
                                 aUriModified, aIsEmbeddingBlockedError);
  } else {
    channel = new ParentProcessDocumentChannel(
        aLoadState, aLoadInfo, aLoadFlags, aCacheKey, aUriModified,
        aIsEmbeddingBlockedError);
  }
  channel->SetNotificationCallbacks(aNotificationCallbacks);
  return channel.forget();
}

/* static */
already_AddRefed<DocumentChannel> DocumentChannel::CreateForObject(
    nsDocShellLoadState* aLoadState, class LoadInfo* aLoadInfo,
    nsLoadFlags aLoadFlags, nsIInterfaceRequestor* aNotificationCallbacks) {
  return CreateForDocument(aLoadState, aLoadInfo, aLoadFlags,
                           aNotificationCallbacks, 0, false, false);
}

NS_IMETHODIMP DocumentChannel::SetCanceledReason(const nsACString& aReason) {
  return SetCanceledReasonImpl(aReason);
}

NS_IMETHODIMP DocumentChannel::GetCanceledReason(nsACString& aReason) {
  return GetCanceledReasonImpl(aReason);
}

NS_IMETHODIMP DocumentChannel::CancelWithReason(nsresult aStatus,
                                                const nsACString& aReason) {
  return CancelWithReasonImpl(aStatus, aReason);
}

NS_IMETHODIMP
DocumentChannel::Cancel(nsresult aStatusCode) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
DocumentChannel::Suspend() {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
DocumentChannel::Resume() {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

//-----------------------------------------------------------------------------
// Remainder of nsIRequest/nsIChannel.
//-----------------------------------------------------------------------------

NS_IMETHODIMP DocumentChannel::GetNotificationCallbacks(
    nsIInterfaceRequestor** aCallbacks) {
  nsCOMPtr<nsIInterfaceRequestor> callbacks(mCallbacks);
  callbacks.forget(aCallbacks);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::SetNotificationCallbacks(
    nsIInterfaceRequestor* aNotificationCallbacks) {
  mCallbacks = aNotificationCallbacks;
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) {
  nsCOMPtr<nsILoadGroup> loadGroup(mLoadGroup);
  loadGroup.forget(aLoadGroup);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) {
  mLoadGroup = aLoadGroup;
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetStatus(nsresult* aStatus) {
  *aStatus = mStatus;
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetName(nsACString& aResult) {
  if (!mURI) {
    aResult.Truncate();
    return NS_OK;
  }
  nsCString spec;
  nsresult rv = mURI->GetSpec(spec);
  NS_ENSURE_SUCCESS(rv, rv);

  aResult.AssignLiteral("documentchannel:");
  aResult.Append(spec);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::IsPending(bool* aResult) {
  *aResult = mIsPending;
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) {
  *aLoadFlags = mLoadFlags;
  return NS_OK;
}

NS_IMETHODIMP
DocumentChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
  return GetTRRModeImpl(aTRRMode);
}

NS_IMETHODIMP
DocumentChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
  return SetTRRModeImpl(aTRRMode);
}

NS_IMETHODIMP DocumentChannel::SetLoadFlags(nsLoadFlags aLoadFlags) {
  nsLoadFlags mayChange = 0;
  if (mLoadInfo->GetExternalContentPolicyType() ==
      ExtContentPolicy::TYPE_OBJECT) {
    // Setting load flags for TYPE_OBJECT is OK, so long as the channel to
    // parent isn't opened yet, or we're only setting the `LOAD_DOCUMENT_URI`
    // flag.
    mayChange = mWasOpened ? LOAD_DOCUMENT_URI : ~0u;
  } else if (!mWasOpened) {
    // If we haven't been opened yet, allow the LoadGroup to
    // set cache control flags inherited from the default channel.
    mayChange = nsLoadGroup::kInheritedLoadFlags;
  }

  // Check if we're allowed to adjust these flags.
  if ((mLoadFlags & ~mayChange) == (aLoadFlags & ~mayChange)) {
    mLoadFlags = aLoadFlags;
    return NS_OK;
  }
  MOZ_CRASH_UNSAFE_PRINTF(
      "DocumentChannel::SetLoadFlags: Don't set flags after creation "
      "(differing flags %x != %x)",
      (mLoadFlags ^ aLoadFlags) & mLoadFlags,
      (mLoadFlags ^ aLoadFlags) & aLoadFlags);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetOriginalURI(nsIURI** aOriginalURI) {
  nsCOMPtr<nsIURI> originalURI =
      mLoadState->OriginalURI() ? mLoadState->OriginalURI() : mLoadState->URI();
  originalURI.forget(aOriginalURI);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::SetOriginalURI(nsIURI* aOriginalURI) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetURI(nsIURI** aURI) {
  nsCOMPtr<nsIURI> uri(mURI);
  uri.forget(aURI);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetOwner(nsISupports** aOwner) {
  nsCOMPtr<nsISupports> owner(mOwner);
  owner.forget(aOwner);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::SetOwner(nsISupports* aOwner) {
  mOwner = aOwner;
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetSecurityInfo(
    nsITransportSecurityInfo** aSecurityInfo) {
  *aSecurityInfo = nullptr;
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetContentType(nsACString& aContentType) {
  // We may be trying to load HTML object data, and have determined that we're
  // going to be performing a document load. In that case, fake the "text/html"
  // content type for nsObjectLoadingContent.
  if ((mLoadFlags & nsIRequest::LOAD_HTML_OBJECT_DATA) &&
      (mLoadFlags & nsIChannel::LOAD_DOCUMENT_URI)) {
    aContentType = TEXT_HTML;
    return NS_OK;
  }

  NS_ERROR("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::SetContentType(const nsACString& aContentType) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetContentCharset(nsACString& aContentCharset) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::SetContentCharset(
    const nsACString& aContentCharset) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetContentLength(int64_t* aContentLength) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::SetContentLength(int64_t aContentLength) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::Open(nsIInputStream** aStream) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetContentDisposition(
    uint32_t* aContentDisposition) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::SetContentDisposition(
    uint32_t aContentDisposition) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetContentDispositionFilename(
    nsAString& aContentDispositionFilename) {
  MOZ_CRASH("If we get here, something will be broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::SetContentDispositionFilename(
    const nsAString& aContentDispositionFilename) {
  MOZ_CRASH("If we get here, something will be broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetContentDispositionHeader(
    nsACString& aContentDispositionHeader) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) {
  nsCOMPtr<nsILoadInfo> loadInfo(mLoadInfo);
  loadInfo.forget(aLoadInfo);
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) {
  MOZ_CRASH("If we get here, something is broken");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP DocumentChannel::GetParentProcessChannelHandle(
    dom::ParentProcessChannelHandle** aValue) {
  *aValue = do_AddRef(mParentProcessChannelHandle).take();
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::SetParentProcessChannelHandle(
    dom::ParentProcessChannelHandle* aValue) {
  if (XRE_IsParentProcess()) {
    MOZ_ASSERT_UNREACHABLE(
        "SetParentProcessChannelHandle in the parent process would leak");
    return NS_ERROR_NOT_AVAILABLE;
  }

  mParentProcessChannelHandle = aValue;
  return NS_OK;
}

NS_IMETHODIMP DocumentChannel::GetIsDocument(bool* aIsDocument) {
  return NS_GetIsDocumentChannel(this, aIsDocument);
}

NS_IMETHODIMP DocumentChannel::GetCanceled(bool* aCanceled) {
  *aCanceled = mCanceled;
  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsIIdentChannel
//-----------------------------------------------------------------------------

NS_IMETHODIMP
DocumentChannel::GetChannelId(uint64_t* aChannelId) {
  *aChannelId = mChannelId;
  return NS_OK;
}

NS_IMETHODIMP
DocumentChannel::SetChannelId(uint64_t aChannelId) {
  mChannelId = aChannelId;
  return NS_OK;
}

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

uint64_t InnerWindowIDForExtantDoc(nsDocShell* docShell) {
  if (!docShell) {
    return 0;
  }

  Document* doc = docShell->GetExtantDocument();
  if (!doc) {
    return 0;
  }

  return doc->InnerWindowID();
}

}  // namespace net
}  // namespace mozilla

#undef LOG
