// 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 "chrome/browser/pdf/chrome_pdf_stream_delegate.h"

#include <optional>
#include <string>
#include <utility>

#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "chrome/browser/pdf/pdf_pref_names.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/pdf_resources.h"
#include "components/pdf/browser/pdf_stream_delegate.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/document_user_data.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest.h"
#include "extensions/browser/mime_handler/mime_handler_stream_manager.h"
#include "extensions/browser/mime_handler/stream_container.h"
#include "extensions/common/api/mime_handler.mojom.h"
#include "extensions/common/constants.h"
#include "net/http/http_response_headers.h"
#include "pdf/pdf_features.h"
#include "printing/buildflags/buildflags.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/color_palette.h"
#include "url/gurl.h"

#if BUILDFLAG(ENABLE_PRINT_PREVIEW)
#include "chrome/common/webui_url_constants.h"
#endif  // BUILDFLAG(ENABLE_PRINT_PREVIEW)

namespace {

// Determines whether the PDF viewer should use Skia renderer based on the
// user's choice, the enterprise policy and the finch experiment. The priority
// hierarchy is: enterprise policy > user choice > finch experiment.
bool ShouldEnableSkiaRenderer(content::WebContents* contents) {
  CHECK(contents);
  const PrefService* prefs =
      Profile::FromBrowserContext(contents->GetBrowserContext())->GetPrefs();

  // When the enterprise policy is set.
  if (prefs->IsManagedPreference(prefs::kPdfUseSkiaRendererEnabled)) {
    return prefs->GetBoolean(prefs::kPdfUseSkiaRendererEnabled);
  }

  //  When the enterprise policy is not set, use finch/feature flag choice.
  return base::FeatureList::IsEnabled(
      chrome_pdf::features::kPdfUseSkiaRenderer);
}

// Determines whether the PDF viewer should allow XFA forms based on the build
// flag, users' choice, the enterprise policy and the finch experiment. The
// priority hierarchy is: enterprise policy > user choice > finch experiment.
bool ShouldEnableXfaForms(content::WebContents* contents) {
  CHECK(contents);
  const PrefService* prefs =
      Profile::FromBrowserContext(contents->GetBrowserContext())->GetPrefs();

  // When the enterprise policy is set.
  if (prefs->IsManagedPreference(prefs::kPdfXfaFormsEnabled)) {
    return prefs->GetBoolean(prefs::kPdfXfaFormsEnabled);
  }

  //  When the enterprise policy is not set, use finch/feature flag choice.
  return base::FeatureList::IsEnabled(chrome_pdf::features::kPdfXfaSupport);
}

// Associates a `pdf::PdfStreamDelegate::StreamInfo` with the PDF extension's
// or Print Preview's `blink::Document`.
// `ChromePdfStreamDelegate::MapToOriginalUrl()` initializes this in
// `PdfNavigationThrottle`, and then `ChromePdfStreamDelegate::GetStreamInfo()`
// returns the stashed result to `PdfURLLoaderRequestInterceptor`.
class StreamInfoHelper : public content::DocumentUserData<StreamInfoHelper> {
 public:
  std::optional<pdf::PdfStreamDelegate::StreamInfo> TakeStreamInfo() {
    return std::move(stream_info_);
  }

 private:
  friend class content::DocumentUserData<StreamInfoHelper>;
  DOCUMENT_USER_DATA_KEY_DECL();

  StreamInfoHelper(content::RenderFrameHost* embedder_frame,
                   pdf::PdfStreamDelegate::StreamInfo stream_info)
      : content::DocumentUserData<StreamInfoHelper>(embedder_frame),
        stream_info_(std::move(stream_info)) {}

  std::optional<pdf::PdfStreamDelegate::StreamInfo> stream_info_;
};

DOCUMENT_USER_DATA_KEY_IMPL(StreamInfoHelper);

}  // namespace

ChromePdfStreamDelegate::ChromePdfStreamDelegate() = default;
ChromePdfStreamDelegate::~ChromePdfStreamDelegate() = default;

