| // Copyright 2020 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/extensions/api/printing/print_job_submitter.h" |
| |
| #include <cstring> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/types/expected.h" |
| #include "base/values.h" |
| #include "chrome/browser/extensions/api/printing/printing_api_utils.h" |
| #include "chrome/browser/printing/pdf_blob_data_flattener.h" |
| #include "chrome/browser/printing/print_job_controller.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/extensions/extensions_dialogs.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/crosapi/mojom/local_printer.mojom.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "extensions/browser/blob_reader.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/image_loader.h" |
| #include "extensions/common/extension.h" |
| #include "printing/metafile_skia.h" |
| #include "printing/print_settings.h" |
| #include "printing/printing_utils.h" |
| #include "third_party/skia/include/codec/SkCodec.h" |
| #include "third_party/skia/include/codec/SkPngRustDecoder.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkImage.h" |
| #include "third_party/skia/include/core/SkRefCnt.h" |
| #include "third_party/skia/include/core/SkStream.h" |
| #include "third_party/skia/include/docs/SkPDFDocument.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/skia_span_util.h" |
| #include "ui/native_window_tracker/native_window_tracker.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| constexpr char kPdfMimeType[] = "application/pdf"; |
| constexpr char kPngMimeType[] = "image/png"; |
| |
| constexpr char kUnsupportedContentType[] = "Unsupported content type"; |
| constexpr char kInvalidTicket[] = "Invalid ticket"; |
| constexpr char kInvalidPrinterId[] = "Invalid printer ID"; |
| constexpr char kPrinterUnavailable[] = "Printer is unavailable at the moment"; |
| constexpr char kUnsupportedTicket[] = |
| "Ticket is unsupported on the given printer"; |
| constexpr char kInvalidData[] = "Invalid document"; |
| constexpr char kPrintingFailed[] = "Printing failed"; |
| |
| constexpr int kIconSize = 64; |
| |
| // There is no easy way to interact with UI dialogs, so we want to have an |
| // ability to skip this stage for browser tests. |
| bool g_skip_confirmation_dialog_for_testing = false; |
| |
| // Returns true if print job request dialog should be shown. |
| bool IsUserConfirmationRequired(content::BrowserContext* browser_context, |
| const std::string& extension_id) { |
| if (g_skip_confirmation_dialog_for_testing) |
| return false; |
| const base::Value::List& list = |
| Profile::FromBrowserContext(browser_context) |
| ->GetPrefs() |
| ->GetList(prefs::kPrintingAPIExtensionsAllowlist); |
| return !base::Contains(list, base::Value(extension_id)); |
| } |
| |
| } // namespace |
| |
| PrintJobSubmitter::PrintJobSubmitter( |
| gfx::NativeWindow native_window, |
| content::BrowserContext* browser_context, |
| printing::PrintJobController* print_job_controller, |
| printing::PdfBlobDataFlattener* pdf_blob_data_flattener, |
| scoped_refptr<const extensions::Extension> extension, |
| api::printing::SubmitJobRequest request, |
| crosapi::mojom::LocalPrinter* local_printer, |
| SubmitJobCallback callback) |
| : native_window_(native_window), |
| browser_context_(browser_context), |
| print_job_controller_(print_job_controller), |
| pdf_blob_data_flattener_(*pdf_blob_data_flattener), |
| extension_(extension), |
| request_(std::move(request)), |
| local_printer_(local_printer), |
| callback_(std::move(callback)) { |
| DCHECK(extension); |
| if (native_window) |
| native_window_tracker_ = ui::NativeWindowTracker::Create(native_window); |
| } |
| |
| PrintJobSubmitter::~PrintJobSubmitter() = default; |
| |
| // static |
| void PrintJobSubmitter::Run(std::unique_ptr<PrintJobSubmitter> submitter) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(submitter->callback_); |
| PrintJobSubmitter* ptr = submitter.get(); |
| ptr->callback_ = std::move(ptr->callback_) |
| .Then(base::OnceClosure( |
| base::DoNothingWithBoundArgs(std::move(submitter)))); |
| ptr->Start(); |
| } |
| |
| void PrintJobSubmitter::Start() { |
| if (!CheckContentType()) { |
| FireErrorCallback(kUnsupportedContentType); |
| return; |
| } |
| if (!CheckPrintTicket()) { |
| FireErrorCallback(kInvalidTicket); |
| return; |
| } |
| CheckPrinter(); |
| } |
| |
| bool PrintJobSubmitter::CheckContentType() const { |
| return request_.job.content_type == kPdfMimeType || |
| request_.job.content_type == kPngMimeType; |
| } |
| |
| bool PrintJobSubmitter::CheckPrintTicket() { |
| settings_ = ParsePrintTicket(request_.job.ticket.ToValue()); |
| if (!settings_) |
| return false; |
| settings_->set_title(base::UTF8ToUTF16(request_.job.title)); |
| settings_->set_device_name(base::UTF8ToUTF16(request_.job.printer_id)); |
| return true; |
| } |
| |
| void PrintJobSubmitter::CheckPrinter() { |
| CHECK(local_printer_); |
| local_printer_->GetCapability( |
| request_.job.printer_id, |
| base::BindOnce(&PrintJobSubmitter::CheckCapabilitiesCompatibility, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PrintJobSubmitter::CheckCapabilitiesCompatibility( |
| crosapi::mojom::CapabilitiesResponsePtr caps) { |
| if (!caps) { |
| FireErrorCallback(kInvalidPrinterId); |
| return; |
| } |
| printer_name_ = base::UTF8ToUTF16(caps->basic_info->name); |
| if (!caps->capabilities) { |
| FireErrorCallback(kPrinterUnavailable); |
| return; |
| } |
| if (!CheckSettingsAndCapabilitiesCompatibility(*settings_, |
| *caps->capabilities)) { |
| FireErrorCallback(kUnsupportedTicket); |
| return; |
| } |
| ReadDocumentData(); |
| } |
| |
| void PrintJobSubmitter::ReadDocumentData() { |
| CHECK(request_.document_blob_uuid); |
| if (request_.job.content_type == kPdfMimeType) { |
| pdf_blob_data_flattener_->ReadAndFlattenPdf( |
| browser_context_->GetBlobRemote(*request_.document_blob_uuid), |
| base::BindOnce(&PrintJobSubmitter::OnPdfReadAndFlattened, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| BlobReader::Read( |
| browser_context_->GetBlobRemote(*request_.document_blob_uuid), |
| base::BindOnce(&PrintJobSubmitter::OnImageDataRead, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void PrintJobSubmitter::OnPdfReadAndFlattened( |
| std::unique_ptr<printing::FlattenPdfResult> result) { |
| if (!result) { |
| FireErrorCallback(kInvalidData); |
| return; |
| } |
| |
| flatten_pdf_result_ = std::move(result); |
| |
| // Directly submit the job if the extension is allowed. |
| if (!IsUserConfirmationRequired(browser_context_, extension_->id())) { |
| StartPrintJob(); |
| return; |
| } |
| extensions::ImageLoader::Get(browser_context_) |
| ->LoadImageAtEveryScaleFactorAsync( |
| extension_.get(), gfx::Size(kIconSize, kIconSize), |
| base::BindOnce(&PrintJobSubmitter::ShowPrintJobConfirmationDialog, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // Handle PNG input by converting it to PDF, and then sending the resulting |
| // PDF to the printer like we would if it had been submitted directly. |
| void PrintJobSubmitter::OnImageDataRead(std::string data, |
| int64_t /*blob_total_size*/) { |
| // Note: `data` must outlive `image_data` and `codec`. |
| std::unique_ptr<SkMemoryStream> image_data = std::make_unique<SkMemoryStream>( |
| gfx::MakeSkDataFromSpanWithoutCopy(base::as_byte_span(data))); |
| std::unique_ptr<SkCodec> codec = |
| SkPngRustDecoder::Decode(std::move(image_data), nullptr); |
| if (!codec) { |
| LOG(WARNING) << "Failed to decode PNG"; |
| FireErrorCallback(kInvalidData); |
| return; |
| } |
| |
| auto img_tuple = codec->getImage(); |
| CHECK_EQ(std::get<1>(img_tuple), SkCodec::Result::kSuccess); |
| sk_sp<SkImage> image = std::get<0>(img_tuple); |
| |
| SkDynamicMemoryWStream buffer; |
| SkPDF::Metadata metadata; |
| auto pdf_document = SkPDF::MakeDocument(&buffer, metadata); |
| CHECK(pdf_document); |
| SkCanvas* canvas = pdf_document->beginPage(image->width(), image->height()); |
| canvas->drawImage(image, 0, 0); |
| pdf_document->endPage(); |
| pdf_document->close(); |
| |
| // The generated PDF consists of a single image and does not contain forms, |
| // JavaScript, etc. So it is already flattened, and can be treated as such. |
| sk_sp<SkData> pdf_data = buffer.detachAsData(); |
| auto metafile = std::make_unique<printing::MetafileSkia>(); |
| CHECK(metafile->InitFromData(gfx::SkDataToSpan(pdf_data))); |
| OnPdfReadAndFlattened( |
| std::make_unique<printing::FlattenPdfResult>(std::move(metafile), 1)); |
| } |
| |
| void PrintJobSubmitter::ShowPrintJobConfirmationDialog( |
| const gfx::Image& extension_icon) { |
| // If the browser window was closed during API request handling, change |
| // |native_window_| appropriately. |
| if (native_window_tracker_ && |
| native_window_tracker_->WasNativeWindowDestroyed()) |
| native_window_ = gfx::NativeWindow(); |
| |
| extensions::ShowPrintJobConfirmationDialog( |
| native_window_, extension_->id(), base::UTF8ToUTF16(extension_->name()), |
| extension_icon.AsImageSkia(), settings_->title(), printer_name_, |
| base::BindOnce(&PrintJobSubmitter::OnPrintJobConfirmationDialogClosed, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PrintJobSubmitter::OnPrintJobConfirmationDialogClosed(bool accepted) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(callback_); |
| // If the user hasn't accepted a print job or the extension is |
| // unloaded/disabled by the time the dialog is closed, reject the request. |
| if (!accepted || !ExtensionRegistry::Get(browser_context_) |
| ->enabled_extensions() |
| .Contains(extension_->id())) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback_), base::unexpected(std::nullopt))); |
| return; |
| } |
| StartPrintJob(); |
| } |
| |
| void PrintJobSubmitter::StartPrintJob() { |
| CHECK(extension_); |
| CHECK(settings_); |
| CHECK(flatten_pdf_result_); |
| |
| auto flatten_pdf_result = std::move(flatten_pdf_result_); |
| uint32_t page_count = flatten_pdf_result->page_count; |
| print_job_controller_->CreatePrintJob( |
| std::move(flatten_pdf_result->flattened_pdf), std::move(settings_), |
| page_count, crosapi::mojom::PrintJob::Source::kExtension, |
| extension_->id(), |
| base::BindOnce(&PrintJobSubmitter::OnPrintJobCreated, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PrintJobSubmitter::OnPrintJobCreated( |
| std::optional<printing::PrintJobCreatedInfo> info) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!info) { |
| FireErrorCallback(kPrintingFailed); |
| return; |
| } |
| DCHECK(callback_); |
| std::move(callback_).Run(std::move(*info)); |
| } |
| |
| void PrintJobSubmitter::FireErrorCallback(const std::string& error) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(callback_); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback_), base::unexpected(error))); |
| } |
| |
| // static |
| base::AutoReset<bool> PrintJobSubmitter::DisablePdfFlatteningForTesting() { |
| return printing::PdfBlobDataFlattener::DisablePdfFlatteningForTesting(); |
| } |
| |
| // static |
| void PrintJobSubmitter::SkipConfirmationDialogForTesting() { |
| g_skip_confirmation_dialog_for_testing = true; |
| } |
| |
| } // namespace extensions |