// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/navigation_interception/intercept_navigation_delegate.h"

#include <memory>

#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/strings/escape.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/page_visibility_state.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/url_request/redirect_info.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/parsed_headers.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/single_request_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/android/gurl_android.h"
#include "url/gurl.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/navigation_interception/jni_headers/InterceptNavigationDelegate_jni.h"

using base::android::ConvertUTF8ToJavaString;
using base::android::ScopedJavaLocalRef;
using content::BrowserThread;
using content::RenderViewHost;
using content::WebContents;
using ui::PageTransition;

namespace navigation_interception {

namespace {

const void* const kInterceptNavigationDelegateUserDataKey =
    &kInterceptNavigationDelegateUserDataKey;

void AllowNavigationToProceed(
    content::NavigationHandle* navigation_handle,
    bool should_run_async,
    InterceptNavigationThrottle::ResultCallback result_callback) {
  std::move(result_callback).Run(false);
}

// This wrapper is necessary because `InterceptNavigationThrottle` can outlive
// `InterceptNavigationDelegate` (e.g., during prerender cancellation).
// If we bound `ShouldIgnoreNavigation` directly to a `WeakPtr`,
// `base::RepeatingCallback` would silently drop the call when the `WeakPtr`
// is invalidated. This would destroy `result_callback` without running it,
// stalling the navigation in `DEFER`. The wrapper detects the dead `WeakPtr`
// and safely invokes `result_callback(false)` to allow continuation.
void ShouldIgnoreNavigationWrapper(
    base::WeakPtr<InterceptNavigationDelegate> delegate,
    content::NavigationHandle* navigation_handle,
    bool should_run_async,
    InterceptNavigationThrottle::ResultCallback result_callback) {
  if (!delegate) {
    std::move(result_callback).Run(false);
    return;
  }
  delegate->ShouldIgnoreNavigation(navigation_handle, should_run_async,
                                   std::move(result_callback));
}

class RedirectURLLoader : public network::mojom::URLLoader {
 public:
  RedirectURLLoader(const network::ResourceRequest& resource_request,
                    mojo::PendingRemote<network::mojom::URLLoaderClient> client)
      : client_(std::move(client)), request_(resource_request) {}

  void DoRedirect(std::unique_ptr<GURL> url) {
    net::HttpStatusCode response_code = net::HTTP_TEMPORARY_REDIRECT;
    auto response_head = network::mojom::URLResponseHead::New();
    response_head->encoded_data_length = 0;
    response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
        net::HttpUtil::AssembleRawHeaders("HTTP/1.1 307 Temporary Redirect"));

    // Avoid a round-trip to the network service by pre-parsing headers.
    // This doesn't violate: `docs/security/rule-of-2.md`, because the input is
    // trusted, before appending the Location: <url> header.
    response_head->parsed_headers =
        network::PopulateParsedHeaders(response_head->headers.get(), *url);

    response_head->headers->AddHeader("Location", url->spec());

    auto first_party_url_policy =
        request_.update_first_party_url_on_redirect
            ? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT
            : net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL;

    client_->OnReceiveRedirect(
        net::RedirectInfo::ComputeRedirectInfo(
            request_.method, request_.url, request_.site_for_cookies,
            first_party_url_policy, request_.referrer_policy,
            request_.referrer.spec(), request_.request_initiator, response_code,
            *url, std::nullopt,
            /*insecure_scheme_was_upgraded=*/false,
            /*copy_fragment=*/false),
        std::move(response_head));
  }

  void OnNonRedirectAsyncAction() {
    client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_ABORTED));
  }

  RedirectURLLoader(const RedirectURLLoader&) = delete;
  RedirectURLLoader& operator=(const RedirectURLLoader&) = delete;

  ~RedirectURLLoader() override = default;

 private:
  // network::mojom::URLLoader overrides:
  void FollowRedirect(
      network::HttpRequestHeadersUpdateParams headers_update_params,
      const std::optional<GURL>& new_url) override {
    NOTREACHED();
  }
  void SetPriority(net::RequestPriority priority,
                   int intra_priority_value) override {}

  mojo::Remote<network::mojom::URLLoaderClient> client_;
  network::ResourceRequest request_;
};

}  // namespace

// static
void InterceptNavigationDelegate::Associate(
    WebContents* web_contents,
    std::unique_ptr<InterceptNavigationDelegate> delegate) {
  if (!delegate) {
    web_contents->RemoveUserData(kInterceptNavigationDelegateUserDataKey);
  } else {
    web_contents->SetUserData(kInterceptNavigationDelegateUserDataKey,
                              std::move(delegate));
  }
}

// static
InterceptNavigationDelegate* InterceptNavigationDelegate::Get(
    WebContents* web_contents) {
  return static_cast<InterceptNavigationDelegate*>(
      web_contents->GetUserData(kInterceptNavigationDelegateUserDataKey));
}

