blob: c390a83277f564f1a67a7dcffa36b9d77a35bc0b [file] [log] [blame]
// 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/pdf/pdf_viewer_stream_manager.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/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);
}
// 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* pdf_viewer_stream_manager =
pdf::PdfViewerStreamManager::FromWebContents(contents);
if (pdf_viewer_stream_manager) {
stream = pdf_viewer_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().host(),
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);
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().host(),
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);
#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* pdf_viewer_stream_manager =
pdf::PdfViewerStreamManager::FromWebContents(web_contents);
if (!pdf_viewer_stream_manager ||
!pdf_viewer_stream_manager->ContainsUnclaimedStreamInfo(
frame_tree_node_id)) {
return false;
}
pdf_viewer_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* pdf_viewer_stream_manager =
pdf::PdfViewerStreamManager::FromWebContents(
navigation_handle->GetWebContents());
if (!pdf_viewer_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 =
pdf_viewer_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 PDF extension frame to navigate to
// the extension URL once.
return !pdf_viewer_stream_manager->IsPdfExtensionFrameTreeNodeId(
parent_frame, frame_tree_node_id) ||
(!pdf_viewer_stream_manager->DidPdfExtensionFinishNavigation(
parent_frame) &&
url == stream->handler_url());
}
// If this navigation is for a PDF content frame, then there should be a
// grandparent frame (the PDF embedder frame) with a stream container. If this
// navigation is unrelated to PDFs, 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 = pdf_viewer_stream_manager->GetStreamContainer(grandparent_frame);
if (!stream) {
return true;
}
// Allow navigations for unrelated frames, which might be injected by
// unrelated extensions. Only allow the PDF content frame to navigate to the
// original PDF URL once.
return !pdf_viewer_stream_manager->IsPdfContentFrameTreeNodeId(
grandparent_frame, frame_tree_node_id) ||
(!pdf_viewer_stream_manager->DidPdfContentNavigate(
grandparent_frame) &&
url == stream->original_url());
}