| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_attach_helper.h" |
| |
| #include "base/bind.h" |
| #include "base/containers/flat_map.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/no_destructor.h" |
| #include "base/task/post_task.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.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/render_process_host.h" |
| #include "content/public/browser/site_instance.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/common/mime_handler_view_mode.h" |
| #include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest.h" |
| #include "extensions/common/guest_view/extensions_guest_view_messages.h" |
| #include "services/service_manager/public/cpp/interface_provider.h" |
| |
| using content::BrowserThread; |
| using content::NavigationHandle; |
| using content::NavigationThrottle; |
| using content::RenderFrameHost; |
| using content::SiteInstance; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // TODO(ekaramad): Make this a proper resource (https://crbug.com/659750). |
| // TODO(ekaramad): Should we use an <embed>? Verify if this causes issues with |
| // post messaging support for PDF viewer (https://crbug.com/659750). |
| const char kFullPageMimeHandlerViewHTML[] = |
| "<!doctype html><html><body style='height: 100%; width: 100%; overflow: " |
| "hidden; margin:0px; background-color: rgb(82, 86, 89);'><iframe " |
| "style='position:absolute; left: 0; top: 0;'width='100%' height='100%'" |
| "></iframe></body></html>"; |
| const uint32_t kFullPageMimeHandlerViewDataPipeSize = 256U; |
| |
| // Arbitrary delay to quit attaching the MimeHandlerViewGuest's WebContents to |
| // the outer WebContents if no about:blank navigation is committed. The reason |
| // for this delay is to allow user to decide on the outcome of 'beforeunload'. |
| const int64_t kAttachFailureDelayMS = 30000; |
| |
| // Cancels the given navigation handle unconditionally. |
| class CancelAndIgnoreNavigationForPluginFrameThrottle |
| : public NavigationThrottle { |
| public: |
| explicit CancelAndIgnoreNavigationForPluginFrameThrottle( |
| NavigationHandle* handle) |
| : NavigationThrottle(handle) {} |
| ~CancelAndIgnoreNavigationForPluginFrameThrottle() override {} |
| |
| const char* GetNameForLogging() override { |
| return "CancelAndIgnoreNavigationForPluginFrameThrottle"; |
| } |
| ThrottleCheckResult WillStartRequest() override { return CANCEL_AND_IGNORE; } |
| ThrottleCheckResult WillProcessResponse() override { return BLOCK_RESPONSE; } |
| }; |
| |
| using ProcessIdToHelperMap = |
| base::flat_map<int32_t, std::unique_ptr<MimeHandlerViewAttachHelper>>; |
| ProcessIdToHelperMap* GetProcessIdToHelperMap() { |
| static base::NoDestructor<ProcessIdToHelperMap> instance; |
| return instance.get(); |
| } |
| |
| // Helper class which tracks navigations related to |frame_tree_node_id| and |
| // looks for a same-SiteInstance child RenderFrameHost created which has |
| // |frame_tree_node_id| as its parent's FrameTreeNode Id. |
| class PendingFullPageNavigation : public content::WebContentsObserver { |
| public: |
| PendingFullPageNavigation(int32_t frame_tree_node_id, |
| const GURL& resource_url, |
| const std::string& mime_type, |
| const std::string& stream_id); |
| ~PendingFullPageNavigation() override; |
| |
| // content::WebContentsObserver overrides. |
| void DidStartNavigation(content::NavigationHandle* handle) override; |
| void RenderFrameCreated(content::RenderFrameHost* render_frame_host) override; |
| void WebContentsDestroyed() override; |
| |
| private: |
| const int32_t frame_tree_node_id_; |
| const GURL resource_url_; |
| const std::string mime_type_; |
| const std::string stream_id_; |
| }; |
| |
| using PendingNavigationMap = |
| base::flat_map<int32_t, std::unique_ptr<PendingFullPageNavigation>>; |
| |
| PendingNavigationMap* GetPendingFullPageNavigationsMap() { |
| static base::NoDestructor<PendingNavigationMap> instance; |
| return instance.get(); |
| } |
| |
| // Returns true if the mime type is relevant to MimeHandlerView. |
| bool IsRelevantMimeType(const std::string& mime_type) { |
| // TODO(ekaramad): Figure out what other relevant mime-types are, e.g., for |
| // quick office. |
| return mime_type == "application/pdf" || mime_type == "text/pdf"; |
| } |
| |
| PendingFullPageNavigation::PendingFullPageNavigation( |
| int32_t frame_tree_node_id, |
| const GURL& resource_url, |
| const std::string& mime_type, |
| const std::string& stream_id) |
| : content::WebContentsObserver( |
| content::WebContents::FromFrameTreeNodeId(frame_tree_node_id)), |
| frame_tree_node_id_(frame_tree_node_id), |
| resource_url_(resource_url), |
| mime_type_(mime_type), |
| stream_id_(stream_id) {} |
| |
| PendingFullPageNavigation::~PendingFullPageNavigation() {} |
| |
| void PendingFullPageNavigation::DidStartNavigation( |
| content::NavigationHandle* handle) { |
| // This observer is created after the observed |frame_tree_node_id_| started |
| // its navigation to the |resource_url|. If any new navigations start then |
| // we should stop now and do not create a MHVG. |
| if (handle->GetFrameTreeNodeId() == frame_tree_node_id_) |
| GetPendingFullPageNavigationsMap()->erase(frame_tree_node_id_); |
| } |
| |
| void PendingFullPageNavigation::RenderFrameCreated( |
| content::RenderFrameHost* render_frame_host) { |
| if (render_frame_host->GetParent() && |
| render_frame_host->GetParent()->GetFrameTreeNodeId() == |
| frame_tree_node_id_ && |
| render_frame_host->GetParent()->GetLastCommittedURL() == resource_url_) { |
| // This suggests that a same-origin child frame is created under the |
| // RFH associated with |frame_tree_node_id_|. This suggests that the HTML |
| // string is loaded in the observed frame's document and now the renderer |
| // can initiate the MimeHandlerViewFrameContainer creation process. |
| mojom::MimeHandlerViewContainerManagerPtr container_manager; |
| render_frame_host->GetParent()->GetRemoteInterfaces()->GetInterface( |
| &container_manager); |
| container_manager->CreateFrameContainer(resource_url_, mime_type_, |
| stream_id_); |
| GetPendingFullPageNavigationsMap()->erase(frame_tree_node_id_); |
| } |
| } |
| |
| void PendingFullPageNavigation::WebContentsDestroyed() { |
| GetPendingFullPageNavigationsMap()->erase(frame_tree_node_id_); |
| } |
| |
| } // namespace |
| |
| // Helper class which navigates a given FrameTreeNode to "about:blank". This is |
| // used for scenarios where the plugin element's content frame has a different |
| // SiteInstance from its parent frame, or, the frame's origin is not |
| // "about:blank". Since this class triggers a navigation, all the document |
| // unload events will be dispatched and handled. During the lifetime of this |
| // helper class, all other navigations for the corresponding FrameTreeNode will |
| // be throttled and ignored. |
| class MimeHandlerViewAttachHelper::FrameNavigationHelper |
| : public content::WebContentsObserver { |
| public: |
| FrameNavigationHelper(RenderFrameHost* plugin_rfh, |
| int32_t guest_instance_id, |
| int32_t element_instance_id, |
| bool is_full_page_plugin, |
| base::WeakPtr<MimeHandlerViewAttachHelper> helper); |
| ~FrameNavigationHelper() override; |
| |
| void FrameDeleted(RenderFrameHost* render_frame_host) override; |
| void DidFinishNavigation(NavigationHandle* handle) override; |
| // During attaching, we should ignore any navigation which is not a navigation |
| // to "about:blank" from the parent frame's SiteInstance. |
| bool ShouldCancelAndIgnore(NavigationHandle* handle); |
| |
| MimeHandlerViewGuest* GetGuestView() const; |
| |
| int32_t guest_instance_id() const { return guest_instance_id_; } |
| bool is_full_page_plugin() const { return is_full_page_plugin_; } |
| SiteInstance* parent_site_instance() const { |
| return parent_site_instance_.get(); |
| } |
| |
| private: |
| void NavigateToAboutBlank(); |
| void CancelPendingTask(); |
| |
| int32_t frame_tree_node_id_; |
| const int32_t guest_instance_id_; |
| const int32_t element_instance_id_; |
| const bool is_full_page_plugin_; |
| base::WeakPtr<MimeHandlerViewAttachHelper> helper_; |
| scoped_refptr<SiteInstance> parent_site_instance_; |
| |
| base::WeakPtrFactory<FrameNavigationHelper> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FrameNavigationHelper); |
| }; |
| |
| MimeHandlerViewAttachHelper::FrameNavigationHelper::FrameNavigationHelper( |
| RenderFrameHost* plugin_rfh, |
| int32_t guest_instance_id, |
| int32_t element_instance_id, |
| bool is_full_page_plugin, |
| base::WeakPtr<MimeHandlerViewAttachHelper> helper) |
| : content::WebContentsObserver( |
| content::WebContents::FromRenderFrameHost(plugin_rfh)), |
| frame_tree_node_id_(plugin_rfh->GetFrameTreeNodeId()), |
| guest_instance_id_(guest_instance_id), |
| element_instance_id_(element_instance_id), |
| is_full_page_plugin_(is_full_page_plugin), |
| helper_(helper), |
| parent_site_instance_(plugin_rfh->GetParent()->GetSiteInstance()), |
| weak_factory_(this) { |
| DCHECK(GetGuestView()); |
| NavigateToAboutBlank(); |
| base::PostDelayedTaskWithTraits( |
| FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(&MimeHandlerViewAttachHelper::FrameNavigationHelper:: |
| CancelPendingTask, |
| weak_factory_.GetWeakPtr()), |
| base::TimeDelta::FromMilliseconds(kAttachFailureDelayMS)); |
| } |
| |
| MimeHandlerViewAttachHelper::FrameNavigationHelper::~FrameNavigationHelper() {} |
| |
| void MimeHandlerViewAttachHelper::FrameNavigationHelper::FrameDeleted( |
| RenderFrameHost* render_frame_host) { |
| if (render_frame_host->GetFrameTreeNodeId() != frame_tree_node_id_) |
| return; |
| // It is possible that the plugin frame is deleted before a NavigationHandle |
| // is created; one such case is to immediately delete the plugin element right |
| // after MimeHandlerViewFrameContainer requests to create the |
| // MimeHandlerViewGuest on the browser side. |
| helper_->ResumeAttachOrDestroy(element_instance_id_, |
| MSG_ROUTING_NONE /* no plugin frame */); |
| } |
| |
| void MimeHandlerViewAttachHelper::FrameNavigationHelper::DidFinishNavigation( |
| NavigationHandle* handle) { |
| if (handle->GetFrameTreeNodeId() != frame_tree_node_id_) |
| return; |
| if (!handle->HasCommitted()) |
| return; |
| if (handle->GetRenderFrameHost()->GetSiteInstance() != parent_site_instance_) |
| return; |
| if (!handle->GetURL().IsAboutBlank()) |
| return; |
| if (!handle->GetRenderFrameHost()->PrepareForInnerWebContentsAttach()) { |
| helper_->ResumeAttachOrDestroy(element_instance_id_, |
| MSG_ROUTING_NONE /* no plugin frame */); |
| } |
| base::PostTaskWithTraits( |
| FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(&MimeHandlerViewAttachHelper::ResumeAttachOrDestroy, |
| helper_, element_instance_id_, |
| handle->GetRenderFrameHost()->GetRoutingID())); |
| } |
| |
| bool MimeHandlerViewAttachHelper::FrameNavigationHelper::ShouldCancelAndIgnore( |
| NavigationHandle* handle) { |
| return handle->GetFrameTreeNodeId() == frame_tree_node_id_; |
| } |
| |
| void MimeHandlerViewAttachHelper::FrameNavigationHelper:: |
| NavigateToAboutBlank() { |
| // Immediately start a navigation to "about:blank". |
| GURL about_blank(url::kAboutBlankURL); |
| content::NavigationController::LoadURLParams params(about_blank); |
| params.frame_tree_node_id = frame_tree_node_id_; |
| // The goal is to have a plugin frame which is same-origin with parent, i.e., |
| // 'about:blank' and share the same SiteInstance. |
| params.source_site_instance = parent_site_instance_; |
| // The renderer (parent of the plugin frame) tries to load a MimeHandlerView |
| // and therefore this navigation should be treated as renderer initiated. |
| params.is_renderer_initiated = true; |
| params.initiator_origin = |
| url::Origin::Create(parent_site_instance_->GetSiteURL()); |
| web_contents()->GetController().LoadURLWithParams(params); |
| } |
| |
| void MimeHandlerViewAttachHelper::FrameNavigationHelper::CancelPendingTask() { |
| helper_->ResumeAttachOrDestroy(element_instance_id_, |
| MSG_ROUTING_NONE /* no plugin frame */); |
| } |
| |
| MimeHandlerViewGuest* |
| MimeHandlerViewAttachHelper::FrameNavigationHelper::GetGuestView() const { |
| return MimeHandlerViewGuest::From( |
| parent_site_instance_->GetProcess()->GetID(), guest_instance_id_) |
| ->As<MimeHandlerViewGuest>(); |
| } |
| |
| // static |
| MimeHandlerViewAttachHelper* MimeHandlerViewAttachHelper::Get( |
| int32_t render_process_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto& map = *GetProcessIdToHelperMap(); |
| if (!base::ContainsKey(map, render_process_id)) { |
| auto* process_host = content::RenderProcessHost::FromID(render_process_id); |
| if (!process_host) |
| return nullptr; |
| map[render_process_id] = base::WrapUnique<MimeHandlerViewAttachHelper>( |
| new MimeHandlerViewAttachHelper(process_host)); |
| } |
| return map[render_process_id].get(); |
| } |
| |
| // static |
| void MimeHandlerViewAttachHelper::OverrideBodyForInterceptedResponse( |
| int32_t navigating_frame_tree_node_id, |
| const GURL& resource_url, |
| const std::string& mime_type, |
| const std::string& stream_id, |
| std::string* payload, |
| uint32_t* data_pipe_size) { |
| if (!content::MimeHandlerViewMode::UsesCrossProcessFrame()) |
| return; |
| if (!IsRelevantMimeType(mime_type)) |
| return; |
| payload->assign(kFullPageMimeHandlerViewHTML); |
| *data_pipe_size = kFullPageMimeHandlerViewDataPipeSize; |
| base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI}, |
| base::BindOnce(CreateFullPageMimeHandlerView, |
| navigating_frame_tree_node_id, |
| resource_url, mime_type, stream_id)); |
| } |
| |
| // static |
| std::unique_ptr<content::NavigationThrottle> |
| MimeHandlerViewAttachHelper::MaybeCreateThrottle( |
| content::NavigationHandle* handle) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!handle->GetParentFrame()) { |
| // A plugin element cannot be the FrameOwner to a main frame. |
| return nullptr; |
| } |
| int32_t parent_process_id = handle->GetParentFrame()->GetProcess()->GetID(); |
| auto& map = *GetProcessIdToHelperMap(); |
| if (!base::ContainsKey(map, parent_process_id) || !map[parent_process_id]) { |
| // This happens if the RenderProcessHost has not been initialized yet. |
| return nullptr; |
| } |
| for (auto& pair : map[parent_process_id]->frame_navigation_helpers_) { |
| if (!pair.second->ShouldCancelAndIgnore(handle)) |
| continue; |
| // Any navigation of the corresponding FrameTreeNode which is not to |
| // "about:blank" or is not initiated by parent SiteInstance should be |
| // ignored. |
| return std::make_unique<CancelAndIgnoreNavigationForPluginFrameThrottle>( |
| handle); |
| } |
| return nullptr; |
| } |
| |
| void MimeHandlerViewAttachHelper::RenderProcessHostDestroyed( |
| content::RenderProcessHost* render_process_host) { |
| if (render_process_host != render_process_host_) |
| return; |
| render_process_host->RemoveObserver(this); |
| GetProcessIdToHelperMap()->erase(render_process_host_->GetID()); |
| } |
| |
| void MimeHandlerViewAttachHelper::AttachToOuterWebContents( |
| MimeHandlerViewGuest* guest_view, |
| int32_t embedder_render_process_id, |
| int32_t plugin_frame_routing_id, |
| int32_t element_instance_id, |
| bool is_full_page_plugin) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto* plugin_rfh = RenderFrameHost::FromID(embedder_render_process_id, |
| plugin_frame_routing_id); |
| if (!plugin_rfh) { |
| // The plugin element has a proxy instead. |
| plugin_rfh = RenderFrameHost::FromPlaceholderId(embedder_render_process_id, |
| plugin_frame_routing_id); |
| } |
| if (!plugin_rfh) { |
| // This should only happen if the original plugin frame was cross-process |
| // and a concurrent navigation in its process won the race and ended up |
| // destroying the proxy whose routing ID was sent here by the |
| // MimeHandlerViewFrameContainer. We should ask the embedder to retry |
| // creating the guest. |
| guest_view->GetEmbedderFrame()->Send( |
| new ExtensionsGuestViewMsg_RetryCreatingMimeHandlerViewGuest( |
| element_instance_id)); |
| guest_view->Destroy(true); |
| return; |
| } |
| |
| if (guest_view->web_contents()->CanAttachToOuterContentsFrame(plugin_rfh)) { |
| guest_view->AttachToOuterWebContentsFrame(plugin_rfh, element_instance_id, |
| is_full_page_plugin); |
| |
| } else { |
| // TODO(ekaramad): Replace this navigation logic with an asynchronous |
| // attach API in content layer (https://crbug.com/911161). |
| // The current API for attaching guests requires the frame in outer |
| // WebContents to be same-origin with parent. The current frame could also |
| // have beforeunload handlers. Considering these issues, we should first |
| // navigate the frame to "about:blank" and put it in the same SiteInstance |
| // as parent before using it for attach API. |
| frame_navigation_helpers_[element_instance_id] = |
| std::make_unique<FrameNavigationHelper>( |
| plugin_rfh, guest_view->guest_instance_id(), element_instance_id, |
| is_full_page_plugin, weak_factory_.GetWeakPtr()); |
| } |
| } |
| |
| // static |
| void MimeHandlerViewAttachHelper::CreateFullPageMimeHandlerView( |
| int32_t frame_tree_node_id, |
| const GURL& resource_url, |
| const std::string& mime_type, |
| const std::string& stream_id) { |
| auto& map = *GetPendingFullPageNavigationsMap(); |
| map[frame_tree_node_id] = std::make_unique<PendingFullPageNavigation>( |
| frame_tree_node_id, resource_url, mime_type, stream_id); |
| } |
| |
| MimeHandlerViewAttachHelper::MimeHandlerViewAttachHelper( |
| content::RenderProcessHost* render_process_host) |
| : render_process_host_(render_process_host), weak_factory_(this) { |
| render_process_host->AddObserver(this); |
| } |
| |
| MimeHandlerViewAttachHelper::~MimeHandlerViewAttachHelper() {} |
| |
| void MimeHandlerViewAttachHelper::ResumeAttachOrDestroy( |
| int32_t element_instance_id, |
| int32_t plugin_frame_routing_id) { |
| auto it = frame_navigation_helpers_.find(element_instance_id); |
| if (it == frame_navigation_helpers_.end()) { |
| // This is the timeout callback. The guest is either attached or destroyed. |
| return; |
| } |
| auto* plugin_rfh = content::RenderFrameHost::FromID( |
| render_process_host_->GetID(), plugin_frame_routing_id); |
| auto* helper = it->second.get(); |
| auto* guest_view = helper->GetGuestView(); |
| if (!guest_view) |
| return; |
| |
| if (plugin_rfh) { |
| DCHECK( |
| guest_view->web_contents()->CanAttachToOuterContentsFrame(plugin_rfh)); |
| guest_view->AttachToOuterWebContentsFrame(plugin_rfh, element_instance_id, |
| helper->is_full_page_plugin()); |
| } else { |
| guest_view->GetEmbedderFrame()->Send( |
| new ExtensionsGuestViewMsg_DestroyFrameContainer(element_instance_id)); |
| guest_view->Destroy(true); |
| } |
| frame_navigation_helpers_.erase(element_instance_id); |
| } |
| } // namespace extensions |