| // 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(); |
| 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(); |
| 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(); |
| 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 |