// 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 "chrome/browser/extensions/api/printing/printing_api_utils.h"
#include <memory>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/json/json_reader.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.h"
#include "base/values.h"
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
#include "chromeos/printing/printer_configuration.h"
#include "components/cloud_devices/common/cloud_device_description.h"
#include "components/cloud_devices/common/printer_description.h"
#include "printing/backend/print_backend.h"
#include "printing/mojom/print.mojom.h"
#include "printing/print_settings.h"
#include "third_party/re2/src/re2/re2.h"
namespace extensions {
namespace idl = api::printing;
namespace {
constexpr char kLocal[] = "local";
constexpr char kKind[] = "kind";
constexpr char kIdPattern[] = "idPattern";
constexpr char kNamePattern[] = "namePattern";
bool DoesPrinterMatchDefaultPrinterRules(
const crosapi::mojom::LocalDestinationInfo& printer,
const std::optional<DefaultPrinterRules>& rules) {
if (!rules.has_value())
return false;
return (rules->kind.empty() || rules->kind == kLocal) &&
(rules->id_pattern.empty() ||
RE2::FullMatch(printer.id, rules->id_pattern)) &&
(rules->name_pattern.empty() ||
RE2::FullMatch(printer.name, rules->name_pattern));
}
// Validate a vendor ticket item from a print job ticket. Items are validated
// against an allow-list of values in addition to the advanced capabilities of
// the printer. Return true if the given item is allowed, false if not.
bool ValidateVendorItem(const std::string& name,
const std::string& value,
const printing::AdvancedCapabilities& capabilities) {
// A map containing the allowed vendor items. The key is an IPP attribute,
// and the value is a set of allowable values for that attribute.
static const base::NoDestructor<
base::flat_map<std::string_view, base::flat_set<std::string_view>>>
kVendorItemAllowList({
{"finishings", {"none", "trim"}},
});
// Check the explicit allow list. If the value does not match, this IPP
// attribute is then checked against the list of printer capabilities.
const auto& item = kVendorItemAllowList->find(name);
if (item != kVendorItemAllowList->end() && item->second.contains(value)) {
return true;
}
// Check other allowed attributes against the printer capabilities.
for (const printing::AdvancedCapability& capability : capabilities) {
if (capability.name != name) {
continue;
}
return base::Contains(capability.values, value,
&printing::AdvancedCapabilityValue::name);
}
return false;
}
} // namespace
std::optional<DefaultPrinterRules> GetDefaultPrinterRules(
const std::string& default_destination_selection_rules) {
if (default_destination_selection_rules.empty())
return std::nullopt;
std::optional<base::Value> default_destination_selection_rules_value =
base::JSONReader::Read(default_destination_selection_rules);
base::Value::Dict* default_destination_selection_rules_dict =
default_destination_selection_rules_value.has_value()
? default_destination_selection_rules_value->GetIfDict()
: nullptr;
if (!default_destination_selection_rules_dict) {
return std::nullopt;
}
DefaultPrinterRules default_printer_rules;
if (const std::string* kind =
default_destination_selection_rules_dict->FindString(kKind)) {
default_printer_rules.kind = *kind;
}
if (const std::string* id_pattern =
default_destination_selection_rules_dict->FindString(kIdPattern)) {
default_printer_rules.id_pattern = *id_pattern;
}
if (const std::string* name_pattern =
default_destination_selection_rules_dict->FindString(kNamePattern)) {
default_printer_rules.name_pattern = *name_pattern;
}
return default_printer_rules;
}
idl::Printer PrinterToIdl(
const crosapi::mojom::LocalDestinationInfo& printer,
const std::optional<DefaultPrinterRules>& default_printer_rules,
const base::flat_map<std::string, int>& recently_used_ranks) {
idl::Printer idl_printer;
idl_printer.id = printer.id;
idl_printer.name = printer.name;
idl_printer.description = printer.description;
if (printer.uri)
idl_printer.uri = *printer.uri;
idl_printer.source = printer.configured_via_policy
? idl::PrinterSource::kPolicy
: idl::PrinterSource::kUser;
idl_printer.is_default =
DoesPrinterMatchDefaultPrinterRules(printer, default_printer_rules);
auto it = recently_used_ranks.find(printer.id);
if (it != recently_used_ranks.end())
idl_printer.recently_used_rank = it->second;
return idl_printer;
}
idl::PrinterStatus PrinterStatusToIdl(chromeos::PrinterErrorCode status) {
switch (status) {
case chromeos::PrinterErrorCode::NO_ERROR:
return idl::PrinterStatus::kAvailable;
case chromeos::PrinterErrorCode::PAPER_JAM:
return idl::PrinterStatus::kPaperJam;
case chromeos::PrinterErrorCode::OUT_OF_PAPER:
return idl::PrinterStatus::kOutOfPaper;
case chromeos::PrinterErrorCode::OUT_OF_INK:
return idl::PrinterStatus::kOutOfInk;
case chromeos::PrinterErrorCode::DOOR_OPEN:
return idl::PrinterStatus::kDoorOpen;
case chromeos::PrinterErrorCode::PRINTER_UNREACHABLE:
return idl::PrinterStatus::kUnreachable;
case chromeos::PrinterErrorCode::TRAY_MISSING:
return idl::PrinterStatus::kTrayMissing;
case chromeos::PrinterErrorCode::OUTPUT_FULL:
return idl::PrinterStatus::kOutputFull;
case chromeos::PrinterErrorCode::STOPPED:
return idl::PrinterStatus::kStopped;
case chromeos::PrinterErrorCode::EXPIRED_CERTIFICATE:
return idl::PrinterStatus::kExpiredCertificate;
default:
break;
}
return idl::PrinterStatus::kGenericIssue;
}
std::unique_ptr<printing::PrintSettings> ParsePrintTicket(
base::Value::Dict ticket) {
cloud_devices::CloudDeviceDescription description;
if (!description.InitFromValue(std::move(ticket))) {
LOG(ERROR) << "Unable to initialize CDD from print ticket.";
return nullptr;
}
auto settings = std::make_unique<printing::PrintSettings>();
cloud_devices::printer::ColorTicketItem color;
if (!color.LoadFrom(description)) {
LOG(ERROR) << "Unable to load color from print ticket.";
return nullptr;
}
switch (color.value().type) {
case cloud_devices::printer::ColorType::STANDARD_MONOCHROME:
case cloud_devices::printer::ColorType::CUSTOM_MONOCHROME:
settings->set_color(printing::mojom::ColorModel::kGray);
break;
case cloud_devices::printer::ColorType::STANDARD_COLOR:
case cloud_devices::printer::ColorType::CUSTOM_COLOR:
case cloud_devices::printer::ColorType::AUTO_COLOR:
settings->set_color(printing::mojom::ColorModel::kColor);
break;
default:
NOTREACHED_IN_MIGRATION();
break;
}
cloud_devices::printer::DuplexTicketItem duplex;
if (!duplex.LoadFrom(description)) {
LOG(ERROR) << "Unable to load duplex from print ticket.";
return nullptr;
}
switch (duplex.value()) {
case cloud_devices::printer::DuplexType::NO_DUPLEX:
settings->set_duplex_mode(printing::mojom::DuplexMode::kSimplex);
break;
case cloud_devices::printer::DuplexType::LONG_EDGE:
settings->set_duplex_mode(printing::mojom::DuplexMode::kLongEdge);
break;
case cloud_devices::printer::DuplexType::SHORT_EDGE:
settings->set_duplex_mode(printing::mojom::DuplexMode::kShortEdge);
break;
default:
NOTREACHED_IN_MIGRATION();
break;
}
cloud_devices::printer::OrientationTicketItem orientation;
if (!orientation.LoadFrom(description)) {
LOG(ERROR) << "Unable to load orientation from print ticket.";
return nullptr;
}
switch (orientation.value()) {
case cloud_devices::printer::OrientationType::LANDSCAPE:
settings->SetOrientation(/*landscape=*/true);
break;
case cloud_devices::printer::OrientationType::PORTRAIT:
settings->SetOrientation(/*landscape=*/false);
break;
default:
NOTREACHED_IN_MIGRATION();
break;
}
cloud_devices::printer::CopiesTicketItem copies;
if (!copies.LoadFrom(description) || copies.value() < 1) {
LOG(ERROR) << "Unable to load copies from print ticket.";
return nullptr;
}
settings->set_copies(copies.value());
cloud_devices::printer::DpiTicketItem dpi;
if (!dpi.LoadFrom(description)) {
LOG(ERROR) << "Unable to load DPI from print ticket.";
return nullptr;
}
settings->set_dpi_xy(dpi.value().horizontal, dpi.value().vertical);
cloud_devices::printer::MediaTicketItem media;
if (!media.LoadFrom(description)) {
LOG(ERROR) << "Unable to load media from print ticket.";
return nullptr;
}
cloud_devices::printer::Media media_value = media.value();
printing::PrintSettings::RequestedMedia requested_media;
if (media_value.size_um.width() <= 0 || media_value.size_um.height() <= 0) {
LOG(ERROR) << "Loaded invalid media from print ticket.";
return nullptr;
}
requested_media.size_microns = media_value.size_um;
requested_media.vendor_id = media_value.vendor_id;
settings->set_requested_media(requested_media);
cloud_devices::printer::CollateTicketItem collate;
if (!collate.LoadFrom(description)) {
LOG(ERROR) << "Unable to load collate from print ticket.";
return nullptr;
}
settings->set_collate(collate.value());
// These items are optional - don't fail if they don't exist.
cloud_devices::printer::VendorTicketItems vendor_items;
if (vendor_items.LoadFrom(description)) {
for (const auto& item : vendor_items) {
settings->advanced_settings().emplace(item.id, item.value);
}
}
return settings;
}
bool CheckSettingsAndCapabilitiesCompatibility(
const printing::PrintSettings& settings,
const printing::PrinterSemanticCapsAndDefaults& capabilities) {
if (settings.collate() && !capabilities.collate_capable)
return false;
if (settings.copies() > capabilities.copies_max)
return false;
if (!base::Contains(capabilities.duplex_modes, settings.duplex_mode()))
return false;
std::optional<bool> is_color =
::printing::IsColorModelSelected(settings.color());
bool color_mode_selected = is_color.has_value() && is_color.value();
if (!color_mode_selected &&
capabilities.bw_model ==
printing::mojom::ColorModel::kUnknownColorModel) {
return false;
}
if (color_mode_selected &&
capabilities.color_model ==
printing::mojom::ColorModel::kUnknownColorModel) {
return false;
}
if (!base::Contains(capabilities.dpis, settings.dpi_size()))
return false;
for (const auto& [name, value] : settings.advanced_settings()) {
if (!value.is_string()) {
LOG(ERROR) << "Advanced setting '" << name
<< "' expects a string value, got: "
<< base::Value::GetTypeName(value.type());
return false;
}
if (!ValidateVendorItem(name, value.GetString(),
capabilities.advanced_capabilities)) {
LOG(ERROR) << "Advanced setting '" << name << ":" << value.GetString()
<< "' is not compatible with printer capabilities";
return false;
}
}
const printing::PrintSettings::RequestedMedia& requested_media =
settings.requested_media();
return base::ranges::any_of(
capabilities.papers,
[&requested_media](
const printing::PrinterSemanticCapsAndDefaults::Paper& paper) {
return paper.IsSizeWithinBounds(requested_media.size_microns);
});
}
} // namespace extensions