[go: nahoru, domu]

blob: 76bec56e7b45c088d42303e7122be847abafb997 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "printing/backend/print_backend_cups.h"
#include <cups/cups.h>
#include <cups/ppd.h>
#include <dlfcn.h>
#include <errno.h>
#include <pthread.h>
#include <string>
#include <string_view>
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/lock.h"
#include "build/build_config.h"
#include "printing/backend/cups_helper.h"
#include "printing/backend/print_backend_consts.h"
#include "printing/mojom/print.mojom.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_MAC)
#include "base/feature_list.h"
#include "printing/backend/cups_connection.h"
#include "printing/backend/print_backend_cups_ipp.h"
#include "printing/printing_features.h"
#endif // BUILDFLAG(IS_MAC)
namespace printing {
namespace {
struct CupsDestsData {
int num_dests;
// This field is not a raw_ptr<> because it was filtered by the rewriter for:
// #reinterpret-cast-trivial-type, #addr-of
RAW_PTR_EXCLUSION cups_dest_t* dests;
};
int CaptureCupsDestCallback(void* data, unsigned flags, cups_dest_t* dest) {
CupsDestsData* dests_data = reinterpret_cast<CupsDestsData*>(data);
if (flags & CUPS_DEST_FLAGS_REMOVED) {
dests_data->num_dests = cupsRemoveDest(
dest->name, dest->instance, dests_data->num_dests, &dests_data->dests);
} else {
dests_data->num_dests =
cupsCopyDest(dest, dests_data->num_dests, &dests_data->dests);
}
return 1; // Keep going.
}
} // namespace
PrintBackendCUPS::PrintBackendCUPS(const GURL& print_server_url,
http_encryption_t encryption,
bool blocking,
const std::string& locale)
: locale_(locale),
print_server_url_(print_server_url),
cups_encryption_(encryption),
blocking_(blocking) {}
PrintBackendCUPS::~PrintBackendCUPS() = default;
// static
mojom::ResultCode PrintBackendCUPS::PrinterBasicInfoFromCUPS(
const cups_dest_t& printer,
PrinterBasicInfo* printer_info) {
const char* type_str =
cupsGetOption(kCUPSOptPrinterType, printer.num_options, printer.options);
if (type_str) {
cups_ptype_t type;
if (base::StringToUint(type_str, &type)) {
if (type & kDestinationsFilterMask)
return mojom::ResultCode::kFailed;
}
}
printer_info->printer_name = printer.name;
printer_info->is_default = printer.is_default;
const char* info =
cupsGetOption(kCUPSOptPrinterInfo, printer.num_options, printer.options);
const char* state =
cupsGetOption(kCUPSOptPrinterState, printer.num_options, printer.options);
if (state)
base::StringToInt(state, &printer_info->printer_status);
const char* drv_info = cupsGetOption(kCUPSOptPrinterMakeAndModel,
printer.num_options, printer.options);
if (drv_info)
printer_info->options[kDriverInfoTagName] = drv_info;
// Store printer options.
for (int opt_index = 0; opt_index < printer.num_options; ++opt_index) {
printer_info->options[printer.options[opt_index].name] =
printer.options[opt_index].value;
}
#if BUILDFLAG(IS_MAC)
// On Mac, "printer-info" option specifies the printer name and
// "printer-make-and-model" specifies the printer description.
if (info)
printer_info->display_name = info;
if (drv_info)
printer_info->printer_description = drv_info;
#else
// On Linux destination name specifies the printer name and "printer-info"
// specifies the printer description.
printer_info->display_name = printer.name;
if (info)
printer_info->printer_description = info;
#endif
return mojom::ResultCode::kSuccess;
}
// static
std::string PrintBackendCUPS::PrinterDriverInfoFromCUPS(
const cups_dest_t& printer) {
// std::string_view will correctly handle nullptrs from cupsGetOption(),
// whereas std::string will not. Thus do not directly assign to `result`.
std::string_view info(
cupsGetOption(kDriverNameTagName, printer.num_options, printer.options));
return std::string(info);
}
void PrintBackendCUPS::DestinationDeleter::operator()(cups_dest_t* dest) const {
cupsFreeDests(1, dest);
}
mojom::ResultCode PrintBackendCUPS::EnumeratePrinters(
PrinterList& printer_list) {
DCHECK(printer_list.empty());
// If possible prefer to use cupsEnumDests() over GetDests(), because the
// latter has been found to filter out some destination values if a device
// reports multiple times (crbug.com/1209175), which can lead to destinations
// not showing as available. Using cupsEnumDests() allows us to do our own
// filtering should any duplicates occur.
CupsDestsData dests_data = {0, nullptr};
ipp_status_t last_error = IPP_STATUS_OK;
if (print_server_url_.is_empty()) {
VLOG(1) << "CUPS: using cupsEnumDests to enumerate printers";
if (!cupsEnumDests(CUPS_DEST_FLAGS_NONE, kCupsTimeoutMs,
/*cancel=*/nullptr,
/*type=*/CUPS_PRINTER_LOCAL, kDestinationsFilterMask,
CaptureCupsDestCallback, &dests_data)) {
// Free any allocations and reset data, and then fall through to common
// error handling below.
last_error = cupsLastError();
cupsFreeDests(dests_data.num_dests, dests_data.dests);
dests_data.num_dests = 0;
dests_data.dests = nullptr;
}
} else {
VLOG(1) << "CUPS: using cupsGetDests2 to enumerate printers";
dests_data.num_dests = GetDests(&dests_data.dests);
if (!dests_data.num_dests)
last_error = cupsLastError();
}
DCHECK_GE(dests_data.num_dests, 0);
if (!dests_data.num_dests) {
// No destinations could mean the operation failed or that there are simply
// no printer drivers installed. Rely upon CUPS error code to distinguish
// between these.
DCHECK(!dests_data.dests);
if (last_error != IPP_STATUS_ERROR_NOT_FOUND) {
VLOG(1) << "CUPS: Error getting printers from CUPS server"
<< ", server: " << print_server_url_
<< ", error: " << static_cast<int>(last_error) << " - "
<< cupsLastErrorString();
return mojom::ResultCode::kFailed;
}
VLOG(1) << "CUPS: No printers found for CUPS server: " << print_server_url_;
return mojom::ResultCode::kSuccess;
}
for (int printer_index = 0; printer_index < dests_data.num_dests;
++printer_index) {
const cups_dest_t& printer = dests_data.dests[printer_index];
PrinterBasicInfo printer_info;
if (PrinterBasicInfoFromCUPS(printer, &printer_info) ==
mojom::ResultCode::kSuccess) {
printer_list.push_back(printer_info);
}
}
cupsFreeDests(dests_data.num_dests, dests_data.dests);
VLOG(1) << "CUPS: Enumerated printers, server: " << print_server_url_
<< ", # of printers: " << printer_list.size();
return mojom::ResultCode::kSuccess;
}
mojom::ResultCode PrintBackendCUPS::GetDefaultPrinterName(
std::string& default_printer) {
// Not using cupsGetDefault() because it lies about the default printer.
cups_dest_t* dests;
int num_dests = GetDests(&dests);
cups_dest_t* dest = cupsGetDest(nullptr, nullptr, num_dests, dests);
mojom::ResultCode result = mojom::ResultCode::kSuccess;
if (dest) {
default_printer = std::string(dest->name);
} else if (cupsLastError() <= IPP_OK_EVENTS_COMPLETE) {
// No default printer found.
default_printer.clear();
} else {
LOG(ERROR) << "CUPS: Error getting default printer: "
<< cupsLastErrorString();
result = mojom::ResultCode::kFailed;
}
cupsFreeDests(num_dests, dests);
return result;
}
mojom::ResultCode PrintBackendCUPS::GetPrinterBasicInfo(
const std::string& printer_name,
PrinterBasicInfo* printer_info) {
ScopedDestination dest = GetNamedDest(printer_name);
if (!dest)
return mojom::ResultCode::kFailed;
DCHECK_EQ(printer_name, dest->name);
return PrinterBasicInfoFromCUPS(*dest, printer_info);
}
mojom::ResultCode PrintBackendCUPS::GetPrinterSemanticCapsAndDefaults(
const std::string& printer_name,
PrinterSemanticCapsAndDefaults* printer_info) {
if (!IsValidPrinter(printer_name))
return mojom::ResultCode::kFailed;
std::string printer_capabilities = GetPrinterCapabilities(printer_name);
if (printer_capabilities.empty()) {
return mojom::ResultCode::kFailed;
}
ScopedDestination dest = GetNamedDest(printer_name);
return ParsePpdCapabilities(dest.get(), locale_, printer_capabilities,
printer_info)
? mojom::ResultCode::kSuccess
: mojom::ResultCode::kFailed;
}
std::string PrintBackendCUPS::GetPrinterCapabilities(
const std::string& printer_name) {
VLOG(1) << "CUPS: Getting caps and defaults, printer name: " << printer_name;
base::FilePath ppd_path(GetPPD(printer_name.c_str()));
// In some cases CUPS failed to get ppd file.
if (ppd_path.empty()) {
LOG(ERROR) << "CUPS: Failed to get PPD, printer name: " << printer_name;
return std::string();
}
std::string content;
if (!base::ReadFileToString(ppd_path, &content)) {
content.clear();
}
base::DeleteFile(ppd_path);
return content;
}
std::vector<std::string> PrintBackendCUPS::GetPrinterDriverInfo(
const std::string& printer_name) {
std::vector<std::string> result;
ScopedDestination dest = GetNamedDest(printer_name);
if (dest) {
DCHECK_EQ(printer_name, dest->name);
result.emplace_back(PrinterDriverInfoFromCUPS(*dest));
}
return result;
}
bool PrintBackendCUPS::IsValidPrinter(const std::string& printer_name) {
return !!GetNamedDest(printer_name);
}
#if !BUILDFLAG(IS_CHROMEOS)
scoped_refptr<PrintBackend> PrintBackend::CreateInstanceImpl(
const std::string& locale) {
#if BUILDFLAG(IS_MAC)
if (base::FeatureList::IsEnabled(features::kCupsIppPrintingBackend)) {
return base::MakeRefCounted<PrintBackendCupsIpp>(CupsConnection::Create());
}
#endif // BUILDFLAG(IS_MAC)
return base::MakeRefCounted<PrintBackendCUPS>(
GURL(), HTTP_ENCRYPT_NEVER, /*cups_blocking=*/false, locale);
}
#endif // !BUILDFLAG(IS_CHROMEOS)
int PrintBackendCUPS::GetDests(cups_dest_t** dests) {
// Default to the local print server (CUPS scheduler)
if (print_server_url_.is_empty())
return cupsGetDests2(CUPS_HTTP_DEFAULT, dests);
HttpConnectionCUPS http(print_server_url_, cups_encryption_, blocking_);
// This call must be made in the same scope as `http` because its destructor
// closes the connection.
return cupsGetDests2(http.http(), dests);
}
base::FilePath PrintBackendCUPS::GetPPD(const char* name) {
// cupsGetPPD returns a filename stored in a static buffer in CUPS.
// Protect this code with lock.
static base::NoDestructor<base::Lock> ppd_lock;
base::AutoLock ppd_autolock(*ppd_lock);
base::FilePath ppd_path;
const char* ppd_file_path = nullptr;
if (print_server_url_.is_empty()) { // Use default (local) print server.
ppd_file_path = cupsGetPPD(name);
if (ppd_file_path)
ppd_path = base::FilePath(ppd_file_path);
} else {
// cupsGetPPD2 gets stuck sometimes in an infinite time due to network
// configuration/issues. To prevent that, use non-blocking http connection
// here.
// Note: After looking at CUPS sources, it looks like non-blocking
// connection will timeout after 10 seconds of no data period. And it will
// return the same way as if data was completely and successfully
// downloaded.
HttpConnectionCUPS http(print_server_url_, cups_encryption_, blocking_);
ppd_file_path = cupsGetPPD2(http.http(), name);
// Check if the get full PPD, since non-blocking call may simply return
// normally after timeout expired.
if (ppd_file_path) {
// There is no reliable way right now to detect full and complete PPD
// get downloaded. If we reach http timeout, it may simply return
// downloaded part as a full response. It might be good enough to check
// http->data_remaining or http->_data_remaining, unfortunately http_t
// is an internal structure and fields are not exposed in CUPS headers.
// httpGetLength or httpGetLength2 returning the full content size.
// Comparing file size against that content length might be unreliable
// since some http reponses are encoded and content_length > file size.
// Let's just check for the obvious CUPS and http errors here.
ppd_path = base::FilePath(ppd_file_path);
ipp_status_t error_code = cupsLastError();
int http_error = httpError(http.http());
if (error_code > IPP_OK_EVENTS_COMPLETE || http_error != 0) {
LOG(ERROR) << "Error downloading PPD file, name: " << name
<< ", CUPS error: " << static_cast<int>(error_code)
<< ", HTTP error: " << http_error;
base::DeleteFile(ppd_path);
ppd_path.clear();
}
}
}
return ppd_path;
}
PrintBackendCUPS::ScopedDestination PrintBackendCUPS::GetNamedDest(
const std::string& printer_name) {
cups_dest_t* dest;
if (print_server_url_.is_empty()) {
// Use default (local) print server.
dest = cupsGetNamedDest(CUPS_HTTP_DEFAULT, printer_name.c_str(), nullptr);
} else {
HttpConnectionCUPS http(print_server_url_, cups_encryption_, blocking_);
dest = cupsGetNamedDest(http.http(), printer_name.c_str(), nullptr);
}
return ScopedDestination(dest);
}
} // namespace printing