// Copyright 2021 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/pdf/browser/pdf_navigation_throttle.h"

#include <memory>
#include <optional>
#include <utility>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "components/pdf/browser/pdf_stream_delegate.h"
#include "components/pdf/common/constants.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/web_contents.h"
#include "net/http/http_response_headers.h"
#include "pdf/pdf_features.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"

namespace pdf {

content::NavigationThrottle::ThrottleCheckResult
PdfNavigationThrottle::WillProcessResponse() {
  // OOPIF PDF viewer only.
  if (!chrome_pdf::features::IsOopifPdfEnabled()) {
    return PROCEED;
  }

  const net::HttpResponseHeaders* response_headers =
      navigation_handle()->GetResponseHeaders();
  if (!response_headers) {
    return PROCEED;
  }

  std::string mime_type;
  response_headers->GetMimeType(&mime_type);
  if (mime_type != kPDFMimeType) {
    return PROCEED;
  }

  // Fenced frames should not be able to load PDFs.
  bool is_sandboxed_pdf = (navigation_handle()->SandboxFlagsToCommit() &
                           network::mojom::WebSandboxFlags::kPlugins) !=
                          network::mojom::WebSandboxFlags::kNone;
  if (!is_sandboxed_pdf) {
    return PROCEED;
  }

  // If there is a PDF stream, then the navigation is for a PDF in a sandboxed
  // iframe and should be canceled. Otherwise, the navigation is for PDFs that
  // are not meant to be viewed inline (e.g. downloads) and should be allowed to
  // proceed.
  if (!stream_delegate_->MaybeDeleteSandboxedStream(
          navigation_handle()->GetFrameTreeNodeId())) {
    return PROCEED;
  }
  return ThrottleCheckResult(CANCEL, net::ERR_BLOCKED_BY_CLIENT);
}

PdfNavigationThrottle::PdfNavigationThrottle(
    content::NavigationThrottleRegistry& registry,
    std::unique_ptr<PdfStreamDelegate> stream_delegate)
    : content::NavigationThrottle(registry),
      stream_delegate_(std::move(stream_delegate)) {
  DCHECK(stream_delegate_);
}

PdfNavigationThrottle::~PdfNavigationThrottle() = default;

const char* PdfNavigationThrottle::GetNameForLogging() {
  return "PdfNavigationThrottle";
}

content::NavigationThrottle::ThrottleCheckResult
PdfNavigationThrottle::WillStartRequest() {
  // Intercepts navigations to a PDF stream URL in a PDF content frame and
  // re-navigates to the original PDF URL.

  // The main frame may contain the PDF extension for non-PdfOopif cases; it
  // should never be navigated away from the extension.
  if (navigation_handle()->IsInMainFrame()) {
    return stream_delegate_->ShouldAllowPdfExtensionFrameNavigation(
               navigation_handle())
               ? PROCEED
               : BLOCK_REQUEST;
  }

  // Skip unless navigating to the stream URL.
  const std::optional<GURL> original_url =
      stream_delegate_->MapToOriginalUrl(*navigation_handle());
  if (!original_url.has_value()) {
    // Block any non-PDF navigations in internal PDF extension and content
    // frames. Allow all other navigations to proceed.
    return stream_delegate_->ShouldAllowPdfFrameNavigation(navigation_handle())
               ? PROCEED
               : BLOCK_REQUEST;
  }

  // Uses the same pattern as `PDFIFrameNavigationThrottle` to redirect
  // navigation to the original URL. We'll use this to navigate to the correct
  // origin, while `PdfURLLoaderRequestInterceptor` will intercept the request
  // and replace its content.
  content::OpenURLParams params =
      content::OpenURLParams::FromNavigationHandle(navigation_handle());
  params.url = original_url.value();
  params.transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME;
  params.is_renderer_initiated = false;
  params.is_pdf = true;

  // Mark that the initiator policies should not be inherited by the content.
  // Otherwise the PDF blob might inherit the security policies of the initiator
  // of the navigation such as CSP.
  params.should_ignore_initiator_policies_for_inheritance = true;

  // The parent frame should always exist after main frame navigations are
  // filtered out at the beginning of this method, and it has the expected
  // embedder URL based on the checks in
  // `PdfStreamDelegate::MapToOriginalUrl()`. For the PDF viewer, the parent
  // frame is the PDF extension frame. For Print Preview, the parent frame is
  // the embedder frame.
  content::RenderFrameHost* embedder_frame =
      navigation_handle()->GetParentFrame();
  CHECK(embedder_frame);

  // Reset the source SiteInstance.  This is a workaround for a lifetime bug:
  // leaving the source SiteInstance in OpenURLParams could inadvertently
  // prolong the SiteInstance's lifetime beyond the lifetime of the
  // BrowserContext it's associated with.  The BrowserContext could get
  // destroyed after the task below is scheduled but before it runs (see
  // https://crbug.com/1382761), and even though the task checks if the frame is
  // null to return early in that case, the task's OpenURLParams would only get
  // destroyed and decrement the source SiteInstance's refcount at the time of
  // that early return, which is already after the BrowserContext is destroyed.
  // This can cause logic in the SiteInstance destructor to trip up if it tries
  // to use the SiteInstance's BrowserContext.
  //
  // The source SiteInstance of this navigation should always be SiteInstance of
  // the parent frame's, which is the embedder frame. Hence, if the navigation
  // task does run and does not get canceled due to the embedder frame becoming
  // null, we can restore the source SiteInstance at that point.
  //
  // TODO(crbug.com/40061670): This should be fixed in a more systematic way.
  DCHECK_EQ(params.source_site_instance, embedder_frame->GetSiteInstance());
  params.source_site_instance.reset();

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](content::GlobalRenderFrameHostId frame_id,
             const content::OpenURLParams& params) {
            auto* embedder_frame = content::RenderFrameHost::FromID(frame_id);
            if (!embedder_frame || !embedder_frame->IsActive()) {
              return;
            }

            // Restore the source SiteInstance that was cleared out of the
            // original OpenURLParams.
            content::OpenURLParams new_params = params;
            new_params.source_site_instance = embedder_frame->GetSiteInstance();

            // `MimeHandlerViewGuest` navigates its embedder for calls to
            // `WebContents::OpenURL()`, so use `LoadURLWithParams()` directly
            // instead.
            embedder_frame->GetController().LoadURLWithParams(
                content::NavigationController::LoadURLParams(new_params));

            // Note that we don't need to register the stream's URL loader as a
            // subresource, as `MimeHandlerViewGuest::ReadyToCommitNavigation()`
            // will handle this as soon as we navigate to a
            // non-`kPdfExtensionId` URL.
          },
          embedder_frame->GetGlobalId(), std::move(params)));
  return CANCEL_AND_IGNORE;
}

}  // namespace pdf
