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

#include "ClientNavigateOpChild.h"

#include "ClientState.h"
#include "ClientSource.h"
#include "ClientSourceChild.h"
#include "mozilla/dom/Document.h"
#include "mozilla/Unused.h"
#include "nsIDocShell.h"
#include "nsDocShellLoadState.h"
#include "nsIWebNavigation.h"
#include "nsIWebProgress.h"
#include "nsIWebProgressListener.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "nsURLHelper.h"
#include "ReferrerInfo.h"

namespace mozilla::dom {

namespace {

class NavigateLoadListener final : public nsIWebProgressListener,
                                   public nsSupportsWeakReference {
  RefPtr<ClientOpPromise::Private> mPromise;
  RefPtr<nsPIDOMWindowOuter> mOuterWindow;
  nsCOMPtr<nsIURI> mBaseURL;

  ~NavigateLoadListener() = default;

 public:
  NavigateLoadListener(ClientOpPromise::Private* aPromise,
                       nsPIDOMWindowOuter* aOuterWindow, nsIURI* aBaseURL)
      : mPromise(aPromise), mOuterWindow(aOuterWindow), mBaseURL(aBaseURL) {
    MOZ_DIAGNOSTIC_ASSERT(mPromise);
    MOZ_DIAGNOSTIC_ASSERT(mOuterWindow);
    MOZ_DIAGNOSTIC_ASSERT(mBaseURL);
  }

  NS_IMETHOD
  OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
                uint32_t aStateFlags, nsresult aResult) override {
    if (!(aStateFlags & STATE_IS_DOCUMENT) ||
        !(aStateFlags & (STATE_STOP | STATE_TRANSFERRING))) {
      return NS_OK;
    }

    aWebProgress->RemoveProgressListener(this);

    nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
    if (!channel) {
      // This is not going to happen; how could it?
      CopyableErrorResult result;
      result.ThrowInvalidStateError("Bad request");
      mPromise->Reject(result, __func__);
      return NS_OK;
    }

    nsCOMPtr<nsIURI> channelURL;
    nsresult rv = NS_GetFinalChannelURI(channel, getter_AddRefs(channelURL));
    if (NS_FAILED(rv)) {
      CopyableErrorResult result;
      // XXXbz We can't actually get here; NS_GetFinalChannelURI never fails in
      // practice!
      result.Throw(rv);
      mPromise->Reject(result, __func__);
      return NS_OK;
    }

    nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
    MOZ_DIAGNOSTIC_ASSERT(ssm);

    // If the resulting window is not same origin, then resolve immediately
    // without returning any information about the new Client.  This is
    // step 6.10 in the Client.navigate(url) spec.
    // todo: if you intend to update CheckSameOriginURI to log the error to the
    // console you also need to update the 'aFromPrivateWindow' argument.
    rv = ssm->CheckSameOriginURI(mBaseURL, channelURL, false, false);
    if (NS_FAILED(rv)) {
      mPromise->Resolve(CopyableErrorResult(), __func__);
      return NS_OK;
    }

    nsPIDOMWindowInner* innerWindow = mOuterWindow->GetCurrentInnerWindow();
    MOZ_DIAGNOSTIC_ASSERT(innerWindow);

    Maybe<ClientInfo> clientInfo = innerWindow->GetClientInfo();
    MOZ_DIAGNOSTIC_ASSERT(clientInfo.isSome());

    Maybe<ClientState> clientState = innerWindow->GetClientState();
    MOZ_DIAGNOSTIC_ASSERT(clientState.isSome());

    // Otherwise, if the new window is same-origin we want to return a
    // ClientInfoAndState object so we can provide a Client snapshot
    // to the caller.  This is step 6.11 and 6.12 in the Client.navigate(url)
    // spec.
    mPromise->Resolve(
        ClientInfoAndState(clientInfo.ref().ToIPC(), clientState.ref().ToIPC()),
        __func__);

    return NS_OK;
  }

  NS_IMETHOD
  OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
                   int32_t aCurSelfProgress, int32_t aMaxSelfProgress,
                   int32_t aCurTotalProgress,
                   int32_t aMaxTotalProgress) override {
    MOZ_CRASH("Unexpected notification.");
    return NS_OK;
  }