// static
void InterceptNavigationDelegate::MaybeCreateAndAdd(
    content::NavigationThrottleRegistry& registry,
    navigation_interception::SynchronyMode mode) {
  // Navigations in a subframe or non-primary frame tree should not be
  // intercepted. As examples of a non-primary frame tree, a navigation
  // occurring in a Portal element or an unactivated prerendering page should
  // not launch an app.
  // TODO(bokan): This is a bit of a stopgap approach since we won't run
  // throttles again when the prerender is activated which means links that are
  // prerendered will avoid launching an app intent that a regular navigation
  // would have. Longer term we'll want prerender activation to check for app
  // intents, or have this throttle cancel the prerender if an intent would
  // have been launched (without launching the intent). It's also not clear
  // what the right behavior for <portal> elements is.
  // https://crbug.com/1227659.
  if (!registry.GetNavigationHandle().IsInPrimaryMainFrame()) {
    return;
  }

  InterceptNavigationDelegate* intercept_navigation_delegate =
      InterceptNavigationDelegate::Get(
          registry.GetNavigationHandle().GetWebContents());

  if (!intercept_navigation_delegate) {
    registry.AddThrottle(std::make_unique<InterceptNavigationThrottle>(
        registry, base::BindRepeating(&AllowNavigationToProceed), mode,
        base::DoNothing()));
  } else {
    registry.AddThrottle(std::make_unique<InterceptNavigationThrottle>(
        registry,
        base::BindRepeating(&ShouldIgnoreNavigationWrapper,
                            intercept_navigation_delegate->GetWeakPtr()),
        mode,
        base::BindRepeating(
            &InterceptNavigationDelegate::RequestFinishPendingShouldIgnoreCheck,
            intercept_navigation_delegate->GetWeakPtr())));
  }
}

InterceptNavigationDelegate::InterceptNavigationDelegate(
    JNIEnv* env,
    const jni_zero::JavaRef<jobject>& jdelegate,
    bool escape_external_handler_value)
    : escape_external_handler_value_(escape_external_handler_value) {
  CHECK(!jdelegate.is_null());
  Java_InterceptNavigationDelegate_associateNative(
      env, jdelegate, reinterpret_cast<intptr_t>(this));
}

InterceptNavigationDelegate::InterceptNavigationDelegate() = default;

InterceptNavigationDelegate::~InterceptNavigationDelegate() {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_InterceptNavigationDelegate_destroyFromNative(
      env, reinterpret_cast<intptr_t>(this));
}

void InterceptNavigationDelegate::ShouldIgnoreNavigation(
    content::NavigationHandle* navigation_handle,
    bool should_run_async,
    InterceptNavigationThrottle::ResultCallback result_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Avoid having two outstanding checks at once for simplicity.
  if (should_ignore_result_callback_) {
    std::move(result_callback).Run(false);
    return;
  }
  GURL escaped_url = escape_external_handler_value_
                         ? GURL(base::EscapeExternalHandlerValue(
                               navigation_handle->GetURL().spec()))
                         : navigation_handle->GetURL();

  if (!escaped_url.is_valid()) {
    std::move(result_callback).Run(false);
    return;
  }

  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> jdelegate = GetJavaDelegate(env);

  if (jdelegate.is_null()) {
    std::move(result_callback).Run(false);
    return;
  }

  bool hidden_cross_frame = false;
  // Only main frame navigations use this path, so we only need to check if the
  // navigation is cross-frame to the main frame.
  // Also, if this is the first non-empty document, allow the navigation as we
  // only care about preventing spoofing of a site performing a navigation it
  // didn't initiate.
  if (!navigation_handle->IsNavigatingFromInitialEmptyDocument() &&
      navigation_handle->GetInitiatorFrameToken() &&
      navigation_handle->GetInitiatorFrameToken() !=
          navigation_handle->GetWebContents()
              ->GetPrimaryMainFrame()
              ->GetFrameToken()) {
    content::RenderFrameHost* initiator_frame_host =
        content::RenderFrameHost::FromFrameToken(
            content::GlobalRenderFrameHostToken(
                navigation_handle->GetInitiatorProcessId(),
                navigation_handle->GetInitiatorFrameToken().value()));
    // If the initiator is gone treat it as not visible.
    hidden_cross_frame =
        !initiator_frame_host || initiator_frame_host->GetVisibilityState() !=
                                     content::PageVisibilityState::kVisible;
  }

  // We don't care which sandbox flags are present, only that any sandbox flags
  // are present, as we don't support persisting sandbox flags through fallback
  // URL navigation.
  bool is_sandboxed = navigation_handle->SandboxFlagsInherited() !=
                          network::mojom::WebSandboxFlags::kNone ||
                      navigation_handle->SandboxFlagsInitiator() !=
                          network::mojom::WebSandboxFlags::kNone;

  should_ignore_result_callback_ = std::move(result_callback);
  Java_InterceptNavigationDelegate_callShouldIgnoreNavigation(
      env, jdelegate, navigation_handle->GetJavaNavigationHandle(),
      url::GURLAndroid::FromNativeGURL(env, escaped_url), hidden_cross_frame,
      is_sandboxed, should_run_async);
}

void InterceptNavigationDelegate::OnShouldIgnoreNavigationResult(
    bool should_ignore) {
  std::move(should_ignore_result_callback_).Run(should_ignore);
}

