| // Copyright 2017 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/printing/browser/print_composite_client.h" |
| |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/read_only_shared_memory_region.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "components/enterprise/buildflags/buildflags.h" |
| #include "components/services/print_compositor/public/cpp/print_service_mojo_types.h" |
| #include "components/services/print_compositor/public/mojom/print_compositor.mojom.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/service_process_host.h" |
| #include "printing/common/metafile_utils.h" |
| #include "printing/mojom/print.mojom.h" |
| #include "printing/print_settings.h" |
| #include "printing/printed_document.h" |
| #include "printing/printing_utils.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| |
| #if BUILDFLAG(ENTERPRISE_WATERMARK) |
| #include "components/enterprise/watermarking/content/watermark_text_container.h" // nogncheck |
| #include "components/enterprise/watermarking/mojom/watermark.mojom.h" |
| #endif |
| |
| namespace printing { |
| |
| namespace { |
| |
| uint64_t GenerateFrameGuid(content::RenderFrameHost* render_frame_host) { |
| int process_id = render_frame_host->GetProcess()->GetDeprecatedID(); |
| int frame_id = render_frame_host->GetRoutingID(); |
| return static_cast<uint64_t>(process_id) << 32 | frame_id; |
| } |
| |
| // Converts a ContentToProxyTokenMap to ContentToFrameMap. |
| // ContentToProxyTokenMap maps content id to the frame token of its |
| // corresponding render frame proxy. This is generated when the content holder |
| // was created; ContentToFrameMap maps content id to its render frame's global |
| // unique id. The global unique id has the render process id concatenated with |
| // render frame routing id, which can uniquely identify a render frame. |
| ContentToFrameMap ConvertContentInfoMap( |
| content::RenderFrameHost* render_frame_host, |
| const ContentToProxyTokenMap& content_proxy_map) { |
| ContentToFrameMap content_frame_map; |
| int process_id = render_frame_host->GetProcess()->GetDeprecatedID(); |
| for (const auto& entry : content_proxy_map) { |
| auto content_id = entry.first; |
| auto proxy_token = entry.second; |
| // Find the RenderFrameHost that the proxy id corresponds to. |
| content::RenderFrameHost* rfh = |
| content::RenderFrameHost::FromPlaceholderToken( |
| process_id, blink::RemoteFrameToken(proxy_token)); |
| if (!rfh) { |
| // If the corresponding RenderFrameHost cannot be found, just skip it. |
| continue; |
| } |
| |
| // Store this frame's global unique id into the map. |
| content_frame_map[content_id] = GenerateFrameGuid(rfh); |
| } |
| return content_frame_map; |
| } |
| |
| } // namespace |
| |
| PrintCompositeClient::PrintCompositeClient(content::WebContents* web_contents) |
| : content::WebContentsUserData<PrintCompositeClient>(*web_contents), |
| content::WebContentsObserver(web_contents) {} |
| |
| PrintCompositeClient::~PrintCompositeClient() = default; |
| |
| void PrintCompositeClient::RenderFrameDeleted( |
| content::RenderFrameHost* render_frame_host) { |
| if (document_cookie_ == 0) { |
| DCHECK(!initiator_frame_); |
| return; |
| } |
| |
| if (initiator_frame_ == render_frame_host) { |
| RemoveCompositeRequest(document_cookie_); |
| return; |
| } |
| |
| auto iter = pending_subframes_.find(render_frame_host); |
| if (iter != pending_subframes_.end()) { |
| // When a subframe we are expecting is deleted, we should notify the print |
| // compositor service. |
| auto* compositor = GetCompositeRequest(document_cookie_); |
| compositor->NotifyUnavailableSubframe(GenerateFrameGuid(render_frame_host)); |
| pending_subframes_.erase(iter); |
| } |
| |
| print_render_frames_.erase(render_frame_host); |
| } |
| |
| PrintCompositeClient::RequestedSubFrame::RequestedSubFrame( |
| content::GlobalRenderFrameHostId rfh_id, |
| int document_cookie, |
| mojom::DidPrintContentParamsPtr params, |
| bool is_live) |
| : rfh_id_(rfh_id), |
| document_cookie_(document_cookie), |
| params_(std::move(params)), |
| is_live_(is_live) {} |
| |
| PrintCompositeClient::RequestedSubFrame::~RequestedSubFrame() = default; |
| |
| void PrintCompositeClient::OnDidPrintFrameContent( |
| content::GlobalRenderFrameHostId rfh_id, |
| int document_cookie, |
| mojom::DidPrintContentParamsPtr params) { |
| auto* outer_contents = web_contents()->GetOuterWebContents(); |
| if (outer_contents) { |
| // When the printed content belongs to an extension or app page, the print |
| // composition needs to be handled by its outer content. |
| // TODO(weili): so far, we don't have printable web contents nested in more |
| // than one level. In the future, especially after PDF plugin is moved to |
| // OOPIF-based webview, we should check whether we need to handle web |
| // contents nested in multiple layers. |
| auto* outer_client = PrintCompositeClient::FromWebContents(outer_contents); |
| DCHECK(outer_client); |
| outer_client->OnDidPrintFrameContent(rfh_id, document_cookie, |
| std::move(params)); |
| return; |
| } |
| |
| if (!IsDocumentCookieValid(document_cookie)) { |
| if (!compositor_) { |
| // Queues the subframe information to |requested_subframes_| to handle it |
| // after |compositor_| is created by the main frame. |
| requested_subframes_.insert(std::make_unique<RequestedSubFrame>( |
| rfh_id, document_cookie, std::move(params), /*is_live=*/true)); |
| } |
| return; |
| } |
| |
| auto* render_frame_host = content::RenderFrameHost::FromID(rfh_id); |
| if (!render_frame_host) |
| return; |
| |
| // Content in |params| is sent from untrusted source; only minimal processing |
| // is done here. Most of it will be directly forwarded to print compositor |
| // service. |
| auto* compositor = GetCompositeRequest(document_cookie); |
| auto region = params->metafile_data_region.Duplicate(); |
| compositor->AddSubframeContent( |
| GenerateFrameGuid(render_frame_host), std::move(region), |
| ConvertContentInfoMap(render_frame_host, params->subframe_content_info)); |
| |
| // Update our internal states about this frame. |
| pending_subframes_.erase(render_frame_host); |
| printed_subframes_.insert(render_frame_host); |
| } |
| |
| void PrintCompositeClient::SetAccessibilityTree( |
| int document_cookie, |
| const ui::AXTreeUpdate& accessibility_tree) { |
| if (!IsDocumentCookieValid(document_cookie)) |
| return; |
| |
| auto* compositor = GetCompositeRequest(document_cookie); |
| compositor->SetAccessibilityTree(accessibility_tree); |
| } |
| |
| void PrintCompositeClient::PrintCrossProcessSubframe( |
| const gfx::Rect& rect, |
| int document_cookie, |
| content::RenderFrameHost* subframe_host) { |
| auto params = mojom::PrintFrameContentParams::New(rect, document_cookie); |
| if (!subframe_host->IsRenderFrameLive()) { |
| if (!IsDocumentCookieValid(document_cookie)) { |
| if (!compositor_) { |
| // Queues the subframe information to |requested_subframes_| to handle |
| // it after |compositor_| is created by the main frame. |
| requested_subframes_.insert(std::make_unique<RequestedSubFrame>( |
| subframe_host->GetGlobalId(), document_cookie, /*params=*/nullptr, |
| /*is_live=*/false)); |
| } |
| return; |
| } |
| |
| // When the subframe is dead, no need to send message, |
| // just notify the service. |
| auto* compositor = GetCompositeRequest(document_cookie); |
| compositor->NotifyUnavailableSubframe(GenerateFrameGuid(subframe_host)); |
| return; |
| } |
| |
| // If this frame is already printed, no need to print again. |
| if (base::Contains(pending_subframes_, subframe_host) || |
| base::Contains(printed_subframes_, subframe_host)) { |
| return; |
| } |
| |
| // Send the request to the destination frame. |
| GetPrintRenderFrame(subframe_host) |
| ->PrintFrameContent( |
| std::move(params), |
| base::BindOnce(&PrintCompositeClient::OnDidPrintFrameContent, |
| weak_ptr_factory_.GetWeakPtr(), |
| subframe_host->GetGlobalId())); |
| pending_subframes_.insert(subframe_host); |
| } |
| |
| void PrintCompositeClient::CompositePage( |
| int document_cookie, |
| content::RenderFrameHost* render_frame_host, |
| const mojom::DidPrintContentParams& content, |
| mojom::PrintCompositor::CompositePageCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!IsDocumentCookieValid(document_cookie)) |
| return; |
| |
| auto* compositor = GetCompositeRequest(document_cookie); |
| auto region = content.metafile_data_region.Duplicate(); |
| compositor->CompositePage( |
| GenerateFrameGuid(render_frame_host), std::move(region), |
| ConvertContentInfoMap(render_frame_host, content.subframe_content_info), |
| base::BindOnce(&PrintCompositeClient::OnDidCompositePage, |
| std::move(callback))); |
| } |
| |
| void PrintCompositeClient::PrepareToCompositeDocument( |
| int document_cookie, |
| content::RenderFrameHost* render_frame_host, |
| mojom::PrintCompositor::DocumentType document_type, |
| mojom::PrintCompositor::PrepareToCompositeDocumentCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(!GetIsDocumentConcurrentlyComposited(document_cookie)); |
| |
| auto* compositor = |
| CreateCompositeRequest(document_cookie, render_frame_host, document_type); |
| is_doc_concurrently_composited_ = true; |
| compositor->PrepareToCompositeDocument( |
| document_type, |
| base::BindOnce(&PrintCompositeClient::OnDidPrepareToCompositeDocument, |
| std::move(callback))); |
| } |
| |
| void PrintCompositeClient::FinishDocumentComposition( |
| int document_cookie, |
| uint32_t pages_count, |
| mojom::PrintCompositor::FinishDocumentCompositionCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(GetIsDocumentConcurrentlyComposited(document_cookie)); |
| |
| if (!IsDocumentCookieValid(document_cookie)) |
| return; |
| |
| auto* compositor = GetCompositeRequest(document_cookie); |
| |
| // Since this class owns compositor, compositor will be gone when this class |
| // is destructed. Mojo won't call its callback in that case so it is safe to |
| // use unretained |this| pointer here. |
| compositor->FinishDocumentComposition( |
| pages_count, |
| base::BindOnce(&PrintCompositeClient::OnDidFinishDocumentComposition, |
| base::Unretained(this), document_cookie, |
| std::move(callback))); |
| } |
| |
| void PrintCompositeClient::CompositeDocument( |
| int document_cookie, |
| content::RenderFrameHost* render_frame_host, |
| const mojom::DidPrintContentParams& content, |
| const ui::AXTreeUpdate& accessibility_tree, |
| mojom::GenerateDocumentOutline generate_document_outline, |
| mojom::PrintCompositor::DocumentType document_type, |
| mojom::PrintCompositor::CompositeDocumentCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(!GetIsDocumentConcurrentlyComposited(document_cookie)); |
| |
| auto* compositor = |
| CreateCompositeRequest(document_cookie, render_frame_host, document_type); |
| compositor->SetAccessibilityTree(accessibility_tree); |
| compositor->SetGenerateDocumentOutline(generate_document_outline); |
| |
| for (auto& requested : requested_subframes_) { |
| if (!IsDocumentCookieValid(requested->document_cookie_)) |
| continue; |
| if (requested->is_live_) { |
| OnDidPrintFrameContent(requested->rfh_id_, requested->document_cookie_, |
| std::move(requested->params_)); |
| } else { |
| compositor->NotifyUnavailableSubframe(GenerateFrameGuid( |
| content::RenderFrameHost::FromID(requested->rfh_id_))); |
| } |
| } |
| requested_subframes_.clear(); |
| |
| auto region = content.metafile_data_region.Duplicate(); |
| |
| // Since this class owns compositor, compositor will be gone when this class |
| // is destructed. Mojo won't call its callback in that case so it is safe to |
| // use unretained |this| pointer here. |
| compositor->CompositeDocument( |
| GenerateFrameGuid(render_frame_host), std::move(region), |
| ConvertContentInfoMap(render_frame_host, content.subframe_content_info), |
| document_type, |
| base::BindOnce(&PrintCompositeClient::OnDidCompositeDocument, |
| base::Unretained(this), document_cookie, |
| std::move(callback))); |
| } |
| |
| // static |
| void PrintCompositeClient::OnDidCompositePage( |
| mojom::PrintCompositor::CompositePageCallback callback, |
| mojom::PrintCompositor::Status status, |
| base::ReadOnlySharedMemoryRegion region) { |
| std::move(callback).Run(status, std::move(region)); |
| } |
| |
| void PrintCompositeClient::OnDidCompositeDocument( |
| int document_cookie, |
| mojom::PrintCompositor::CompositeDocumentCallback callback, |
| mojom::PrintCompositor::Status status, |
| base::ReadOnlySharedMemoryRegion region) { |
| RemoveCompositeRequest(document_cookie); |
| std::move(callback).Run(status, std::move(region)); |
| } |
| |
| // static |
| void PrintCompositeClient::OnDidPrepareToCompositeDocument( |
| mojom::PrintCompositor::PrepareToCompositeDocumentCallback callback, |
| mojom::PrintCompositor::Status status) { |
| std::move(callback).Run(status); |
| } |
| |
| void PrintCompositeClient::OnDidFinishDocumentComposition( |
| int document_cookie, |
| mojom::PrintCompositor::FinishDocumentCompositionCallback callback, |
| mojom::PrintCompositor::Status status, |
| base::ReadOnlySharedMemoryRegion region) { |
| RemoveCompositeRequest(document_cookie); |
| std::move(callback).Run(status, std::move(region)); |
| } |
| |
| bool PrintCompositeClient::GetIsDocumentConcurrentlyComposited( |
| int cookie) const { |
| return is_doc_concurrently_composited_ && document_cookie_ == cookie; |
| } |
| |
| mojom::PrintCompositor* PrintCompositeClient::CreateCompositeRequest( |
| int cookie, |
| content::RenderFrameHost* initiator_frame, |
| mojom::PrintCompositor::DocumentType document_type) { |
| DCHECK(initiator_frame); |
| |
| if (document_cookie_ != 0) { |
| DCHECK_NE(document_cookie_, cookie); |
| RemoveCompositeRequest(document_cookie_); |
| } |
| document_cookie_ = cookie; |
| |
| // Track which frame kicked off the composite request. |
| initiator_frame_ = initiator_frame; |
| |
| compositor_ = content::ServiceProcessHost::Launch<mojom::PrintCompositor>( |
| content::ServiceProcessHost::Options() |
| .WithDisplayName(IDS_PRINT_COMPOSITOR_SERVICE_DISPLAY_NAME) |
| .Pass()); |
| |
| compositor_->SetTitle(base::UTF16ToUTF8(web_contents()->GetTitle())); |
| compositor_->SetWebContentsURL(web_contents()->GetLastCommittedURL()); |
| compositor_->SetUserAgent(user_agent_); |
| |
| #if BUILDFLAG(ENTERPRISE_WATERMARK) |
| auto* watermark_text_container = |
| enterprise_watermark::WatermarkTextContainer::FromWebContents( |
| web_contents()); |
| if (watermark_text_container) { |
| compositor_->SetWatermarkBlock(watermark_text_container->Serialize()); |
| } |
| #endif |
| return compositor_.get(); |
| } |
| |
| void PrintCompositeClient::RemoveCompositeRequest(int cookie) { |
| DCHECK_EQ(document_cookie_, cookie); |
| compositor_.reset(); |
| document_cookie_ = PrintSettings::NewInvalidCookie(); |
| initiator_frame_ = nullptr; |
| |
| // Reset state of the client. |
| pending_subframes_.clear(); |
| printed_subframes_.clear(); |
| print_render_frames_.clear(); |
| |
| // No longer concurrently compositing this document. |
| is_doc_concurrently_composited_ = false; |
| } |
| |
| bool PrintCompositeClient::IsDocumentCookieValid(int document_cookie) const { |
| return document_cookie != 0 && document_cookie == document_cookie_; |
| } |
| |
| mojom::PrintCompositor* PrintCompositeClient::GetCompositeRequest( |
| int cookie) const { |
| DCHECK(IsDocumentCookieValid(cookie)); |
| DCHECK(compositor_.is_bound()); |
| return compositor_.get(); |
| } |
| |
| const mojo::AssociatedRemote<mojom::PrintRenderFrame>& |
| PrintCompositeClient::GetPrintRenderFrame(content::RenderFrameHost* rfh) { |
| auto it = print_render_frames_.find(rfh); |
| if (it == print_render_frames_.end()) { |
| mojo::AssociatedRemote<mojom::PrintRenderFrame> remote; |
| rfh->GetRemoteAssociatedInterfaces()->GetInterface(&remote); |
| it = print_render_frames_.emplace(rfh, std::move(remote)).first; |
| } |
| |
| return it->second; |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PrintCompositeClient); |
| |
| } // namespace printing |