| // Copyright 2023 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/pdf_viewer_stream_manager.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/supports_user_data.h" |
| #include "components/crash/core/common/crash_key.h" |
| #include "components/pdf/browser/pdf_frame_util.h" |
| #include "components/pdf/common/pdf_util.h" |
| #include "components/zoom/zoom_controller.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/host_zoom_map.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.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 "extensions/common/mojom/guest_view.mojom.h" |
| #include "mojo/public/cpp/bindings/associated_remote.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/blink/public/common/frame/frame_owner_element_type.h" |
| |
| namespace pdf { |
| |
| namespace { |
| |
| // Static factory instance (always nullptr for non-test). |
| PdfViewerStreamManager::Factory* g_factory = nullptr; |
| |
| // Creates a claimed `EmbedderHostInfo` from the `embedder_host`. |
| PdfViewerStreamManager::EmbedderHostInfo GetEmbedderHostInfo( |
| const content::RenderFrameHost* embedder_host) { |
| return {embedder_host->GetFrameTreeNodeId(), embedder_host->GetGlobalId()}; |
| } |
| |
| // Creates a new unclaimed `EmbedderHostInfo` for the given frame tree node ID |
| // (without the `content::GlobalRenderFrameHostId`). |
| PdfViewerStreamManager::EmbedderHostInfo GetUnclaimedEmbedderHostInfo( |
| content::FrameTreeNodeId frame_tree_node_id) { |
| return {frame_tree_node_id, content::GlobalRenderFrameHostId()}; |
| } |
| |
| // Gets the embedder host from the PDF content host's navigation handle. |
| content::RenderFrameHost* GetEmbedderHostFromPdfContentNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // Since `navigation_handle` is for a PDF content frame, the parent frame is |
| // the PDF extension frame, and the grandparent frame is the embedder frame. |
| content::RenderFrameHost* extension_host = |
| navigation_handle->GetParentFrame(); |
| CHECK(extension_host); |
| |
| return extension_host->GetParent(); |
| } |
| |
| // Gets the `extensions::mojom::MimeHandlerViewContainerManager` from the |
| // `container_host`. |
| mojo::AssociatedRemote<extensions::mojom::MimeHandlerViewContainerManager> |
| GetMimeHandlerViewContainerManager(content::RenderFrameHost* container_host) { |
| CHECK(container_host); |
| |
| mojo::AssociatedRemote<extensions::mojom::MimeHandlerViewContainerManager> |
| container_manager; |
| container_host->GetRemoteAssociatedInterfaces()->GetInterface( |
| &container_manager); |
| return container_manager; |
| } |
| |
| // Debugging data for crbug.com/391459596. |
| // TODO(crbug.com/391459596): Remove once fixed. |
| struct PdfNavigationDebugData : public base::SupportsUserData::Data { |
| bool did_start_navigation = false; |
| bool did_start_navigation_with_parent = false; |
| }; |
| |
| static int g_debug_manager_instances = 0; |
| static int g_debug_ongoing_content_navigations = 0; |
| |
| // Manager crash keys. |
| static crash_reporter::CrashKeyString<32> crash_key_manager_instances( |
| "pdf-manager-instances"); |
| static crash_reporter::CrashKeyString<32> crash_key_stream_count( |
| "pdf-stream-count"); |
| static crash_reporter::CrashKeyString<32> crash_key_ongoing_content_navigations( |
| "pdf-ongoing-content-navigations"); |
| |
| // PDF content navigation-specific crash keys. |
| static crash_reporter::CrashKeyString<6> crash_key_did_start_navigation( |
| "pdf-did-start-navigation"); |
| static crash_reporter::CrashKeyString<6> |
| crash_key_did_start_navigation_with_parent( |
| "pdf-did-start-navigation-with-parent"); |
| |
| void SetManagerCrashKeys(size_t stream_count) { |
| crash_key_manager_instances.Set(base::ToString(g_debug_manager_instances)); |
| crash_key_stream_count.Set(base::ToString(stream_count)); |
| crash_key_ongoing_content_navigations.Set( |
| base::ToString(g_debug_ongoing_content_navigations)); |
| } |
| |
| void SetContentNavigationCrashKeys( |
| const content::NavigationHandle* navigation_handle, |
| const void* key) { |
| auto* data = navigation_handle->GetUserData(key); |
| if (!data) { |
| // Can be nullptr in tests. |
| return; |
| } |
| |
| auto* debug_data = static_cast<PdfNavigationDebugData*>(data); |
| |
| crash_key_did_start_navigation.Set( |
| base::ToString(debug_data->did_start_navigation)); |
| crash_key_did_start_navigation_with_parent.Set( |
| base::ToString(debug_data->did_start_navigation_with_parent)); |
| } |
| |
| void ClearContentNavigationCrashKeys() { |
| crash_key_did_start_navigation.Clear(); |
| crash_key_did_start_navigation_with_parent.Clear(); |
| } |
| |
| } // namespace |
| |
| bool PdfViewerStreamManager::EmbedderHostInfo::operator<( |
| const PdfViewerStreamManager::EmbedderHostInfo& other) const { |
| return std::tie(frame_tree_node_id, global_id) < |
| std::tie(other.frame_tree_node_id, other.global_id); |
| } |
| |
| PdfViewerStreamManager::StreamInfo::StreamInfo( |
| const std::string& embed_internal_id, |
| std::unique_ptr<extensions::StreamContainer> stream_container) |
| : internal_id_(embed_internal_id), stream_(std::move(stream_container)) { |
| // Make sure 0 is never used because some APIs (particularly WebRequest) have |
| // special meaning for 0 IDs. |
| static int32_t next_instance_id = 0; |
| instance_id_ = ++next_instance_id; |
| } |
| |
| PdfViewerStreamManager::StreamInfo::~StreamInfo() = default; |
| |
| void PdfViewerStreamManager::StreamInfo::SetDidExtensionFinishNavigation() { |
| CHECK(!did_extension_finish_navigation_); |
| did_extension_finish_navigation_ = true; |
| } |
| |
| bool PdfViewerStreamManager::StreamInfo::DidPdfExtensionStartNavigation() |
| const { |
| return !!extension_host_frame_tree_node_id_; |
| } |
| |
| bool PdfViewerStreamManager::StreamInfo::DidPdfContentNavigate() const { |
| return container_manager_.is_bound(); |
| } |
| |
| PdfViewerStreamManager::PdfViewerStreamManager(content::WebContents* contents) |
| : content::WebContentsObserver(contents), |
| content::WebContentsUserData<PdfViewerStreamManager>(*contents) { |
| ++g_debug_manager_instances; |
| } |
| |
| PdfViewerStreamManager::~PdfViewerStreamManager() { |
| --g_debug_manager_instances; |
| } |
| |
| // static |
| void PdfViewerStreamManager::Create(content::WebContents* contents) { |
| if (FromWebContents(contents)) { |
| return; |
| } |
| |
| if (g_factory) { |
| g_factory->CreatePdfViewerStreamManager(contents); |
| } else { |
| // Using `new` to access a non-public constructor. |
| contents->SetUserData( |
| UserDataKey(), base::WrapUnique(new PdfViewerStreamManager(contents))); |
| } |
| } |
| |
| // static |
| PdfViewerStreamManager* PdfViewerStreamManager::FromRenderFrameHost( |
| content::RenderFrameHost* render_frame_host) { |
| return FromWebContents( |
| content::WebContents::FromRenderFrameHost(render_frame_host)); |
| } |
| |
| // static |
| void PdfViewerStreamManager::SetFactoryForTesting(Factory* factory) { |
| if (factory) { |
| CHECK(!g_factory); |
| } |
| g_factory = factory; |
| } |
| |
| void PdfViewerStreamManager::AddStreamContainer( |
| content::FrameTreeNodeId frame_tree_node_id, |
| const std::string& internal_id, |
| std::unique_ptr<extensions::StreamContainer> stream_container) { |
| CHECK(stream_container); |
| |
| // If an entry with the same frame tree node ID already exists in |
| // `stream_infos_`, then a new PDF navigation has occurred. If the |
| // existing `StreamInfo` hasn't been claimed, replace the entry. This is safe, |
| // since `GetStreamContainer()` verifies the original PDF URL. If the existing |
| // `StreamInfo` has been claimed, then it will eventually be deleted, and the |
| // new `StreamInfo` will be used instead. This can occur if a full page PDF |
| // viewer refreshes or navigates to another PDF URL. |
| auto embedder_host_info = GetUnclaimedEmbedderHostInfo(frame_tree_node_id); |
| stream_infos_[embedder_host_info] = |
| std::make_unique<StreamInfo>(internal_id, std::move(stream_container)); |
| } |
| |
| base::WeakPtr<extensions::StreamContainer> |
| PdfViewerStreamManager::GetStreamContainer( |
| content::RenderFrameHost* embedder_host) { |
| auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| if (!stream_info) { |
| return nullptr; |
| } |
| |
| // It's possible to have multiple `extensions::StreamContainer`s under the |
| // same frame tree node ID. Verify the original URL in the stream container to |
| // avoid a potential URL spoof. |
| if (embedder_host->GetLastCommittedURL() != |
| stream_info->stream()->original_url()) { |
| return nullptr; |
| } |
| |
| return stream_info->stream()->GetWeakPtr(); |
| } |
| |
| bool PdfViewerStreamManager::IsPdfExtensionHost( |
| const content::RenderFrameHost* render_frame_host) const { |
| // The PDF extension host should always have a parent host (the embedder |
| // host). |
| const content::RenderFrameHost* parent_host = render_frame_host->GetParent(); |
| if (!parent_host) { |
| return false; |
| } |
| |
| return IsPdfExtensionFrameTreeNodeId(parent_host, |
| render_frame_host->GetFrameTreeNodeId()); |
| } |
| |
| bool PdfViewerStreamManager::IsPdfExtensionFrameTreeNodeId( |
| const content::RenderFrameHost* embedder_host, |
| content::FrameTreeNodeId frame_tree_node_id) const { |
| const auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| return stream_info && |
| frame_tree_node_id == stream_info->extension_host_frame_tree_node_id(); |
| } |
| |
| bool PdfViewerStreamManager::DidPdfExtensionFinishNavigation( |
| const content::RenderFrameHost* embedder_host) const { |
| const auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| return stream_info && stream_info->did_extension_finish_navigation(); |
| } |
| |
| bool PdfViewerStreamManager::IsPdfContentHost( |
| const content::RenderFrameHost* render_frame_host) const { |
| // The PDF content host should always have a parent host. |
| content::RenderFrameHost* parent_host = render_frame_host->GetParent(); |
| if (!parent_host) { |
| return false; |
| } |
| |
| // The parent host should always be the PDF extension host. |
| if (!IsPdfExtensionHost(parent_host)) { |
| return false; |
| } |
| |
| // The PDF extension host should always have a parent host (the embedder |
| // host). |
| content::RenderFrameHost* embedder_host = parent_host->GetParent(); |
| CHECK(embedder_host); |
| return IsPdfContentFrameTreeNodeId(embedder_host, |
| render_frame_host->GetFrameTreeNodeId()); |
| } |
| |
| bool PdfViewerStreamManager::IsPdfContentFrameTreeNodeId( |
| const content::RenderFrameHost* embedder_host, |
| content::FrameTreeNodeId frame_tree_node_id) const { |
| const auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| return stream_info && |
| frame_tree_node_id == stream_info->content_host_frame_tree_node_id(); |
| } |
| |
| bool PdfViewerStreamManager::DidPdfContentNavigate( |
| const content::RenderFrameHost* embedder_host) const { |
| const auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| return stream_info && stream_info->DidPdfContentNavigate(); |
| } |
| |
| bool PdfViewerStreamManager::PluginCanSave( |
| const content::RenderFrameHost* embedder_host) const { |
| auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| return stream_info && stream_info->plugin_can_save(); |
| } |
| |
| void PdfViewerStreamManager::SetPluginCanSave( |
| content::RenderFrameHost* embedder_host, |
| bool plugin_can_save) { |
| auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| if (!stream_info) { |
| return; |
| } |
| |
| stream_info->set_plugin_can_save(plugin_can_save); |
| } |
| |
| bool PdfViewerStreamManager::ContainsUnclaimedStreamInfo( |
| content::FrameTreeNodeId frame_tree_node_id) const { |
| return base::Contains(stream_infos_, |
| GetUnclaimedEmbedderHostInfo(frame_tree_node_id)); |
| } |
| |
| void PdfViewerStreamManager::DeleteUnclaimedStreamInfo( |
| content::FrameTreeNodeId frame_tree_node_id) { |
| CHECK(stream_infos_.erase(GetUnclaimedEmbedderHostInfo(frame_tree_node_id))); |
| |
| if (stream_infos_.empty()) { |
| web_contents()->RemoveUserData(UserDataKey()); |
| // DO NOT add code past this point. RemoveUserData() deleted `this`. |
| } |
| } |
| |
| void PdfViewerStreamManager::RenderFrameDeleted( |
| content::RenderFrameHost* render_frame_host) { |
| // When the PDF embedder frame is deleted, delete its stream. |
| if (GetClaimedStreamInfo(render_frame_host)) { |
| DeleteClaimedStreamInfo(render_frame_host); |
| // DO NOT add code past this point. `this` may have been deleted. |
| return; |
| } |
| |
| // If `render_frame_host` isn't active, ignore. An unclaimed `StreamInfo`'s |
| // FrameTreeNode may delete a speculative `content::RenderFrameHost` before |
| // the embedder `content::RenderFrameHost` commits and claims the stream. The |
| // speculative `content::RenderFrameHost` won't be considered active, and |
| // shouldn't cause the stream to be deleted. |
| if (!render_frame_host->IsActive()) { |
| return; |
| } |
| |
| // If `render_frame_host` is an unrelated host (there isn't an unclaimed |
| // stream), ignore. |
| content::FrameTreeNodeId frame_tree_node_id = |
| render_frame_host->GetFrameTreeNodeId(); |
| if (!ContainsUnclaimedStreamInfo(frame_tree_node_id)) { |
| return; |
| } |
| |
| DeleteUnclaimedStreamInfo(frame_tree_node_id); |
| // DO NOT add code past this point. `this` may have been deleted. |
| } |
| |
| void PdfViewerStreamManager::RenderFrameHostChanged( |
| content::RenderFrameHost* old_host, |
| content::RenderFrameHost* new_host) { |
| // If the `old_host` is null, then it means that a subframe is being created. |
| // Don't treat this like a host change. |
| if (!old_host) { |
| return; |
| } |
| |
| if (MaybeDeleteStreamOnPdfExtensionHostChanged(old_host) || |
| MaybeDeleteStreamOnPdfContentHostChanged(old_host)) { |
| // DO NOT add code past this point. `this` may have been deleted. |
| return; |
| } |
| |
| // If this is an unrelated host, ignore. |
| if (!GetClaimedStreamInfo(old_host)) { |
| return; |
| } |
| |
| // The `old_host`'s `StreamInfo` should be deleted since this event could be |
| // triggered from navigating the embedder host to a non-PDF URL. If the |
| // embedder host is navigating to another PDF URL, then a new `StreamInfo` |
| // should have already been created and claimed by `new_host`, so it's still |
| // safe to delete `old_host`'s `StreamInfo`. |
| DeleteClaimedStreamInfo(old_host); |
| // DO NOT add code past this point. `this` may have been deleted. |
| } |
| |
| void PdfViewerStreamManager::FrameDeleted( |
| content::FrameTreeNodeId frame_tree_node_id) { |
| // If a PDF host is deleted, delete the associated `StreamInfo`. |
| for (auto iter = stream_infos_.begin(); iter != stream_infos_.end();) { |
| StreamInfo* stream_info = iter->second.get(); |
| // Check if `frame_tree_node_id` is a PDF host's frame tree node ID. |
| // |
| // Deleting the stream for the extension host and the content host here |
| // should be almost equivalent to how |
| // `MaybeDeleteStreamOnPdfExtensionHostChanged()` and |
| // `MaybeDeleteStreamOnPdfContentHostChanged()` delete the stream. However, |
| // there is only a frame tree node ID here and not a |
| // `content::RenderFrameHost`, so deleting the stream requires iterating |
| // over all `StreamInfo` instances. |
| if (frame_tree_node_id == iter->first.frame_tree_node_id || |
| frame_tree_node_id == |
| stream_info->extension_host_frame_tree_node_id() || |
| frame_tree_node_id == stream_info->content_host_frame_tree_node_id()) { |
| if (stream_info->mime_handler_view_container_manager()) { |
| stream_info->mime_handler_view_container_manager() |
| ->DestroyFrameContainer(stream_info->instance_id()); |
| } |
| |
| iter = stream_infos_.erase(iter); |
| } else { |
| ++iter; |
| } |
| } |
| |
| // Delete `this` if there are no remaining stream infos. |
| if (stream_infos_.empty()) { |
| web_contents()->RemoveUserData(UserDataKey()); |
| // DO NOT add code past this point. RemoveUserData() deleted `this`. |
| } |
| } |
| |
| void PdfViewerStreamManager::DidStartNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // Set the content host frame tree node ID if the navigation is for a content |
| // host. This needs to occur before the network request for the PDF content |
| // navigation so that |
| // `ChromePdfStreamDelegate::ShouldAllowPdfFrameNavigation()` can properly |
| // check that the navigation is allowed. |
| if (navigation_handle->IsPdf()) { |
| ++g_debug_ongoing_content_navigations; |
| auto debug_data = std::make_unique<PdfNavigationDebugData>(); |
| debug_data->did_start_navigation = true; |
| debug_data->did_start_navigation_with_parent = |
| navigation_handle->GetParentFrame(); |
| navigation_handle->SetUserData(UserDataKey(), std::move(debug_data)); |
| SetManagerCrashKeys(stream_infos_.size()); |
| SetContentNavigationCrashKeys(navigation_handle, UserDataKey()); |
| |
| SetStreamContentHostFrameTreeNodeId(navigation_handle); |
| } |
| } |
| |
| void PdfViewerStreamManager::ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (navigation_handle->IsPdf()) { |
| SetManagerCrashKeys(stream_infos_.size()); |
| SetContentNavigationCrashKeys(navigation_handle, UserDataKey()); |
| } |
| |
| // Maybe register a PDF subresource override in the PDF content host. |
| if (MaybeRegisterPdfSubresourceOverride(navigation_handle)) { |
| return; |
| } |
| |
| // The initial load notification for the URL being served in the embedder |
| // host. The `embedder_host` should claim the unclaimed `StreamInfo`. This |
| // should replace any existing `StreamInfo` objects related to |
| // `embedder_host`. This is safe since `GetStreamContainer()` checks the |
| // original URL for URL spoofs, and any security-relevant changes in the |
| // response should result in a different `content::RenderFrameHost`. |
| content::RenderFrameHost* embedder_host = |
| navigation_handle->GetRenderFrameHost(); |
| if (!ContainsUnclaimedStreamInfo(embedder_host->GetFrameTreeNodeId())) { |
| return; |
| } |
| |
| StreamInfo* claimed_stream_info = ClaimStreamInfo(embedder_host); |
| |
| // Set the internal ID to set up postMessage later, when the PDF content host |
| // finishes navigating. |
| auto container_manager = GetMimeHandlerViewContainerManager(embedder_host); |
| container_manager->SetInternalId(claimed_stream_info->internal_id()); |
| } |
| |
| void PdfViewerStreamManager::DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (navigation_handle->IsPdf()) { |
| --g_debug_ongoing_content_navigations; |
| SetManagerCrashKeys(stream_infos_.size()); |
| ClearContentNavigationCrashKeys(); |
| } |
| |
| // Maybe set up postMessage support after the PDF content host finishes |
| // navigating. |
| if (MaybeSetUpPostMessage(navigation_handle)) { |
| return; |
| } |
| |
| // The rest of the method handles the extension host. The parent host should |
| // be the tracked embedder host. |
| content::RenderFrameHost* embedder_host = navigation_handle->GetParentFrame(); |
| if (!embedder_host) { |
| return; |
| } |
| |
| // The `StreamInfo` should already have been claimed by the time the extension |
| // host navigates. |
| auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| if (!stream_info) { |
| return; |
| } |
| |
| // If the extension host has already started its navigation to the PDF |
| // extension URL, set the extension as finished navigating, ignoring other |
| // children of the embedder host. |
| if (stream_info->DidPdfExtensionStartNavigation()) { |
| if (stream_info->extension_host_frame_tree_node_id() == |
| navigation_handle->GetFrameTreeNodeId()) { |
| stream_info->SetDidExtensionFinishNavigation(); |
| if (navigation_handle->HasCommitted() && |
| !navigation_handle->IsErrorPage()) { |
| // Setup zoom level for the PDF extension. Zoom level 0 corresponds |
| // to zoom factor of 1, or 100%. This is done so the PDF viewer UI |
| // does not change if the page zoom does. This is analogous to page |
| // zoom not affecting the browser UI. |
| const GURL pdf_extension_url = stream_info->stream()->handler_url(); |
| CHECK_EQ(pdf_extension_url, navigation_handle->GetURL()); |
| CHECK_EQ(extensions::kExtensionScheme, pdf_extension_url.GetScheme()); |
| content::HostZoomMap::Get( |
| navigation_handle->GetRenderFrameHost()->GetSiteInstance()) |
| ->SetZoomLevelForHostAndScheme(pdf_extension_url.GetScheme(), |
| pdf_extension_url.GetHost(), 0); |
| // Set ZoomController on the extension host. |
| zoom::ZoomController::CreateForWebContentsAndRenderFrameHost( |
| web_contents(), |
| navigation_handle->GetRenderFrameHost()->GetGlobalId()); |
| } |
| } |
| return; |
| } |
| |
| // During PDF navigation, in the embedder host, an about:blank embed is |
| // inserted in a synthetic HTML document as a placeholder for the PDF |
| // extension. Navigate the about:blank embed to the PDF extension URL to load |
| // the PDF extension. |
| if (!navigation_handle->GetURL().IsAboutBlank()) { |
| return; |
| } |
| |
| content::RenderFrameHost* about_blank_host = |
| navigation_handle->GetRenderFrameHost(); |
| if (!about_blank_host) { |
| return; |
| } |
| |
| // `about_blank_host`'s FrameTreeNode will be reused for the extension |
| // `content::RenderFrameHost`, so it is safe to set it in `stream_info` to |
| // identify both hosts. |
| content::FrameTreeNodeId extension_host_frame_tree_node_id = |
| about_blank_host->GetFrameTreeNodeId(); |
| stream_info->set_extension_host_frame_tree_node_id( |
| extension_host_frame_tree_node_id); |
| |
| NavigateToPdfExtensionUrl(extension_host_frame_tree_node_id, stream_info, |
| embedder_host->GetSiteInstance(), |
| about_blank_host->GetGlobalId()); |
| } |
| |
| void PdfViewerStreamManager::ClaimStreamInfoForTesting( |
| content::RenderFrameHost* embedder_host) { |
| ClaimStreamInfo(embedder_host); |
| } |
| |
| void PdfViewerStreamManager::SetExtensionFrameTreeNodeIdForTesting( |
| content::RenderFrameHost* embedder_host, |
| content::FrameTreeNodeId frame_tree_node_id) { |
| auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| CHECK(stream_info); |
| |
| stream_info->set_extension_host_frame_tree_node_id(frame_tree_node_id); |
| } |
| |
| void PdfViewerStreamManager::SetContentFrameTreeNodeIdForTesting( |
| content::RenderFrameHost* embedder_host, |
| content::FrameTreeNodeId frame_tree_node_id) { |
| auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| CHECK(stream_info); |
| |
| stream_info->set_content_host_frame_tree_node_id(frame_tree_node_id); |
| } |
| |
| void PdfViewerStreamManager::NavigateToPdfExtensionUrl( |
| content::FrameTreeNodeId extension_host_frame_tree_node_id, |
| StreamInfo* stream_info, |
| content::SiteInstance* source_site_instance, |
| content::GlobalRenderFrameHostId global_id) { |
| CHECK(stream_info); |
| |
| content::NavigationController::LoadURLParams params( |
| stream_info->stream()->handler_url()); |
| params.frame_tree_node_id = extension_host_frame_tree_node_id; |
| params.source_site_instance = source_site_instance; |
| web_contents()->GetController().LoadURLWithParams(params); |
| } |
| |
| PdfViewerStreamManager::StreamInfo* |
| PdfViewerStreamManager::GetClaimedStreamInfo( |
| const content::RenderFrameHost* embedder_host) { |
| auto iter = stream_infos_.find(GetEmbedderHostInfo(embedder_host)); |
| return iter != stream_infos_.end() ? iter->second.get() : nullptr; |
| } |
| |
| const PdfViewerStreamManager::StreamInfo* |
| PdfViewerStreamManager::GetClaimedStreamInfo( |
| const content::RenderFrameHost* embedder_host) const { |
| auto iter = stream_infos_.find(GetEmbedderHostInfo(embedder_host)); |
| return iter != stream_infos_.end() ? iter->second.get() : nullptr; |
| } |
| |
| PdfViewerStreamManager::StreamInfo* |
| PdfViewerStreamManager::GetClaimedStreamInfoFromPdfContentNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsPdf()) { |
| return nullptr; |
| } |
| |
| // `navigation_handle` is for a PDF content frame, as checked by |
| // `NavigationHandle::IsPdf()`. |
| content::RenderFrameHost* embedder_host = |
| GetEmbedderHostFromPdfContentNavigation(navigation_handle); |
| CHECK(embedder_host); |
| |
| return GetClaimedStreamInfo(embedder_host); |
| } |
| |
| PdfViewerStreamManager::StreamInfo* PdfViewerStreamManager::ClaimStreamInfo( |
| content::RenderFrameHost* embedder_host) { |
| auto unclaimed_embedder_info = |
| GetUnclaimedEmbedderHostInfo(embedder_host->GetFrameTreeNodeId()); |
| auto iter = stream_infos_.find(unclaimed_embedder_info); |
| CHECK(iter != stream_infos_.end()); |
| |
| PdfViewerStreamManager::StreamInfo* stream_info = iter->second.get(); |
| |
| auto claimed_embedder_info = GetEmbedderHostInfo(embedder_host); |
| stream_infos_[claimed_embedder_info] = std::move(iter->second); |
| stream_infos_.erase(iter); |
| |
| return stream_info; |
| } |
| |
| void PdfViewerStreamManager::DeleteClaimedStreamInfo( |
| content::RenderFrameHost* embedder_host) { |
| auto iter = stream_infos_.find(GetEmbedderHostInfo(embedder_host)); |
| CHECK(iter != stream_infos_.end()); |
| |
| StreamInfo* stream_info = iter->second.get(); |
| if (stream_info->mime_handler_view_container_manager()) { |
| stream_info->mime_handler_view_container_manager()->DestroyFrameContainer( |
| stream_info->instance_id()); |
| } |
| |
| stream_infos_.erase(iter); |
| |
| if (stream_infos_.empty()) { |
| web_contents()->RemoveUserData(UserDataKey()); |
| // DO NOT add code past this point. RemoveUserData() deleted `this`. |
| } |
| } |
| |
| bool PdfViewerStreamManager::MaybeDeleteStreamOnPdfExtensionHostChanged( |
| content::RenderFrameHost* old_host) { |
| if (!IsPdfExtensionHost(old_host)) { |
| return false; |
| } |
| |
| // In a PDF load, the initial RFH for the PDF extension frame commits an |
| // initial about:blank URL. Don't delete the stream when this RFH changes. |
| // Another RFH will be chosen to host the PDF extension, with the PDF |
| // extension URL. |
| if (old_host->GetLastCommittedURL().IsAboutBlank()) { |
| return false; |
| } |
| |
| content::RenderFrameHost* embedder_host = old_host->GetParent(); |
| CHECK(embedder_host); |
| |
| DeleteClaimedStreamInfo(embedder_host); |
| // DO NOT add code past this point. `this` may have been deleted. |
| |
| return true; |
| } |
| |
| bool PdfViewerStreamManager::MaybeDeleteStreamOnPdfContentHostChanged( |
| content::RenderFrameHost* old_host) { |
| if (!IsPdfContentHost(old_host)) { |
| return false; |
| } |
| |
| content::RenderFrameHost* embedder_host = |
| pdf_frame_util::GetEmbedderHost(old_host); |
| CHECK(embedder_host); |
| auto* stream_info = GetClaimedStreamInfo(embedder_host); |
| |
| // In a PDF load, the initial RFH for the PDF content frame is created for the |
| // navigation to the PDF stream URL. This navigation is canceled in |
| // `pdf::PdfNavigationThrottle::WillStartRequest()` and never commits. The |
| // initial RFH and the actual PDF content RFH have the same frame tree node |
| // ID, but the actual PDF content RFH commits its navigation to the original |
| // PDF URL. Don't delete the stream when the initial RFH changes. |
| const GURL& url = old_host->GetLastCommittedURL(); |
| if (url.is_empty()) { |
| return false; |
| } |
| CHECK(url == stream_info->stream()->original_url()); |
| |
| DeleteClaimedStreamInfo(embedder_host); |
| // DO NOT add code past this point. `this` may have been deleted. |
| |
| return true; |
| } |
| |
| bool PdfViewerStreamManager::MaybeRegisterPdfSubresourceOverride( |
| content::NavigationHandle* navigation_handle) { |
| // Only register the subresource override if `navigation_handle` is for the |
| // PDF content frame. Ignore all other navigations in different frames, such |
| // as navigations in the embedder frame or PDF extension frame. |
| auto* claimed_stream_info = |
| GetClaimedStreamInfoFromPdfContentNavigation(navigation_handle); |
| if (!claimed_stream_info) { |
| return false; |
| } |
| |
| navigation_handle->RegisterSubresourceOverride( |
| claimed_stream_info->stream()->TakeTransferrableURLLoader()); |
| |
| return true; |
| } |
| |
| bool PdfViewerStreamManager::MaybeSetUpPostMessage( |
| content::NavigationHandle* navigation_handle) { |
| // Only set up postMessage if `navigation_handle` is for a PDF content frame. |
| auto* claimed_stream_info = |
| GetClaimedStreamInfoFromPdfContentNavigation(navigation_handle); |
| if (!claimed_stream_info) { |
| return false; |
| } |
| |
| // If the user reloads the PDF URL before the PDF content frame finishes |
| // loading, the initial PDF content frame navigation might reach |
| // `MaybeSetUpPostMessage()` during the new PDF load and incorrectly set up |
| // the stream info. Only continue PDF setup if the PDF content navigation is |
| // for the new PDF load. |
| if (navigation_handle->GetFrameTreeNodeId() != |
| claimed_stream_info->content_host_frame_tree_node_id()) { |
| return false; |
| } |
| |
| // `navigation_handle` is for a PDF content frame, as checked by |
| // `NavigationHandle::IsPdf()`. |
| content::RenderFrameHost* embedder_host = |
| GetEmbedderHostFromPdfContentNavigation(navigation_handle); |
| CHECK(embedder_host); |
| |
| // If `owner_type` is kEmbed or kObject, then the PDF is embedded onto another |
| // HTML page. `container_host` should be the PDF embedder host's parent. |
| // Otherwise, the PDF is full-page, in which `container_host` should be the |
| // PDF embedder host itself. |
| auto owner_type = embedder_host->GetFrameOwnerElementType(); |
| bool is_full_page = owner_type != blink::FrameOwnerElementType::kEmbed && |
| owner_type != blink::FrameOwnerElementType::kObject; |
| auto* container_host = |
| is_full_page ? embedder_host : embedder_host->GetParent(); |
| CHECK(container_host); |
| |
| auto container_manager = GetMimeHandlerViewContainerManager(container_host); |
| |
| // Set up beforeunload support for full page PDF viewer, which will also help |
| // set up postMessage support. |
| if (is_full_page) { |
| container_manager->CreateBeforeUnloadControl( |
| base::BindOnce(&PdfViewerStreamManager::SetUpBeforeUnloadControl, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| // Enable postMessage support. |
| // The first parameter for DidLoad() is |
| // mime_handler_view_guest_element_instance_id, which is used to identify and |
| // delete `extensions::MimeHandlerViewFrameContainer` objects. However, OOPIF |
| // PDF viewer doesn't have a guest element instance ID. Use the instance ID |
| // instead, which is a unique ID for `StreamInfo`. |
| container_manager->DidLoad(claimed_stream_info->instance_id(), |
| claimed_stream_info->stream()->original_url()); |
| claimed_stream_info->set_mime_handler_view_container_manager( |
| std::move(container_manager)); |
| |
| // Now that postMessage is set up, the PDF viewer has finished loading, so |
| // update metrics. |
| ReportPDFLoadStatus(embedder_host->IsInPrimaryMainFrame() |
| ? PDFLoadStatus::kLoadedFullPagePdfWithPdfium |
| : PDFLoadStatus::kLoadedEmbeddedPdfWithPdfium); |
| // TODO(b:289010799): Call `RecordPDFOpenedWithA11yFeatureWithPdfOcr`in |
| // pdf_ocr_util.cc after figuring out how to fix the build dependency issue. |
| |
| return true; |
| } |
| |
| void PdfViewerStreamManager::SetStreamContentHostFrameTreeNodeId( |
| content::NavigationHandle* navigation_handle) { |
| auto* claimed_stream_info = |
| GetClaimedStreamInfoFromPdfContentNavigation(navigation_handle); |
| CHECK(claimed_stream_info); |
| claimed_stream_info->set_content_host_frame_tree_node_id( |
| navigation_handle->GetFrameTreeNodeId()); |
| } |
| |
| void PdfViewerStreamManager::SetUpBeforeUnloadControl( |
| mojo::PendingRemote<extensions::mime_handler::BeforeUnloadControl> |
| before_unload_control_remote) { |
| // TODO(crbug.com/40268279): Currently a no-op. Support the beforeunload API. |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PdfViewerStreamManager); |
| |
| } // namespace pdf |