| // 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/printing/web_api/web_printing_service_chromeos.h" |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/map_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/chromeos/printing/cups_wrapper.h" |
| #include "chrome/browser/printing/local_printer_utils_chromeos.h" |
| #include "chrome/browser/printing/pdf_blob_data_flattener.h" |
| #include "chrome/browser/printing/print_job_controller.h" |
| #include "chrome/browser/printing/web_api/web_printing_type_converters.h" |
| #include "chrome/browser/printing/web_api/web_printing_utils.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/permissions/permission_request_data.h" |
| #include "content/public/browser/permission_controller.h" |
| #include "content/public/browser/permission_descriptor_util.h" |
| #include "content/public/browser/permission_result.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "printing/backend/cups_ipp_constants.h" |
| #include "printing/backend/print_backend.h" |
| #include "printing/metafile_skia.h" |
| #include "printing/print_settings.h" |
| #include "printing/printed_document.h" |
| #include "third_party/blink/public/mojom/permissions/permission_status.mojom-shared.h" |
| |
| namespace printing { |
| |
| namespace { |
| |
| blink::mojom::WebPrinterAttributesPtr ConvertResponse( |
| crosapi::mojom::CapabilitiesResponsePtr response) { |
| if (!response || !response->capabilities) { |
| return nullptr; |
| } |
| return blink::mojom::WebPrinterAttributes::From(*response->capabilities); |
| } |
| |
| std::optional<PrinterSemanticCapsAndDefaults> ExtractCapsAndDefaults( |
| crosapi::mojom::CapabilitiesResponsePtr response) { |
| return response ? response->capabilities : std::nullopt; |
| } |
| |
| bool IsDuplexModeKnown(mojom::DuplexMode duplex_mode) { |
| return duplex_mode != mojom::DuplexMode::kUnknownDuplexMode; |
| } |
| |
| bool IsColorModelKnown(mojom::ColorModel color_model) { |
| return color_model != mojom::ColorModel::kUnknownColorModel; |
| } |
| |
| bool ValidateMediaCol( |
| const PrintSettings& pjt_attributes, |
| const PrinterSemanticCapsAndDefaults& printer_attributes) { |
| // media-size / media-size-name: |
| const auto& media = pjt_attributes.requested_media(); |
| if (media.IsDefault()) { |
| // Means nothing has actually been requested. |
| return true; |
| } |
| const auto& papers = printer_attributes.papers; |
| // Validate that the requested paper is supported by the printer. |
| if (!std::ranges::any_of(papers, [&](const auto& paper) { |
| return paper.IsSizeWithinBounds(media.size_microns); |
| })) { |
| return false; |
| } |
| return true; |
| } |
| |
| void UpdatePrintJobTemplateAttributesWithPrinterDefaults( |
| PrintSettings& pjt_attributes, |
| const PrinterSemanticCapsAndDefaults& printer_attributes) { |
| if (!IsDuplexModeKnown(pjt_attributes.duplex_mode())) { |
| pjt_attributes.set_duplex_mode(printer_attributes.duplex_default); |
| } |
| if (!IsColorModelKnown(pjt_attributes.color())) { |
| pjt_attributes.set_color(printer_attributes.color_default |
| ? mojom::ColorModel::kColorModeColor |
| : mojom::ColorModel::kColorModeMonochrome); |
| } |
| } |
| |
| bool ValidateAdvancedCapability( |
| const PrintSettings& pjt_attributes, |
| const PrinterSemanticCapsAndDefaults& printer_attributes, |
| const std::string& capability_name) { |
| auto* requested_capability = |
| base::FindOrNull(pjt_attributes.advanced_settings(), capability_name); |
| if (!requested_capability) { |
| // If the capability has not been actually requested, we're good. |
| return true; |
| } |
| auto* printer_capability = |
| internal::FindAdvancedCapability(printer_attributes, capability_name); |
| if (!printer_capability) { |
| // If the capability has been requested but the printer doesn't support it, |
| // reject. |
| return false; |
| } |
| // `requested_capability` is guaranteed to be a string -- it's set this way in |
| // StructTraits<>. |
| return base::Contains(printer_capability->values, |
| requested_capability->GetString(), |
| &AdvancedCapabilityValue::name); |
| } |
| |
| bool ValidateAttributesAndUpdateIfNecessary( |
| PrintSettings& pjt_attributes, |
| const PrinterSemanticCapsAndDefaults& printer_attributes) { |
| if (pjt_attributes.copies() < 1 || |
| pjt_attributes.copies() > printer_attributes.copies_max) { |
| return false; |
| } |
| if (pjt_attributes.collate() && !printer_attributes.collate_capable) { |
| return false; |
| } |
| // Checks that printer supports color printing if requested so. |
| if (IsColorModelKnown(pjt_attributes.color()) && |
| ::printing::IsColorModelSelected(pjt_attributes.color()).value() && |
| !IsColorModelKnown(printer_attributes.color_model)) { |
| return false; |
| } |
| if (IsDuplexModeKnown(pjt_attributes.duplex_mode()) && |
| !base::Contains(printer_attributes.duplex_modes, |
| pjt_attributes.duplex_mode())) { |
| return false; |
| } |
| if (!IsDuplexModeKnown(pjt_attributes.duplex_mode()) && |
| !IsDuplexModeKnown(printer_attributes.duplex_default)) { |
| return false; |
| } |
| if (!pjt_attributes.dpi_size().IsZero() && |
| !base::Contains(printer_attributes.dpis, pjt_attributes.dpi_size())) { |
| return false; |
| } |
| if (!ValidateMediaCol(pjt_attributes, printer_attributes)) { |
| return false; |
| } |
| if (!ValidateAdvancedCapability(pjt_attributes, printer_attributes, |
| kIppMediaSource)) { |
| return false; |
| } |
| // Update selected fields to printer defaults if they're not specified. |
| UpdatePrintJobTemplateAttributesWithPrinterDefaults(pjt_attributes, |
| printer_attributes); |
| return true; |
| } |
| |
| blink::mojom::WebPrinterAttributesPtr MergePrinterAttributesAndStatus( |
| blink::mojom::WebPrinterAttributesPtr printer_attributes, |
| std::unique_ptr<PrinterStatus> printer_status) { |
| if (!printer_status) { |
| // Even though `printer_attributes` were successfully fetched, it's better |
| // to play safe here and pretend that the entire request has failed. |
| return nullptr; |
| } |
| printer_attributes->printer_state = printer_status->state; |
| auto& printer_state_reasons = printer_attributes->printer_state_reasons; |
| for (const auto& reason : printer_status->reasons) { |
| printer_state_reasons.push_back(reason.reason); |
| } |
| std::ranges::sort(printer_state_reasons); |
| auto repeated = std::ranges::unique(printer_state_reasons); |
| printer_state_reasons.erase(repeated.begin(), repeated.end()); |
| printer_attributes->printer_state_message = printer_status->message; |
| return printer_attributes; |
| } |
| |
| bool HasPrintingPermission(content::RenderFrameHost& rfh) { |
| return rfh.GetBrowserContext() |
| ->GetPermissionController() |
| ->GetPermissionStatusForCurrentDocument( |
| content::PermissionDescriptorUtil:: |
| CreatePermissionDescriptorForPermissionType( |
| blink::PermissionType::WEB_PRINTING), |
| &rfh) == blink::mojom::PermissionStatus::GRANTED; |
| } |
| |
| void InvokeFetchAttributesCallback( |
| WebPrintingServiceChromeOS::FetchAttributesCallback callback, |
| blink::mojom::WebPrinterAttributesPtr printer_attributes) { |
| if (!printer_attributes) { |
| std::move(callback).Run(blink::mojom::WebPrinterFetchResult::NewError( |
| blink::mojom::WebPrinterFetchError::kPrinterUnreachable)); |
| return; |
| } |
| std::move(callback).Run( |
| blink::mojom::WebPrinterFetchResult::NewPrinterAttributes( |
| std::move(printer_attributes))); |
| } |
| |
| } // namespace |
| |
| WebPrintingServiceChromeOS::WebPrintingServiceChromeOS( |
| content::RenderFrameHost* render_frame_host, |
| mojo::PendingReceiver<blink::mojom::WebPrintingService> receiver, |
| const std::string& app_id) |
| : DocumentService(*render_frame_host, std::move(receiver)), |
| app_id_(app_id), |
| cups_wrapper_(chromeos::CupsWrapper::Create()), |
| pdf_flattener_(std::make_unique<PdfBlobDataFlattener>( |
| Profile::FromBrowserContext(render_frame_host->GetBrowserContext()))), |
| print_job_controller_(std::make_unique<PrintJobController>()) {} |
| |
| WebPrintingServiceChromeOS::~WebPrintingServiceChromeOS() = default; |
| |
| void WebPrintingServiceChromeOS::GetPrinters(GetPrintersCallback callback) { |
| render_frame_host() |
| .GetBrowserContext() |
| ->GetPermissionController() |
| ->RequestPermissionFromCurrentDocument( |
| &render_frame_host(), |
| content::PermissionRequestDescription( |
| content::PermissionDescriptorUtil:: |
| CreatePermissionDescriptorForPermissionType( |
| blink::PermissionType::WEB_PRINTING)), |
| base::BindOnce( |
| &WebPrintingServiceChromeOS::OnPermissionDecidedForGetPrinters, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void WebPrintingServiceChromeOS::FetchAttributes( |
| FetchAttributesCallback callback) { |
| if (!HasPrintingPermission(render_frame_host())) { |
| std::move(callback).Run(blink::mojom::WebPrinterFetchResult::NewError( |
| blink::mojom::WebPrinterFetchError::kUserPermissionDenied)); |
| return; |
| } |
| |
| const std::string& printer_id = *printers_.current_context(); |
| GetLocalPrinterInterface()->GetCapability( |
| printer_id, |
| base::BindOnce(&ConvertResponse) |
| .Then(base::BindOnce( |
| &WebPrintingServiceChromeOS::OnPrinterAttributesRetrieved, |
| weak_factory_.GetWeakPtr(), printer_id, std::move(callback)))); |
| } |
| |
| void WebPrintingServiceChromeOS::Print( |
| mojo::PendingRemote<blink::mojom::Blob> document, |
| std::unique_ptr<PrintSettings> attributes, |
| PrintCallback callback) { |
| if (!HasPrintingPermission(render_frame_host())) { |
| std::move(callback).Run(blink::mojom::WebPrintResult::NewError( |
| blink::mojom::WebPrintError::kUserPermissionDenied)); |
| return; |
| } |
| |
| const std::string& printer_id = *printers_.current_context(); |
| attributes->set_device_name(base::UTF8ToUTF16(printer_id)); |
| GetLocalPrinterInterface()->GetCapability( |
| printer_id, |
| base::BindOnce(&ExtractCapsAndDefaults) |
| .Then(base::BindOnce( |
| &WebPrintingServiceChromeOS::OnPrinterAttributesRetrievedForPrint, |
| weak_factory_.GetWeakPtr(), std::move(document), |
| std::move(attributes), std::move(callback), printer_id))); |
| } |
| |
| void WebPrintingServiceChromeOS::OnPermissionDecidedForGetPrinters( |
| GetPrintersCallback callback, |
| content::PermissionResult permission_result) { |
| if (permission_result.status != blink::mojom::PermissionStatus::GRANTED) { |
| std::move(callback).Run(blink::mojom::GetPrintersResult::NewError( |
| blink::mojom::GetPrintersError::kUserPermissionDenied)); |
| return; |
| } |
| GetLocalPrinterInterface()->GetPrinters( |
| base::BindOnce(&WebPrintingServiceChromeOS::OnPrintersRetrieved, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void WebPrintingServiceChromeOS::OnPrintersRetrieved( |
| GetPrintersCallback callback, |
| std::vector<crosapi::mojom::LocalDestinationInfoPtr> printers) { |
| // TODO(b/302505962): Figure out the correct permissions UX. |
| std::vector<blink::mojom::WebPrinterInfoPtr> web_printers; |
| for (const auto& printer : printers) { |
| mojo::PendingRemote<blink::mojom::WebPrinter> printer_remote; |
| printers_.Add(this, printer_remote.InitWithNewPipeAndPassReceiver(), |
| PrinterId(printer->id)); |
| |
| auto printer_info = blink::mojom::WebPrinterInfo::New(); |
| printer_info->printer_name = printer->name; |
| printer_info->printer_remote = std::move(printer_remote); |
| web_printers.push_back(std::move(printer_info)); |
| } |
| std::move(callback).Run( |
| blink::mojom::GetPrintersResult::NewPrinters(std::move(web_printers))); |
| } |
| |
| void WebPrintingServiceChromeOS::OnPrinterAttributesRetrieved( |
| const std::string& printer_id, |
| FetchAttributesCallback callback, |
| blink::mojom::WebPrinterAttributesPtr printer_attributes) { |
| if (!printer_attributes) { |
| InvokeFetchAttributesCallback(std::move(callback), |
| /*printer_attributes=*/nullptr); |
| return; |
| } |
| cups_wrapper_->QueryCupsPrinterStatus( |
| printer_id, base::BindOnce(&MergePrinterAttributesAndStatus, |
| std::move(printer_attributes)) |
| .Then(base::BindOnce(&InvokeFetchAttributesCallback, |
| std::move(callback)))); |
| } |
| |
| void WebPrintingServiceChromeOS::OnPrinterAttributesRetrievedForPrint( |
| mojo::PendingRemote<blink::mojom::Blob> document, |
| std::unique_ptr<PrintSettings> pjt_attributes, |
| PrintCallback callback, |
| const std::string& printer_id, |
| std::optional<PrinterSemanticCapsAndDefaults> printer_attributes) { |
| if (!printer_attributes) { |
| std::move(callback).Run(blink::mojom::WebPrintResult::NewError( |
| blink::mojom::WebPrintError::kPrinterUnreachable)); |
| return; |
| } |
| |
| if (!ValidateAttributesAndUpdateIfNecessary(*pjt_attributes, |
| *printer_attributes)) { |
| std::move(callback).Run(blink::mojom::WebPrintResult::NewError( |
| blink::mojom::WebPrintError::kPrintJobTemplateAttributesMismatch)); |
| return; |
| } |
| |
| pdf_flattener_->ReadAndFlattenPdf( |
| std::move(document), |
| base::BindOnce(&WebPrintingServiceChromeOS::OnPdfReadAndFlattened, |
| weak_factory_.GetWeakPtr(), std::move(pjt_attributes), |
| std::move(callback))); |
| } |
| |
| void WebPrintingServiceChromeOS::OnPdfReadAndFlattened( |
| std::unique_ptr<PrintSettings> settings, |
| PrintCallback callback, |
| std::unique_ptr<FlattenPdfResult> flatten_pdf_result) { |
| if (!flatten_pdf_result) { |
| std::move(callback).Run(blink::mojom::WebPrintResult::NewError( |
| blink::mojom::WebPrintError::kDocumentMalformed)); |
| return; |
| } |
| |
| mojo::PendingRemote<blink::mojom::WebPrintJobStateObserver> observer; |
| mojo::PendingReceiver<blink::mojom::WebPrintJobController> controller; |
| auto job_info = blink::mojom::WebPrintJobInfo::New(); |
| job_info->job_name = base::UTF16ToUTF8(settings->title()); |
| // Total number of pages in all copies. |
| job_info->job_pages = flatten_pdf_result->page_count * settings->copies(); |
| job_info->observer = observer.InitWithNewPipeAndPassReceiver(); |
| job_info->controller = controller.InitWithNewPipeAndPassRemote(); |
| |
| print_job_controller_->CreatePrintJob( |
| std::move(flatten_pdf_result->flattened_pdf), std::move(settings), |
| flatten_pdf_result->page_count, |
| /*source=*/crosapi::mojom::PrintJob::Source::kIsolatedWebApp, |
| /*source_id=*/app_id_, |
| base::BindOnce(&WebPrintingServiceChromeOS::OnPrintJobCreated, |
| weak_factory_.GetWeakPtr(), std::move(observer), |
| std::move(controller))); |
| |
| std::move(callback).Run( |
| blink::mojom::WebPrintResult::NewPrintJobInfo(std::move(job_info))); |
| } |
| |
| void WebPrintingServiceChromeOS::OnPrintJobCreated( |
| mojo::PendingRemote<blink::mojom::WebPrintJobStateObserver> observer, |
| mojo::PendingReceiver<blink::mojom::WebPrintJobController> controller, |
| std::optional<PrintJobCreatedInfo> creation_info) { |
| if (!creation_info) { |
| // Dispatches a notification and deletes itself. |
| auto update = blink::mojom::WebPrintJobUpdate::New(); |
| update->state = blink::mojom::WebPrintJobState::kAborted; |
| mojo::Remote<blink::mojom::WebPrintJobStateObserver>(std::move(observer)) |
| ->OnWebPrintJobUpdate(std::move(update)); |
| return; |
| } |
| |
| std::string printer_id = |
| base::UTF16ToUTF8(creation_info->document->settings().device_name()); |
| in_progress_jobs_storage_.PrintJobAcknowledgedByThePrintSystem( |
| printer_id, creation_info->job_id, std::move(observer), |
| std::move(controller)); |
| } |
| |
| } // namespace printing |