std::optional<GURL> ChromePdfStreamDelegate::MapToOriginalUrl(
    content::NavigationHandle& navigation_handle) {
  // The embedder frame's `Document` is used to store `StreamInfoHelper`.
  content::RenderFrameHost* embedder_frame = navigation_handle.GetParentFrame();

  StreamInfoHelper* helper =
      StreamInfoHelper::GetForCurrentDocument(embedder_frame);
  if (helper) {
    // PDF viewer and Print Preview only do this once per `blink::Document`.
    return std::nullopt;
  }

  GURL original_url;
  StreamInfo info;

  content::WebContents* contents = navigation_handle.GetWebContents();
  base::WeakPtr<extensions::StreamContainer> stream;
  content::RenderFrameHost* embedder_parent_frame = embedder_frame->GetParent();
  if (chrome_pdf::features::IsOopifPdfEnabled()) {
    if (embedder_parent_frame) {
      // For the PDF viewer, the `embedder_frame` is the PDF extension frame.
      // The `StreamContainer` is stored using the PDF viewer's embedder frame,
      // which is the parent of the extension frame.
      auto* mime_handler_stream_manager =
          extensions::mime_handler::MimeHandlerStreamManager::FromWebContents(
              contents);
      if (mime_handler_stream_manager) {
        stream = mime_handler_stream_manager->GetStreamContainer(
            embedder_parent_frame);
      }
    }
  } else {
    extensions::MimeHandlerViewGuest* guest =
        extensions::MimeHandlerViewGuest::FromNavigationHandle(
            &navigation_handle);
    if (guest) {
      stream = guest->GetStreamWeakPtr();
    }
  }

  const GURL& stream_url = navigation_handle.GetURL();
  if (stream) {
    if (stream->extension_id() != extension_misc::kPdfExtensionId ||
        stream->stream_url() != stream_url ||
        !stream->pdf_plugin_attributes()) {
      return std::nullopt;
    }

    CHECK_EQ(embedder_frame->GetLastCommittedURL().GetHost(),
             extension_misc::kPdfExtensionId);

    original_url = stream->original_url();
    info.background_color = base::checked_cast<SkColor>(
        stream->pdf_plugin_attributes()->background_color);
    info.full_frame = !stream->embedded();
    info.allow_javascript = stream->pdf_plugin_attributes()->allow_javascript;
    info.use_skia = ShouldEnableSkiaRenderer(contents);
    info.allow_xfa_forms = ShouldEnableXfaForms(contents);
    if (chrome_pdf::features::IsOopifPdfEnabled()) {
      net::HttpResponseHeaders* response_headers = stream->response_headers();
      if (response_headers) {
        std::optional<std::string> coep_header =
            response_headers->GetNormalizedHeader(
                "Cross-Origin-Embedder-Policy");
        if (coep_header.has_value()) {
          info.coep_header = coep_header.value();
        }
      }
    }
#if BUILDFLAG(ENABLE_PRINT_PREVIEW)
  } else if (stream_url.GetWithEmptyPath() ==
             chrome::kChromeUIUntrustedPrintURL) {
    CHECK_EQ(embedder_frame->GetLastCommittedURL().GetHost(),
             chrome::kChromeUIPrintHost);

    // Print Preview doesn't have access to `chrome.mimeHandlerPrivate`, so just
    // use values that match those set by `PDFViewerPPElement`.
    original_url = stream_url;
    info.background_color = gfx::kGoogleGrey300;
    info.full_frame = false;
    info.allow_javascript = false;
    info.use_skia = ShouldEnableSkiaRenderer(contents);
    info.allow_xfa_forms = false;
#endif  // BUILDFLAG(ENABLE_PRINT_PREVIEW)
  } else {
    return std::nullopt;
  }

  static const base::NoDestructor<std::string> injected_script(
      ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
          IDR_PDF_PDF_INTERNAL_PLUGIN_WRAPPER_ROLLUP_JS));

  info.stream_url = stream_url;
  info.original_url = original_url;
  info.injected_script = injected_script.get();

  StreamInfoHelper::CreateForCurrentDocument(embedder_frame, std::move(info));
  return original_url;
}

std::optional<pdf::PdfStreamDelegate::StreamInfo>
ChromePdfStreamDelegate::GetStreamInfo(
    content::RenderFrameHost* embedder_frame) {
  if (!embedder_frame) {
    return std::nullopt;
  }

  StreamInfoHelper* helper =
      StreamInfoHelper::GetForCurrentDocument(embedder_frame);
  if (!helper) {
    return std::nullopt;
  }

  // Only the call immediately following `MapToOriginalUrl()` requires a valid
  // `StreamInfo`; subsequent calls should just get nothing.
  return helper->TakeStreamInfo();
}