  NS_IMETHOD
  OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
                   nsIURI* aLocation, uint32_t aFlags) override {
    MOZ_CRASH("Unexpected notification.");
    return NS_OK;
  }

  NS_IMETHOD
  OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
                 nsresult aStatus, const char16_t* aMessage) override {
    MOZ_CRASH("Unexpected notification.");
    return NS_OK;
  }

  NS_IMETHOD
  OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
                   uint32_t aState) override {
    MOZ_CRASH("Unexpected notification.");
    return NS_OK;
  }

  NS_IMETHOD
  OnContentBlockingEvent(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
                         uint32_t aEvent) override {
    MOZ_CRASH("Unexpected notification.");
    return NS_OK;
  }

  NS_DECL_ISUPPORTS
};

NS_IMPL_ISUPPORTS(NavigateLoadListener, nsIWebProgressListener,
                  nsISupportsWeakReference);

}  // anonymous namespace

RefPtr<ClientOpPromise> ClientNavigateOpChild::DoNavigate(
    const ClientNavigateOpConstructorArgs& aArgs) {
  nsCOMPtr<nsPIDOMWindowInner> window;

  // Navigating the target client window will result in the original
  // ClientSource being destroyed.  To avoid potential UAF mistakes
  // we use a small scope to access the ClientSource object.  Once
  // we have a strong reference to the window object we should not
  // access the ClientSource again.
  {
    ClientSourceChild* targetActor =
        static_cast<ClientSourceChild*>(aArgs.target().AsChild().get());
    MOZ_DIAGNOSTIC_ASSERT(targetActor);

    ClientSource* target = targetActor->GetSource();
    if (!target) {
      CopyableErrorResult rv;
      rv.ThrowInvalidStateError("Unknown Client");
      return ClientOpPromise::CreateAndReject(rv, __func__);
    }

    window = target->GetInnerWindow();
    if (!window) {
      CopyableErrorResult rv;
      rv.ThrowInvalidStateError("Client load for a destroyed Window");
      return ClientOpPromise::CreateAndReject(rv, __func__);
    }
  }

  MOZ_ASSERT(NS_IsMainThread());

  mSerialEventTarget = window->EventTargetFor(TaskCategory::Other);

  // In theory we could do the URL work before paying the IPC overhead
  // cost, but in practice its easier to do it here.  The ClientHandle
  // may be off-main-thread while this method is guaranteed to always
  // be main thread.
  nsCOMPtr<nsIURI> baseURL;
  nsresult rv = NS_NewURI(getter_AddRefs(baseURL), aArgs.baseURL());
  if (NS_FAILED(rv)) {
    // This is rather unexpected: This is the worker URL we passed from the
    // parent, so we expect this to parse fine!
    CopyableErrorResult result;
    result.ThrowInvalidStateError("Invalid worker URL");
    return ClientOpPromise::CreateAndReject(result, __func__);
  }

  // There is an edge case for view-source url here. According to the wpt test
  // windowclient-navigate.https.html, a view-source URL with a relative inner
  // URL should be treated as an invalid URL. However, we will still resolve it
  // into a valid view-source URL since the baseURL is involved while creating
  // the URI. So, an invalid view-source URL will be treated as a valid URL
  // in this case. To address this, we should not take the baseURL into account
  // for the view-source URL.
  bool shouldUseBaseURL = true;
  nsAutoCString scheme;
  if (NS_SUCCEEDED(net_ExtractURLScheme(aArgs.url(), scheme)) &&
      scheme.LowerCaseEqualsLiteral("view-source")) {
    shouldUseBaseURL = false;
  }

  nsCOMPtr<nsIURI> url;
  rv = NS_NewURI(getter_AddRefs(url), aArgs.url(), nullptr,
                 shouldUseBaseURL ? baseURL.get() : nullptr);
  if (NS_FAILED(rv)) {
    // Per https://w3c.github.io/ServiceWorker/#dom-windowclient-navigate step
    // 2, if the URL fails to parse, we reject with a TypeError.
    nsPrintfCString err("Invalid URL \"%s\"", aArgs.url().get());
    CopyableErrorResult result;
    result.ThrowTypeError(err);
    return ClientOpPromise::CreateAndReject(result, __func__);
  }

  if (url->GetSpecOrDefault().EqualsLiteral("about:blank")) {
    CopyableErrorResult result;
    result.ThrowTypeError("Navigation to \"about:blank\" is not allowed");
    return ClientOpPromise::CreateAndReject(result, __func__);
  }

  RefPtr<Document> doc = window->GetExtantDoc();
  if (!doc || !doc->IsActive()) {
    CopyableErrorResult result;
    result.ThrowInvalidStateError("Document is not active.");
    return ClientOpPromise::CreateAndReject(result, __func__);
  }

  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();

  nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
  nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell);
  if (!docShell || !webProgress) {
    CopyableErrorResult result;
    result.ThrowInvalidStateError(
        "Document's browsing context has been discarded");
    return ClientOpPromise::CreateAndReject(result, __func__);
  }

  RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(url);
  loadState->SetTriggeringPrincipal(principal);
  loadState->SetTriggeringSandboxFlags(doc->GetSandboxFlags());
  loadState->SetCsp(doc->GetCsp());

  auto referrerInfo = MakeRefPtr<ReferrerInfo>(*doc);
  loadState->SetReferrerInfo(referrerInfo);
  loadState->SetLoadType(LOAD_STOP_CONTENT);
  loadState->SetSourceBrowsingContext(docShell->GetBrowsingContext());
  loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE);
  loadState->SetFirstParty(true);
  loadState->SetHasValidUserGestureActivation(
      doc->HasValidTransientUserGestureActivation());
  rv = docShell->LoadURI(loadState, false);
  if (NS_FAILED(rv)) {
    /// There are tests that try sending file:/// and mixed-content URLs
    /// in here and expect them to reject with a TypeError.  This does not match
    /// the spec, but does match the current behavior of both us and Chrome.
    /// https://github.com/w3c/ServiceWorker/issues/1500 tracks sorting that
    /// out.
    /// We now run security checks asynchronously, so these tests now
    /// just fail to load rather than hitting this failure path. I've
    /// marked them as failing for now until they get fixed to match the
    /// spec.
    nsPrintfCString err("Invalid URL \"%s\"", aArgs.url().get());
    CopyableErrorResult result;
    result.ThrowTypeError(err);
    return ClientOpPromise::CreateAndReject(result, __func__);
  }

  RefPtr<ClientOpPromise::Private> promise =
      new ClientOpPromise::Private(__func__);

  nsCOMPtr<nsIWebProgressListener> listener =
      new NavigateLoadListener(promise, window->GetOuterWindow(), baseURL);

  rv = webProgress->AddProgressListener(listener,
                                        nsIWebProgress::NOTIFY_STATE_DOCUMENT);
  if (NS_FAILED(rv)) {
    CopyableErrorResult result;
    // XXXbz Can we throw something better here?
    result.Throw(rv);
    promise->Reject(result, __func__);
    return promise;
  }

  return promise->Then(
      mSerialEventTarget, __func__,
      [listener](const ClientOpPromise::ResolveOrRejectValue& aValue) {
        return ClientOpPromise::CreateAndResolveOrReject(aValue, __func__);
      });
}

void ClientNavigateOpChild::ActorDestroy(ActorDestroyReason aReason) {
  mPromiseRequestHolder.DisconnectIfExists();
}

void ClientNavigateOpChild::Init(const ClientNavigateOpConstructorArgs& aArgs) {
  RefPtr<ClientOpPromise> promise = DoNavigate(aArgs);

  // Normally we get the event target from the window in DoNavigate().  If a
  // failure occurred, though, we may need to fall back to the current thread
  // target.
  if (!mSerialEventTarget) {
    mSerialEventTarget = GetCurrentSerialEventTarget();
  }

  // Capturing `this` is safe here since we clear the mPromiseRequestHolder in
  // ActorDestroy.
  promise
      ->Then(
          mSerialEventTarget, __func__,
          [this](const ClientOpResult& aResult) {
            mPromiseRequestHolder.Complete();
            PClientNavigateOpChild::Send__delete__(this, aResult);
          },
          [this](const CopyableErrorResult& aResult) {
            mPromiseRequestHolder.Complete();
            PClientNavigateOpChild::Send__delete__(this, aResult);
          })
      ->Track(mPromiseRequestHolder);
}

}  // namespace mozilla::dom