void InterceptNavigationDelegate::RequestFinishPendingShouldIgnoreCheck() {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> jdelegate = GetJavaDelegate(env);

  if (jdelegate.is_null()) {
    OnShouldIgnoreNavigationResult(false);
    return;
  }
  Java_InterceptNavigationDelegate_requestFinishPendingShouldIgnoreCheck(
      env, jdelegate);
}

void InterceptNavigationDelegate::HandleSubframeExternalProtocol(
    const GURL& url,
    ui::PageTransition page_transition,
    bool has_user_gesture,
    const std::optional<url::Origin>& initiating_origin,
    mojo::PendingRemote<network::mojom::URLLoaderFactory>* out_factory) {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> jdelegate = GetJavaDelegate(env);

  if (jdelegate.is_null()) {
    return;
  }

  // We don't support checking for subframes and main frames in parallel. Try to
  // finish the main frame check.
  if (should_ignore_result_callback_) {
    Java_InterceptNavigationDelegate_requestFinishPendingShouldIgnoreCheck(
        env, jdelegate);
    // If we are still doing a main frame check, block the subframe check.
    if (should_ignore_result_callback_) {
      return;
    }
  }

  // If there's a pending async subframe action, don't consider external
  // navigation for the current navigation.
  if (subframe_redirect_url_ || url_loader_) {
    return;
  }

  GURL escaped_url = escape_external_handler_value_
                         ? GURL(base::EscapeExternalHandlerValue(url.spec()))
                         : url;
  if (!escaped_url.is_valid()) {
    return;
  }

  ScopedJavaLocalRef<jobject> j_gurl =
      Java_InterceptNavigationDelegate_handleSubframeExternalProtocol(
          env, jdelegate, url::GURLAndroid::FromNativeGURL(env, escaped_url),
          page_transition, has_user_gesture,
          initiating_origin ? initiating_origin->ToJavaObject(env) : nullptr);
  if (j_gurl.is_null()) {
    return;
  }
  subframe_redirect_url_ =
      std::make_unique<GURL>(url::GURLAndroid::ToNativeGURL(env, j_gurl));

  mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver =
      out_factory->InitWithNewPipeAndPassReceiver();
  scoped_refptr<network::SharedURLLoaderFactory> loader_factory =
      base::MakeRefCounted<network::SingleRequestURLLoaderFactory>(
          base::BindOnce(&InterceptNavigationDelegate::LoaderCallback,
                         weak_ptr_factory_.GetWeakPtr()));
  loader_factory->Clone(std::move(receiver));
}

void InterceptNavigationDelegate::LoaderCallback(
    const network::ResourceRequest& resource_request,
    mojo::PendingReceiver<network::mojom::URLLoader> pending_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderClient> pending_client) {
  url_loader_ = mojo::MakeSelfOwnedReceiver(
      std::make_unique<RedirectURLLoader>(resource_request,
                                          std::move(pending_client)),
      std::move(pending_receiver));
  MaybeHandleSubframeAction();
}

void InterceptNavigationDelegate::MaybeHandleSubframeAction() {
  // An empty subframe_redirect_url_ implies a pending async action.
  if (!url_loader_ ||
      (subframe_redirect_url_ && subframe_redirect_url_->is_empty())) {
    return;
  }
  RedirectURLLoader* loader =
      static_cast<RedirectURLLoader*>(url_loader_->impl());
  if (!subframe_redirect_url_) {
    loader->OnNonRedirectAsyncAction();
  } else {
    loader->DoRedirect(std::move(subframe_redirect_url_));
  }
  url_loader_.reset();
}

void InterceptNavigationDelegate::OnResourceRequestWithGesture() {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> jdelegate = GetJavaDelegate(env);
  if (jdelegate.is_null()) {
    return;
  }
  Java_InterceptNavigationDelegate_onResourceRequestWithGesture(env, jdelegate);
}

void InterceptNavigationDelegate::OnSubframeAsyncActionTaken(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& j_gurl) {
  // subframe_redirect_url_ no longer empty indicates the async action has been
  // taken.
  subframe_redirect_url_ =
      j_gurl.is_null()
          ? nullptr
          : std::make_unique<GURL>(url::GURLAndroid::ToNativeGURL(env, j_gurl));
  MaybeHandleSubframeAction();
}

base::android::ScopedJavaLocalRef<jobject>
InterceptNavigationDelegate::GetJavaDelegate(JNIEnv* env) {
  return Java_InterceptNavigationDelegate_get(env,
                                              reinterpret_cast<intptr_t>(this));
}

static void JNI_InterceptNavigationDelegate_OnShouldIgnoreNavigationResult(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& jweb_contents,
    bool should_ignore) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(jweb_contents);
  if (!web_contents) {
    return;
  }
  navigation_interception::InterceptNavigationDelegate* delegate =
      navigation_interception::InterceptNavigationDelegate::Get(web_contents);
  CHECK(delegate);
  delegate->OnShouldIgnoreNavigationResult(should_ignore);
}

}  // namespace navigation_interception

DEFINE_JNI(InterceptNavigationDelegate)