bool ChromePdfStreamDelegate::MaybeDeleteSandboxedStream(
    content::FrameTreeNodeId frame_tree_node_id) {
  CHECK(chrome_pdf::features::IsOopifPdfEnabled());

  auto* web_contents =
      content::WebContents::FromFrameTreeNodeId(frame_tree_node_id);
  if (!web_contents) {
    return false;
  }

  // Only delete if a stream exists. The stream should always be unclaimed,
  // since the navigation hasn't committed.
  auto* mime_handler_stream_manager =
      extensions::mime_handler::MimeHandlerStreamManager::FromWebContents(
          web_contents);
  if (!mime_handler_stream_manager ||
      !mime_handler_stream_manager->ContainsUnclaimedStreamInfo(
          frame_tree_node_id)) {
    return false;
  }

  mime_handler_stream_manager->DeleteUnclaimedStreamInfo(frame_tree_node_id);
  return true;
}

bool ChromePdfStreamDelegate::ShouldAllowPdfExtensionFrameNavigation(
    content::NavigationHandle* navigation_handle) {
  // If PdfOopif is enabled, or if this is an about:blank navigation, allow it.
  if (chrome_pdf::features::IsOopifPdfEnabled() ||
      navigation_handle->GetURL().IsAboutBlank()) {
    return true;
  }

  // Verify this is a guest, otherwise allow the navigation to proceed.
  auto* guest =
      extensions::MimeHandlerViewGuest::FromNavigationHandle(navigation_handle);
  if (!guest) {
    return true;
  }

  // Since this is the PDF delegate, don't suppress navigations for other stream
  // types (should they exist).
  base::WeakPtr<extensions::StreamContainer> stream = guest->GetStreamWeakPtr();
  if (!stream || stream->extension_id() != extension_misc::kPdfExtensionId) {
    return true;
  }

  return url::IsSameOriginWith(stream->handler_url(),
                               navigation_handle->GetURL());
}

bool ChromePdfStreamDelegate::ShouldAllowPdfFrameNavigation(
    content::NavigationHandle* navigation_handle) {
  // Blocks any non-setup navigations in the PDF extension frame and the PDF
  // content frame.

  // OOPIF PDF viewer only.
  if (!chrome_pdf::features::IsOopifPdfEnabled()) {
    return true;
  }

  auto* mime_handler_stream_manager =
      extensions::mime_handler::MimeHandlerStreamManager::FromWebContents(
          navigation_handle->GetWebContents());
  if (!mime_handler_stream_manager) {
    return true;
  }

  // The parent frame should always exist after main frame navigations are
  // filtered out in `PdfNavigationThrottle::WillStartRequest()`. The
  // parent frame could be the PDF extension frame, PDF embedder frame, or an
  // unrelated frame.
  content::RenderFrameHost* parent_frame = navigation_handle->GetParentFrame();
  CHECK(parent_frame);

  const GURL& url = navigation_handle->GetURL();

  // If `parent_frame` is the PDF embedder frame and thus has an
  // `extensions::StreamContainer`, then the current frame could be the PDF
  // extension frame.
  base::WeakPtr<extensions::StreamContainer> stream =
      mime_handler_stream_manager->GetStreamContainer(parent_frame);
  content::FrameTreeNodeId frame_tree_node_id =
      navigation_handle->GetFrameTreeNodeId();
  if (stream) {
    // Allow navigations for unrelated frames, which might be injected by
    // unrelated extensions. Only allow the MIME handler extension frame to
    // navigate to the extension URL once.
    return !mime_handler_stream_manager->IsExtensionFrameTreeNodeId(
               parent_frame, frame_tree_node_id) ||
           (!mime_handler_stream_manager->DidExtensionFrameFinishNavigation(
                parent_frame) &&
            url == stream->handler_url());
  }

  // If this navigation is for a content frame, then there should be a
  // grandparent frame (the embedder frame) with a stream container. If this
  // navigation is unrelated to MIME handlers, then there may or may not be a
  // grandparent frame, and there will not be a stream container. In that case,
  // the navigation should not be blocked.
  content::RenderFrameHost* grandparent_frame = parent_frame->GetParent();
  if (!grandparent_frame) {
    return true;
  }
  stream = mime_handler_stream_manager->GetStreamContainer(grandparent_frame);
  if (!stream) {
    return true;
  }

  // Allow navigations for unrelated frames, which might be injected by
  // unrelated extensions. Only allow the content frame to navigate to the
  // original URL once.
  return !mime_handler_stream_manager->IsContentFrameTreeNodeId(
             grandparent_frame, frame_tree_node_id) ||
         (!mime_handler_stream_manager->DidContentFrameFinishNavigation(
              grandparent_frame) &&
          url == stream->original_url());
}
