// 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 <utility>
#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/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 (!base::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);
}
base::ranges::sort(printer_state_reasons);
printer_state_reasons.erase(base::ranges::unique(printer_state_reasons),
printer_state_reasons.end());
printer_attributes->printer_state_message = printer_status->message;
return printer_attributes;
}
bool HasPrintingPermission(content::RenderFrameHost& rfh) {
return rfh.GetBrowserContext()
->GetPermissionController()
->GetPermissionStatusForCurrentDocument(
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(
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,
blink::mojom::PermissionStatus permission_status) {
if (permission_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));
#if BUILDFLAG(IS_CHROMEOS_LACROS)
NotifyAshJobCreated(
creation_info->job_id, *creation_info->document,
/*source=*/crosapi::mojom::PrintJob::Source::kIsolatedWebApp,
/*source_id=*/app_id_, GetLocalPrinterInterface());
#endif
}
} // namespace printing