| // Copyright 2019 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/paint_preview/browser/paint_preview_client.h" |
| |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/map_util.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/unguessable_token.h" |
| #include "base/version.h" |
| #include "components/paint_preview/common/capture_result.h" |
| #include "components/paint_preview/common/file_stream.h" |
| #include "components/paint_preview/common/mojom/paint_preview_recorder.mojom.h" |
| #include "components/paint_preview/common/mojom/paint_preview_types.mojom.h" |
| #include "components/paint_preview/common/proto_validator.h" |
| #include "components/paint_preview/common/redaction_params.h" |
| #include "components/paint_preview/common/serialized_recording.h" |
| #include "components/paint_preview/common/version.h" |
| #include "components/version_info/version_info.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/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "mojo/public/cpp/base/big_buffer.h" |
| #include "services/metrics/public/cpp/metrics_utils.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "skia/ext/skia_utils_base.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/perfetto/include/perfetto/tracing/track.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/core/SkPicture.h" |
| #include "third_party/skia/include/core/SkPictureRecorder.h" |
| #include "third_party/skia/include/core/SkRect.h" |
| #include "third_party/skia/include/core/SkRefCnt.h" |
| #include "third_party/skia/include/core/SkStream.h" |
| |
| namespace paint_preview { |
| |
| namespace { |
| |
| // Converts gfx::Rect to its RectProto form. |
| void RectToRectProto(const gfx::Rect& rect, RectProto* proto) { |
| proto->set_x(rect.x()); |
| proto->set_y(rect.y()); |
| proto->set_width(rect.width()); |
| proto->set_height(rect.height()); |
| } |
| |
| // Converts |response| into |proto|. Returns a list of the frame GUIDs |
| // referenced by the response. |
| std::vector<base::UnguessableToken> |
| PaintPreviewCaptureResponseToPaintPreviewFrameProto( |
| mojom::PaintPreviewCaptureResponsePtr response, |
| base::UnguessableToken frame_guid, |
| PaintPreviewFrameProto* proto) { |
| proto->set_embedding_token_high(frame_guid.GetHighForSerialization()); |
| proto->set_embedding_token_low(frame_guid.GetLowForSerialization()); |
| proto->set_scroll_offset_x(response->geometry_metadata->scroll_offsets.x()); |
| proto->set_scroll_offset_y(response->geometry_metadata->scroll_offsets.y()); |
| proto->set_frame_offset_x(response->geometry_metadata->frame_offsets.x()); |
| proto->set_frame_offset_y(response->geometry_metadata->frame_offsets.y()); |
| |
| std::vector<base::UnguessableToken> frame_guids; |
| for (const auto& id_pair : response->content_id_to_embedding_token) { |
| auto* content_id_embedding_token_pair = |
| proto->add_content_id_to_embedding_tokens(); |
| content_id_embedding_token_pair->set_content_id(id_pair.first); |
| content_id_embedding_token_pair->set_embedding_token_low( |
| id_pair.second.GetLowForSerialization()); |
| content_id_embedding_token_pair->set_embedding_token_high( |
| id_pair.second.GetHighForSerialization()); |
| frame_guids.push_back(id_pair.second); |
| } |
| |
| for (const auto& link : response->links) { |
| auto* link_proto = proto->add_links(); |
| link_proto->set_url(link->url.spec()); |
| RectToRectProto(link->rect, link_proto->mutable_rect()); |
| } |
| |
| return frame_guids; |
| } |
| |
| // Records UKM data for the capture. |
| // TODO(crbug.com/40113169): Add more metrics; |
| // - Peak memory during capture (bucketized). |
| // - Compressed on disk size (bucketized). |
| void RecordUkmCaptureData(ukm::SourceId source_id, |
| base::TimeDelta blink_recording_time) { |
| if (source_id == ukm::kInvalidSourceId) { |
| return; |
| } |
| ukm::builders::PaintPreviewCapture(source_id) |
| .SetBlinkCaptureTime(blink_recording_time.InMilliseconds()) |
| .Record(ukm::UkmRecorder::Get()); |
| } |
| |
| base::flat_set<base::UnguessableToken> CreateAcceptedTokenList( |
| content::RenderFrameHost* render_frame_host) { |
| std::vector<base::UnguessableToken> tokens; |
| render_frame_host->ForEachRenderFrameHost( |
| [&tokens](content::RenderFrameHost* rfh) { |
| auto maybe_token = rfh->GetEmbeddingToken(); |
| if (maybe_token.has_value()) { |
| tokens.push_back(maybe_token.value()); |
| } |
| }); |
| return base::flat_set<base::UnguessableToken>(std::move(tokens)); |
| } |
| |
| mojom::PaintPreviewCaptureParamsPtr CreateRecordingRequestParams( |
| RecordingPersistence persistence, |
| const RecordingParams& capture_params, |
| base::File file) { |
| mojom::PaintPreviewCaptureParamsPtr mojo_params = |
| mojom::PaintPreviewCaptureParams::New(); |
| mojo_params->persistence = persistence; |
| mojo_params->capture_links = capture_params.capture_links; |
| mojo_params->guid = capture_params.get_document_guid(); |
| mojo_params->geometry_metadata_params = mojom::GeometryMetadataParams::New(); |
| mojo_params->geometry_metadata_params->clip_rect = capture_params.clip_rect; |
| mojo_params->geometry_metadata_params->clip_x_coord_override = |
| capture_params.clip_x_coord_override; |
| mojo_params->geometry_metadata_params->clip_y_coord_override = |
| capture_params.clip_y_coord_override; |
| // For now treat all clip rects as hints only. This API should be exposed |
| // when clip_rects are used intentionally to limit capture time. |
| mojo_params->geometry_metadata_params->clip_rect_is_hint = true; |
| mojo_params->is_main_frame = capture_params.is_main_frame; |
| mojo_params->skip_accelerated_content = |
| capture_params.skip_accelerated_content; |
| mojo_params->file = std::move(file); |
| mojo_params->max_capture_size = capture_params.max_capture_size; |
| mojo_params->max_decoded_image_size_bytes = |
| capture_params.max_decoded_image_size_bytes; |
| return mojo_params; |
| } |
| |
| // Unconditionally create or overwrite a file for writing. |
| base::File CreateOrOverwriteFileForWriting(const base::FilePath& path) { |
| uint32_t flags = base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE; |
| // This file will be passed to an untrusted process. |
| flags = base::File::AddFlagsForPassingToUntrustedProcess(flags); |
| base::File file(path, flags); |
| return file; |
| } |
| |
| void CloseFile(base::File file) { |
| file.Close(); |
| } |
| |
| void OnSerializedRecordingFileCreated( |
| RecordingParams capture_params, |
| const base::FilePath& filename, |
| PaintPreviewClient::RecordingRequestParamsReadyCallback callback, |
| base::File file) { |
| if (!file.IsValid()) { |
| DLOG(ERROR) << "File create failed: " << file.error_details(); |
| std::move(callback).Run( |
| std::move(capture_params), |
| base::unexpected(mojom::PaintPreviewStatus::kFileCreationError)); |
| } else if (callback.IsCancelled()) { |
| // The weak pointer is invalid, we should close the file on a background |
| // thread to avoid it being closed implicitly via the default dtor on the UI |
| // thread and triggering a scoped blocking call violation. |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, |
| base::BindOnce(&CloseFile, std::move(file))); |
| } else { |
| mojom::PaintPreviewCaptureParamsPtr params = CreateRecordingRequestParams( |
| RecordingPersistence::kFileSystem, capture_params, std::move(file)); |
| std::move(callback).Run(std::move(capture_params), std::move(params)); |
| } |
| } |
| |
| // Creates and returns an SkPicture representing a black rectangle of the |
| // specified width and height. |
| sk_sp<SkPicture> CreateBlackRectangle(const gfx::Size& size) { |
| SkPictureRecorder recorder; |
| const SkRect bounds = SkRect::MakeWH(static_cast<float>(size.width()), |
| static_cast<float>(size.height())); |
| SkCanvas* canvas = recorder.beginRecording(bounds); |
| |
| SkPaint paint; |
| paint.setColor(SkColors::kBlack); |
| paint.setStyle(SkPaint::Style::kFill_Style); |
| |
| canvas->drawRect(bounds, paint); |
| |
| return recorder.finishRecordingAsPicture(); |
| } |
| |
| sk_sp<SkData> SerializeToBytes(sk_sp<SkPicture> skp) { |
| SkDynamicMemoryWStream memory_stream; |
| skp->serialize(&memory_stream); |
| return memory_stream.detachAsData(); |
| } |
| |
| base::expected<mojom::PaintPreviewCaptureResponsePtr, mojom::PaintPreviewStatus> |
| SerializeRedactedFrameToFile(const gfx::Size& size, |
| std::optional<size_t> max_capture_size, |
| const base::FilePath& file_path, |
| mojom::PaintPreviewCaptureResponsePtr response) { |
| CHECK(max_capture_size != 0); |
| sk_sp<SkData> data = SerializeToBytes(CreateBlackRectangle(size)); |
| |
| base::span<const uint8_t> bytes = skia::as_byte_span(*data); |
| if (max_capture_size && data->size() > max_capture_size) { |
| return base::unexpected(mojom::PaintPreviewStatus::kFileCreationError); |
| } |
| |
| base::File file = CreateOrOverwriteFileForWriting(file_path); |
| |
| if (!file.IsValid() || !file.WriteAndCheck(/*offset=*/0, bytes)) { |
| return base::unexpected(mojom::PaintPreviewStatus::kFileCreationError); |
| } |
| |
| return std::move(response); |
| } |
| |
| struct BufferAndMetadata { |
| mojo_base::BigBuffer buffer; |
| uint64_t buffer_size; |
| }; |
| |
| std::optional<BufferAndMetadata> SerializeRedactedFrameToBuffer( |
| const gfx::Size& size, |
| std::optional<size_t> max_capture_size) { |
| CHECK(max_capture_size != 0); |
| sk_sp<SkData> data = SerializeToBytes(CreateBlackRectangle(size)); |
| |
| if (max_capture_size && data->size() > max_capture_size) { |
| return std::nullopt; |
| } |
| size_t serialized_size = data->size(); |
| |
| return BufferAndMetadata{ |
| .buffer = mojo_base::BigBuffer(skia::as_byte_span(*data)), |
| .buffer_size = serialized_size, |
| }; |
| } |
| |
| content::GlobalRenderFrameHostId GetGlobalRenderFrameHostId( |
| content::RenderFrameHost* rfh) { |
| // TODO(https://crbug.com/441908441): use `rfh->GetGlobalId()` here. |
| return content::GlobalRenderFrameHostId(rfh->GetProcess()->GetDeprecatedID(), |
| rfh->GetRoutingID()); |
| } |
| |
| } // namespace |
| |
| PaintPreviewClient::PaintPreviewParams::PaintPreviewParams( |
| RecordingPersistence persistence) |
| : PaintPreviewClient::PaintPreviewParams(persistence, |
| base::UnguessableToken::Create()) { |
| } |
| |
| PaintPreviewClient::PaintPreviewParams::PaintPreviewParams( |
| RecordingPersistence persistence, |
| base::UnguessableToken document_guid) |
| : persistence(persistence), |
| inner(RecordingParams(std::move(document_guid))) {} |
| |
| PaintPreviewClient::PaintPreviewParams::~PaintPreviewParams() = default; |
| |
| PaintPreviewClient::PaintPreviewParams |
| PaintPreviewClient::PaintPreviewParams::Clone() const { |
| PaintPreviewParams copy(persistence, inner.get_document_guid()); |
| |
| copy.inner = inner.Clone(); |
| copy.root_dir = root_dir; |
| |
| return copy; |
| } |
| |
| // static |
| PaintPreviewClient::PaintPreviewParams |
| PaintPreviewClient::PaintPreviewParams::CreateForTesting( |
| RecordingPersistence persistence, |
| base::UnguessableToken document_guid) { |
| return PaintPreviewClient::PaintPreviewParams(persistence, |
| std::move(document_guid)); |
| } |
| |
| PaintPreviewClient::InProgressDocumentCaptureState:: |
| InProgressDocumentCaptureState() = default; |
| |
| PaintPreviewClient::InProgressDocumentCaptureState:: |
| ~InProgressDocumentCaptureState() { |
| if (persistence == RecordingPersistence::kFileSystem && |
| should_clean_up_files) { |
| for (const auto& subframe_guid : awaiting_subframes) { |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, |
| base::GetDeleteFileCallback(FilePathForFrame(subframe_guid))); |
| } |
| |
| for (const auto& subframe_guid : finished_subframes) { |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, |
| base::GetDeleteFileCallback(FilePathForFrame(subframe_guid))); |
| } |
| } |
| } |
| |
| PaintPreviewClient::InProgressDocumentCaptureState& |
| PaintPreviewClient::InProgressDocumentCaptureState::operator=( |
| InProgressDocumentCaptureState&& rhs) noexcept = default; |
| |
| PaintPreviewClient::InProgressDocumentCaptureState:: |
| InProgressDocumentCaptureState( |
| InProgressDocumentCaptureState&& other) noexcept = default; |
| |
| bool PaintPreviewClient::InProgressDocumentCaptureState::IsAllowedToCapture( |
| const base::UnguessableToken& frame_token) const { |
| return base::Contains(accepted_tokens, frame_token); |
| } |
| |
| bool PaintPreviewClient::InProgressDocumentCaptureState::IsFinishedCapturing( |
| const base::UnguessableToken& frame_token) const { |
| return base::Contains(finished_subframes, frame_token); |
| } |
| |
| bool PaintPreviewClient::InProgressDocumentCaptureState::IsCaptureInProgress( |
| const base::UnguessableToken& frame_token) const { |
| return base::Contains(awaiting_subframes, frame_token); |
| } |
| |
| base::FilePath |
| PaintPreviewClient::InProgressDocumentCaptureState::FilePathForFrame( |
| const base::UnguessableToken& frame_guid) const { |
| CHECK_EQ(persistence, RecordingPersistence::kFileSystem); |
| return root_dir.AppendASCII(base::StrCat({frame_guid.ToString(), ".skp"})); |
| } |
| |
| void PaintPreviewClient::InProgressDocumentCaptureState:: |
| PrepareRecordingRequestParams( |
| RecordingParams capture_params, |
| const base::UnguessableToken& frame_guid, |
| RecordingRequestParamsReadyCallback ready_callback) const { |
| if (persistence == RecordingPersistence::kFileSystem) { |
| const base::FilePath frame_filepath = FilePathForFrame(frame_guid); |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE}, |
| base::BindOnce(&CreateOrOverwriteFileForWriting, frame_filepath), |
| base::BindOnce(&OnSerializedRecordingFileCreated, |
| std::move(capture_params), frame_filepath, |
| std::move(ready_callback))); |
| } else { |
| mojom::PaintPreviewCaptureParamsPtr params = |
| CreateRecordingRequestParams(persistence, capture_params, {}); |
| std::move(ready_callback).Run(std::move(capture_params), std::move(params)); |
| } |
| } |
| |
| void PaintPreviewClient::InProgressDocumentCaptureState::RecordSuccessfulFrame( |
| const base::UnguessableToken& frame_guid, |
| bool is_main_frame, |
| mojom::PaintPreviewCaptureResponsePtr response) { |
| // Records the data from a processed frame if it was captured successfully. |
| had_success = true; |
| |
| PaintPreviewFrameProto* frame_proto; |
| if (frame_guid == root_frame_token) { |
| main_frame_blink_recording_time = response->blink_recording_time; |
| frame_proto = proto.mutable_root_frame(); |
| frame_proto->set_is_main_frame(is_main_frame); |
| } else { |
| frame_proto = proto.add_subframes(); |
| frame_proto->set_is_main_frame(false); |
| } |
| |
| if (persistence == RecordingPersistence::kFileSystem) { |
| // Safe since |filename| is always in the form: "{hexadecimal}.skp". |
| frame_proto->set_file_path(FilePathForFrame(frame_guid).AsUTF8Unsafe()); |
| } else { |
| DCHECK(response->skp.has_value()); |
| serialized_skps.insert({frame_guid, std::move(response->skp.value())}); |
| } |
| |
| std::vector<base::UnguessableToken> remote_frame_guids = |
| PaintPreviewCaptureResponseToPaintPreviewFrameProto( |
| std::move(response), frame_guid, frame_proto); |
| |
| for (const auto& remote_frame_guid : remote_frame_guids) { |
| // Don't wait again for a frame that was already captured. Also don't wait |
| // on frames that navigated during capture and have new embedding tokens. |
| if (!IsFinishedCapturing(remote_frame_guid) && |
| IsAllowedToCapture(remote_frame_guid)) { |
| awaiting_subframes.insert(remote_frame_guid); |
| } |
| } |
| } |
| |
| std::unique_ptr<CaptureResult> |
| PaintPreviewClient::InProgressDocumentCaptureState::IntoCaptureResult() && { |
| // Do not clean up files since we're about to return to the user. |
| should_clean_up_files = false; |
| |
| std::unique_ptr<CaptureResult> result = |
| std::make_unique<CaptureResult>(persistence); |
| result->proto = std::move(proto); |
| result->serialized_skps = std::move(serialized_skps); |
| result->capture_success = had_success; |
| return result; |
| } |
| |
| PaintPreviewClient::PaintPreviewClient(content::WebContents* web_contents) |
| : content::WebContentsUserData<PaintPreviewClient>(*web_contents), |
| content::WebContentsObserver(web_contents) {} |
| |
| PaintPreviewClient::~PaintPreviewClient() = default; |
| |
| void PaintPreviewClient::CapturePaintPreview( |
| PaintPreviewParams params, |
| content::RenderFrameHost* render_frame_host, |
| PaintPreviewCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| CHECK(callback); |
| auto document_data_it = |
| all_document_data_.find(params.inner.get_document_guid()); |
| if (document_data_it != all_document_data_.end()) { |
| std::move(callback).Run(params.inner.get_document_guid(), |
| mojom::PaintPreviewStatus::kGuidCollision, {}); |
| return; |
| } |
| if (!render_frame_host || params.inner.get_document_guid().is_empty()) { |
| std::move(callback).Run(params.inner.get_document_guid(), |
| mojom::PaintPreviewStatus::kFailed, {}); |
| return; |
| } |
| const GURL& url = render_frame_host->GetLastCommittedURL(); |
| if (!url.is_valid()) { |
| std::move(callback).Run(params.inner.get_document_guid(), |
| mojom::PaintPreviewStatus::kFailed, {}); |
| return; |
| } |
| |
| InProgressDocumentCaptureState document_data; |
| document_data.should_clean_up_files = true; |
| document_data.persistence = params.persistence; |
| document_data.root_dir = params.root_dir; |
| auto* metadata = document_data.proto.mutable_metadata(); |
| metadata->set_url(url.spec()); |
| metadata->set_version(kPaintPreviewVersion); |
| auto* chromeVersion = metadata->mutable_chrome_version(); |
| const auto& current_chrome_version = version_info::GetVersion(); |
| chromeVersion->set_major(current_chrome_version.components()[0]); |
| chromeVersion->set_minor(current_chrome_version.components()[1]); |
| chromeVersion->set_build(current_chrome_version.components()[2]); |
| chromeVersion->set_patch(current_chrome_version.components()[3]); |
| document_data.callback = base::BindOnce( |
| [](base::WeakPtr<PaintPreviewClient> client, |
| PaintPreviewCallback callback, base::UnguessableToken guid, |
| mojom::PaintPreviewStatus status, |
| std::unique_ptr<CaptureResult> result) { |
| if (client) { |
| client->all_document_data_.erase(guid); |
| } |
| std::move(callback).Run(guid, status, std::move(result)); |
| }, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback)); |
| |
| // Ensure the frame is not under prerendering state as the UKM cannot be |
| // recorded while prerendering. Current callers pass frames that are under |
| // the primary page. |
| CHECK(!render_frame_host->IsInLifecycleState( |
| content::RenderFrameHost::LifecycleState::kPrerendering)); |
| document_data.source_id = render_frame_host->GetPageUkmSourceId(); |
| |
| document_data.accepted_tokens = CreateAcceptedTokenList(render_frame_host); |
| auto token = render_frame_host->GetEmbeddingToken(); |
| CHECK(token.has_value()) |
| << "Error: Root frame does not have an embedding token."; |
| document_data.root_frame_token = token.value(); |
| document_data.capture_links = params.inner.capture_links; |
| document_data.max_per_capture_size = params.inner.max_capture_size; |
| document_data.max_decoded_image_size_bytes = |
| params.inner.max_decoded_image_size_bytes; |
| document_data.skip_accelerated_content = |
| params.inner.skip_accelerated_content; |
| document_data.redaction_params = params.inner.redaction_params; |
| document_data_it = all_document_data_.insert( |
| document_data_it, |
| {params.inner.get_document_guid(), std::move(document_data)}); |
| TRACE_EVENT_BEGIN("paint_preview", "PaintPreviewClient::CapturePaintPreview", |
| perfetto::Track::FromPointer(&document_data_it->second)); |
| CapturePaintPreviewInternal(std::move(params.inner), render_frame_host, |
| document_data_it->second); |
| } |
| |
| void PaintPreviewClient::CaptureSubframePaintPreview( |
| const base::UnguessableToken& guid, |
| const gfx::Rect& rect, |
| content::RenderFrameHost* render_subframe_host) { |
| if (guid.is_empty() || !render_subframe_host || rect.IsEmpty()) { |
| return; |
| } |
| |
| auto* document_data = base::FindOrNull(all_document_data_, guid); |
| if (!document_data) { |
| // If the screenshot has already failed, there's no need to request a |
| // subframe paint. |
| return; |
| } |
| CHECK(document_data->callback); |
| |
| RecordingParams params(guid); |
| params.clip_rect = rect; |
| // Note: no need to set `params.clip_x_coordinate_override` or |
| // `params.clip_y_coordinate_override` for subframes. |
| params.is_main_frame = false; |
| params.capture_links = document_data->capture_links; |
| params.max_capture_size = document_data->max_per_capture_size; |
| params.max_decoded_image_size_bytes = |
| document_data->max_decoded_image_size_bytes; |
| params.skip_accelerated_content = document_data->skip_accelerated_content; |
| |
| if (document_data->redaction_params.ShouldRedactSubframe( |
| render_subframe_host->GetLastCommittedOrigin())) { |
| BeginSubframeRedaction(guid, std::move(params), render_subframe_host, |
| *document_data); |
| return; |
| } |
| |
| CapturePaintPreviewInternal(std::move(params), render_subframe_host, |
| *document_data); |
| } |
| |
| void PaintPreviewClient::BeginSubframeRedaction( |
| const base::UnguessableToken& guid, |
| RecordingParams params, |
| content::RenderFrameHost* render_subframe_host, |
| InProgressDocumentCaptureState& document_data) { |
| std::optional<base::UnguessableToken> token = |
| render_subframe_host->GetEmbeddingToken(); |
| if (!token.has_value()) { |
| DVLOG(1) << "Error: Attempted to capture a frame without an " |
| "embedding token."; |
| DUMP_WILL_BE_NOTREACHED(); |
| return; |
| } |
| base::UnguessableToken frame_guid = token.value(); |
| |
| // This bookkeeping would normally be done by `RequestCaptureOnUIThread`, |
| // but we're skipping that, so we do it here instead. |
| AwaitSubframeCapture(frame_guid, params, document_data); |
| |
| auto geometry_metadata_params = mojom::GeometryMetadataParams::New(); |
| geometry_metadata_params->clip_rect = params.clip_rect; |
| // All clip rects are treated as hints for now. |
| geometry_metadata_params->clip_rect_is_hint = true; |
| // Same as defaults, but just to be explicit: |
| geometry_metadata_params->clip_x_coord_override = |
| mojom::ClipCoordOverride::kNone; |
| geometry_metadata_params->clip_y_coord_override = |
| mojom::ClipCoordOverride::kNone; |
| |
| content::GlobalRenderFrameHostId render_frame_id = |
| GetGlobalRenderFrameHostId(render_subframe_host); |
| |
| GetOrInsertRecorder(frame_guid, *render_subframe_host) |
| ->GetGeometryMetadata( |
| std::move(geometry_metadata_params), |
| base::BindOnce( |
| &PaintPreviewClient::RedactSubframe, |
| weak_ptr_factory_.GetWeakPtr(), frame_guid, render_frame_id, |
| std::move(params), |
| base::BindOnce( |
| &PaintPreviewClient::OnPaintPreviewCapturedCallback, |
| weak_ptr_factory_.GetWeakPtr(), frame_guid, |
| render_frame_id))); |
| } |
| |
| void PaintPreviewClient::RedactSubframe( |
| const base::UnguessableToken& frame_guid, |
| const content::GlobalRenderFrameHostId& render_frame_id, |
| RecordingParams params, |
| base::OnceCallback< |
| void(RecordingParams, |
| base::expected<mojom::PaintPreviewCaptureResponsePtr, |
| mojom::PaintPreviewStatus>)> callback, |
| mojom::GeometryMetadataResponsePtr response) { |
| auto* document_data = |
| base::FindOrNull(all_document_data_, params.get_document_guid()); |
| if (!document_data) { |
| return; |
| } |
| |
| auto* render_frame_host = content::RenderFrameHost::FromID(render_frame_id); |
| |
| if (!render_frame_host || |
| render_frame_host->GetEmbeddingToken().value_or( |
| base::UnguessableToken::Null()) != frame_guid || |
| !response) { |
| std::move(callback).Run( |
| std::move(params), |
| base::unexpected(mojom::PaintPreviewStatus::kCaptureFailed)); |
| return; |
| } |
| |
| auto capture_response = mojom::PaintPreviewCaptureResponse::New(); |
| capture_response->embedding_token = frame_guid; |
| capture_response->geometry_metadata = std::move(response); |
| |
| switch (document_data->persistence) { |
| case RecordingPersistence::kFileSystem: { |
| gfx::Size clip_rect_size = params.clip_rect.size(); |
| std::optional<size_t> max_capture_size = |
| params.max_capture_size == 0U |
| ? std::nullopt |
| : std::make_optional(params.max_capture_size); |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()}, |
| base::BindOnce(&SerializeRedactedFrameToFile, clip_rect_size, |
| max_capture_size, |
| document_data->FilePathForFrame(frame_guid), |
| std::move(capture_response)), |
| base::BindOnce(std::move(callback), std::move(params))); |
| return; |
| } |
| case RecordingPersistence::kMemoryBuffer: { |
| std::optional<BufferAndMetadata> serialized = |
| SerializeRedactedFrameToBuffer( |
| params.clip_rect.size(), |
| document_data->max_per_capture_size == 0 |
| ? std::nullopt |
| : std::make_optional(document_data->max_per_capture_size)); |
| if (!serialized) { |
| std::move(callback).Run( |
| std::move(params), |
| base::unexpected(mojom::PaintPreviewStatus::kCaptureFailed)); |
| return; |
| } |
| |
| capture_response->skp.emplace(std::move(serialized->buffer)); |
| capture_response->serialized_size = serialized->buffer_size; |
| |
| std::move(callback).Run(std::move(params), std::move(capture_response)); |
| return; |
| } |
| } |
| NOTREACHED(); |
| } |
| |
| void PaintPreviewClient::AwaitSubframeCapture( |
| const base::UnguessableToken& frame_guid, |
| const RecordingParams& params, |
| InProgressDocumentCaptureState& document_data) { |
| document_data.awaiting_subframes.insert(frame_guid); |
| pending_previews_on_subframe_[frame_guid].insert(params.get_document_guid()); |
| } |
| |
| void PaintPreviewClient::RenderFrameDeleted( |
| content::RenderFrameHost* render_frame_host) { |
| // TODO(crbug.com/40115832): Investigate possible issues with cleanup if just |
| // a single subframe gets deleted. |
| auto maybe_token = render_frame_host->GetEmbeddingToken(); |
| if (!maybe_token.has_value()) { |
| return; |
| } |
| |
| bool is_main_frame = render_frame_host->GetParentOrOuterDocument() == nullptr; |
| base::UnguessableToken frame_guid = maybe_token.value(); |
| auto* tokens = base::FindOrNull(pending_previews_on_subframe_, frame_guid); |
| if (!tokens) { |
| return; |
| } |
| |
| for (const auto& document_guid : *tokens) { |
| auto* document_data = base::FindOrNull(all_document_data_, document_guid); |
| if (!document_data) { |
| continue; |
| } |
| |
| document_data->awaiting_subframes.erase(frame_guid); |
| document_data->finished_subframes.insert(frame_guid); |
| document_data->had_error = true; |
| if (document_data->awaiting_subframes.empty() || is_main_frame) { |
| if (is_main_frame) { |
| for (const auto& subframe_guid : document_data->awaiting_subframes) { |
| auto subframe_docs = pending_previews_on_subframe_[subframe_guid]; |
| subframe_docs.erase(document_guid); |
| if (subframe_docs.empty()) { |
| pending_previews_on_subframe_.erase(subframe_guid); |
| } |
| } |
| document_data->awaiting_subframes.clear(); |
| } |
| interface_ptrs_.erase(frame_guid); |
| OnFinished(document_guid, *document_data); |
| } |
| } |
| pending_previews_on_subframe_.erase(frame_guid); |
| } |
| |
| void PaintPreviewClient::CapturePaintPreviewInternal( |
| RecordingParams params, |
| content::RenderFrameHost* render_frame_host, |
| const InProgressDocumentCaptureState& document_data) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| // Use a frame's embedding token as its GUID. |
| auto token = render_frame_host->GetEmbeddingToken(); |
| |
| // This should be impossible, but if it happens in a release build just abort. |
| if (!token.has_value()) { |
| DVLOG(1) << "Error: Attempted to capture a frame without an " |
| "embedding token."; |
| DUMP_WILL_BE_NOTREACHED(); |
| return; |
| } |
| |
| // The embedding token should be in the list of tokens in the tree when |
| // capture was started. If this is not the case then the frame may have |
| // navigated. This is unsafe to capture. |
| base::UnguessableToken frame_guid = token.value(); |
| if (!document_data.IsAllowedToCapture(frame_guid)) { |
| return; |
| } |
| |
| // Deduplicate data if a subframe is required multiple times. |
| if (document_data.IsCaptureInProgress(frame_guid) || |
| document_data.IsFinishedCapturing(frame_guid)) { |
| return; |
| } |
| |
| document_data.PrepareRecordingRequestParams( |
| std::move(params), frame_guid, |
| base::BindOnce(&PaintPreviewClient::RequestCaptureOnUIThread, |
| weak_ptr_factory_.GetWeakPtr(), frame_guid, |
| GetGlobalRenderFrameHostId(render_frame_host))); |
| } |
| |
| void PaintPreviewClient::RequestCaptureOnUIThread( |
| const base::UnguessableToken& frame_guid, |
| const content::GlobalRenderFrameHostId& render_frame_id, |
| RecordingParams params, |
| base::expected<mojom::PaintPreviewCaptureParamsPtr, |
| mojom::PaintPreviewStatus> capture_params) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| auto* document_data = |
| base::FindOrNull(all_document_data_, params.get_document_guid()); |
| if (!document_data) { |
| return; |
| } |
| CHECK(document_data->callback); |
| |
| if (!capture_params.has_value()) { |
| std::move(document_data->callback) |
| .Run(params.get_document_guid(), capture_params.error(), {}); |
| return; |
| } |
| |
| // If the RenderFrameHost navigated or is no longer around treat this as a |
| // failure as a navigation occurring during capture is bad. |
| auto* render_frame_host = content::RenderFrameHost::FromID(render_frame_id); |
| |
| if (!render_frame_host || |
| render_frame_host->GetEmbeddingToken().value_or( |
| base::UnguessableToken::Null()) != frame_guid || |
| !capture_params.value()) { |
| std::move(document_data->callback) |
| .Run(params.get_document_guid(), |
| mojom::PaintPreviewStatus::kCaptureFailed, {}); |
| return; |
| } |
| |
| AwaitSubframeCapture(frame_guid, params, *document_data); |
| |
| // For the main frame, apply a clip rect if one is provided. |
| if (params.is_main_frame) { |
| capture_params.value()->geometry_metadata_params->clip_rect_is_hint = false; |
| } |
| |
| GetOrInsertRecorder(frame_guid, *render_frame_host) |
| ->CapturePaintPreview( |
| std::move(capture_params).value(), |
| base::BindOnce(&PaintPreviewClient::OnPaintPreviewCapturedCallback, |
| weak_ptr_factory_.GetWeakPtr(), frame_guid, |
| render_frame_id, std::move(params))); |
| } |
| |
| mojo::AssociatedRemote<mojom::PaintPreviewRecorder>& |
| PaintPreviewClient::GetOrInsertRecorder( |
| const base::UnguessableToken& frame_guid, |
| content ::RenderFrameHost& render_frame_host) { |
| CHECK_EQ(render_frame_host.GetEmbeddingToken().value_or( |
| base::UnguessableToken::Null()), |
| frame_guid); |
| mojo::AssociatedRemote<mojom::PaintPreviewRecorder>& recorder_remote = |
| interface_ptrs_[frame_guid]; |
| if (!recorder_remote.is_bound()) { |
| render_frame_host.GetRemoteAssociatedInterfaces()->GetInterface( |
| &recorder_remote); |
| } |
| return recorder_remote; |
| } |
| |
| void PaintPreviewClient::OnPaintPreviewCapturedCallback( |
| const base::UnguessableToken& frame_guid, |
| const content::GlobalRenderFrameHostId& render_frame_id, |
| RecordingParams params, |
| base::expected<mojom::PaintPreviewCaptureResponsePtr, |
| mojom::PaintPreviewStatus> response) { |
| auto* document_data = |
| base::FindOrNull(all_document_data_, params.get_document_guid()); |
| |
| // There is no retry logic so always treat a frame as processed regardless of |
| // |status| |
| MarkFrameAsProcessed(params.get_document_guid(), frame_guid, document_data); |
| |
| // If the RenderFrameHost navigated or is no longer around treat this as a |
| // failure as a navigation occurring during capture is bad. |
| auto* render_frame_host = content::RenderFrameHost::FromID(render_frame_id); |
| if (!render_frame_host || render_frame_host->GetEmbeddingToken().value_or( |
| base::UnguessableToken::Null()) != frame_guid) { |
| response = base::unexpected(mojom::PaintPreviewStatus::kCaptureFailed); |
| } |
| |
| if (!document_data) { |
| return; |
| } |
| |
| if (response.has_value()) { |
| document_data->RecordSuccessfulFrame(frame_guid, params.is_main_frame, |
| std::move(response).value()); |
| } else { |
| document_data->had_error = true; |
| |
| // If this is the main frame we should just abort the capture on failure. |
| if (params.is_main_frame) { |
| document_data->awaiting_subframes.clear(); |
| OnFinished(params.get_document_guid(), *document_data); |
| return; |
| } |
| } |
| |
| if (document_data->awaiting_subframes.empty()) { |
| OnFinished(params.get_document_guid(), *document_data); |
| } |
| } |
| |
| void PaintPreviewClient::MarkFrameAsProcessed( |
| base::UnguessableToken guid, |
| const base::UnguessableToken& frame_guid, |
| InProgressDocumentCaptureState* document_data) { |
| auto& tokens = pending_previews_on_subframe_[frame_guid]; |
| tokens.erase(guid); |
| if (tokens.empty()) { |
| interface_ptrs_.erase(frame_guid); |
| } |
| if (!document_data) { |
| return; |
| } |
| document_data->finished_subframes.insert(frame_guid); |
| document_data->awaiting_subframes.erase(frame_guid); |
| } |
| |
| void PaintPreviewClient::OnFinished( |
| base::UnguessableToken guid, |
| InProgressDocumentCaptureState& document_data) { |
| CHECK_EQ(document_data.awaiting_subframes.size(), 0U); |
| CHECK(document_data.callback); |
| |
| if (!PaintPreviewProtoValid(document_data.proto)) { |
| document_data.had_success = false; |
| } |
| |
| TRACE_EVENT_END("paint_preview", perfetto::Track::FromPointer(&document_data), |
| "success", document_data.had_success, "subframes", |
| document_data.finished_subframes.size()); |
| |
| base::UmaHistogramBoolean("Browser.PaintPreview.Capture.Success", |
| document_data.had_success); |
| if (document_data.had_success) { |
| base::UmaHistogramCounts100( |
| "Browser.PaintPreview.Capture.NumberOfFramesCaptured", |
| document_data.finished_subframes.size()); |
| |
| RecordUkmCaptureData(document_data.source_id, |
| document_data.main_frame_blink_recording_time); |
| |
| // At a minimum one frame was captured successfully, it is up to the |
| // caller to decide if a partial success is acceptable based on what is |
| // contained in the proto. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(document_data.callback), guid, |
| document_data.had_error |
| ? mojom::PaintPreviewStatus::kPartialSuccess |
| : mojom::PaintPreviewStatus::kOk, |
| std::move(document_data).IntoCaptureResult())); |
| } else { |
| // A proto could not be created indicating all frames failed to capture. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(document_data.callback), guid, |
| mojom::PaintPreviewStatus::kFailed, nullptr)); |
| } |
| all_document_data_.erase(guid); |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PaintPreviewClient); |
| |
| } // namespace paint_preview |