blob: 06334a7e674ecccda4ee3a852938d115dc2989be [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "printing/backend/cups_jobs.h"
#include <cups/ipp.h>
#include <array>
#include <cstring>
#include <map>
#include <memory>
#include <string>
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/version.h"
#include "printing/backend/cups_deleters.h"
#include "printing/backend/cups_ipp_helper.h"
#include "printing/printer_status.h"
namespace printing {
namespace {
using PReason = PrinterStatus::PrinterReason::Reason;
using PSeverity = PrinterStatus::PrinterReason::Severity;
// printer attributes
const char kPrinterUri[] = "printer-uri";
const char kPrinterState[] = "printer-state";
const char kPrinterStateReasons[] = "printer-state-reasons";
const char kPrinterStateMessage[] = "printer-state-message";
const char kPrinterMakeAndModel[] = "printer-make-and-model";
const char kIppVersionsSupported[] = "ipp-versions-supported";
const char kIppFeaturesSupported[] = "ipp-features-supported";
const char kDocumentFormatSupported[] = "document-format-supported";
// job attributes
const char kJobUri[] = "job-uri";
const char kJobId[] = "job-id";
const char kJobState[] = "job-state";
const char kJobStateReasons[] = "job-state-reasons";
const char kJobStateMessage[] = "job-state-message";
const char kJobImpressionsCompleted[] = "job-impressions-completed";
const char kTimeAtProcessing[] = "time-at-processing";
// request parameters
const char kRequestedAttributes[] = "requested-attributes";
const char kWhichJobs[] = "which-jobs";
const char kLimit[] = "limit";
// request values
const char kCompleted[] = "completed";
const char kNotCompleted[] = "not-completed";
// ipp features
const char kIppEverywhere[] = "ipp-everywhere";
// printer state severities
const char kSeverityReport[] = "report";
const char kSeverityWarn[] = "warning";
const char kSeverityError[] = "error";
// printer state reason values
const char kNone[] = "none";
const char kMediaNeeded[] = "media-needed";
const char kMediaJam[] = "media-jam";
const char kMovingToPaused[] = "moving-to-paused";
const char kPaused[] = "paused";
const char kShutdown[] = "shutdown";
const char kConnectingToDevice[] = "connecting-to-device";
const char kTimedOut[] = "timed-out";
const char kStopping[] = "stopping";
const char kStoppedPartly[] = "stopped-partly";
const char kTonerLow[] = "toner-low";
const char kTonerEmpty[] = "toner-empty";
const char kSpoolAreaFull[] = "spool-area-full";
const char kCoverOpen[] = "cover-open";
const char kInterlockOpen[] = "interlock-open";
const char kDoorOpen[] = "door-open";
const char kInputTrayMissing[] = "input-tray-missing";
const char kMediaLow[] = "media-low";
const char kMediaEmpty[] = "media-empty";
const char kOutputTrayMissing[] = "output-tray-missing";
const char kOutputAreaAlmostFull[] = "output-area-almost-full";
const char kOutputAreaFull[] = "output-area-full";
const char kMarkerSupplyLow[] = "marker-supply-low";
const char kMarkerSupplyEmpty[] = "marker-supply-empty";
const char kMarkerWasteAlmostFull[] = "marker-waste-almost-full";
const char kMarkerWasteFull[] = "marker-waste-full";
const char kFuserOverTemp[] = "fuser-over-temp";
const char kFuserUnderTemp[] = "fuser-under-temp";
const char kOpcNearEol[] = "opc-near-eol";
const char kOpcLifeOver[] = "opc-life-over";
const char kDeveloperLow[] = "developer-low";
const char kDeveloperEmpty[] = "developer-empty";
const char kInterpreterResourceUnavailable[] =
"interpreter-resource-unavailable";
constexpr char kIppScheme[] = "ipp";
constexpr char kIppsScheme[] = "ipps";
// Timeout for establishing a HTTP connection in milliseconds. Anecdotally,
// some print servers are slow and can use the extra time.
constexpr int kHttpConnectTimeoutMs = 1000;
constexpr std::array<const char* const, 3> kPrinterAttributes{
{kPrinterState, kPrinterStateReasons, kPrinterStateMessage}};
constexpr std::array<const char* const, 7> kPrinterInfoAndStatus{
{kPrinterMakeAndModel, kIppVersionsSupported, kIppFeaturesSupported,
kDocumentFormatSupported, kPrinterState, kPrinterStateReasons,
kPrinterStateMessage}};
// Converts an IPP attribute |attr| to the appropriate JobState enum.
CupsJob::JobState ToJobState(ipp_attribute_t* attr) {
DCHECK_EQ(IPP_TAG_ENUM, ippGetValueTag(attr));
int state = ippGetInteger(attr, 0);
switch (state) {
case IPP_JOB_ABORTED:
return CupsJob::ABORTED;
case IPP_JOB_CANCELLED:
return CupsJob::CANCELED;
case IPP_JOB_COMPLETED:
return CupsJob::COMPLETED;
case IPP_JOB_HELD:
return CupsJob::HELD;
case IPP_JOB_PENDING:
return CupsJob::PENDING;
case IPP_JOB_PROCESSING:
return CupsJob::PROCESSING;
case IPP_JOB_STOPPED:
return CupsJob::STOPPED;
default:
NOTREACHED() << "Unidentifed state " << state;
break;
}
return CupsJob::UNKNOWN;
}
// Returns a lookup map from strings to PrinterReason::Reason.
const std::map<base::StringPiece, PReason>& GetLabelToReason() {
static const std::map<base::StringPiece, PReason> kLabelToReason =
std::map<base::StringPiece, PReason>{
{kNone, PReason::kNone},
{kMediaNeeded, PReason::kMediaNeeded},
{kMediaJam, PReason::kMediaJam},
{kMovingToPaused, PReason::kMovingToPaused},
{kPaused, PReason::kPaused},
{kShutdown, PReason::kShutdown},
{kConnectingToDevice, PReason::kConnectingToDevice},
{kTimedOut, PReason::kTimedOut},
{kStopping, PReason::kStopping},
{kStoppedPartly, PReason::kStoppedPartly},
{kTonerLow, PReason::kTonerLow},
{kTonerEmpty, PReason::kTonerEmpty},
{kSpoolAreaFull, PReason::kSpoolAreaFull},
{kCoverOpen, PReason::kCoverOpen},
{kInterlockOpen, PReason::kInterlockOpen},
{kDoorOpen, PReason::kDoorOpen},
{kInputTrayMissing, PReason::kInputTrayMissing},
{kMediaLow, PReason::kMediaLow},
{kMediaEmpty, PReason::kMediaEmpty},
{kOutputTrayMissing, PReason::kOutputTrayMissing},
{kOutputAreaAlmostFull, PReason::kOutputAreaAlmostFull},
{kOutputAreaFull, PReason::kOutputAreaFull},
{kMarkerSupplyLow, PReason::kMarkerSupplyLow},
{kMarkerSupplyEmpty, PReason::kMarkerSupplyEmpty},
{kMarkerWasteAlmostFull, PReason::kMarkerWasteAlmostFull},
{kMarkerWasteFull, PReason::kMarkerWasteFull},
{kFuserOverTemp, PReason::kFuserOverTemp},
{kFuserUnderTemp, PReason::kFuserUnderTemp},
{kOpcNearEol, PReason::kOpcNearEol},
{kOpcLifeOver, PReason::kOpcLifeOver},
{kDeveloperLow, PReason::kDeveloperLow},
{kDeveloperEmpty, PReason::kDeveloperEmpty},
{kInterpreterResourceUnavailable,
PReason::kInterpreterResourceUnavailable},
};
return kLabelToReason;
}
// Returns the Reason cooresponding to the string |reason|. Returns
// UNKOWN_REASON if the string is not recognized.
PrinterStatus::PrinterReason::Reason ToReason(base::StringPiece reason) {
const auto& enum_map = GetLabelToReason();
const auto& entry = enum_map.find(reason);
return entry != enum_map.end() ? entry->second : PReason::kUnknownReason;
}
// Returns the Severity cooresponding to |severity|. Returns UNKNOWN_SEVERITY
// if the strin gis not recognized.
PSeverity ToSeverity(base::StringPiece severity) {
if (severity == kSeverityError)
return PSeverity::kError;
if (severity == kSeverityWarn)
return PSeverity::kWarning;
if (severity == kSeverityReport)
return PSeverity::kReport;
return PSeverity::kUnknownSeverity;
}
// Parses the |reason| string into a PrinterReason. Splits the string based on
// the last '-' to determine severity. If a recognized severity is not
// included, severity is assumed to be ERROR per RFC2911.
PrinterStatus::PrinterReason ToPrinterReason(base::StringPiece reason) {
PrinterStatus::PrinterReason parsed;
if (reason == kNone) {
parsed.reason = PReason::kNone;
parsed.severity = PSeverity::kUnknownSeverity;
return parsed;
}
size_t last_dash = reason.rfind('-');
auto severity = PSeverity::kUnknownSeverity;
if (last_dash != base::StringPiece::npos) {
// try to parse the last part of the string as the severity.
severity = ToSeverity(reason.substr(last_dash + 1));
}
if (severity == PSeverity::kUnknownSeverity) {
// Severity is unknown. No severity in the reason.
// Per spec, if there is no severity, severity is error.
parsed.severity = PSeverity::kError;
parsed.reason = ToReason(reason);
} else {
parsed.severity = severity;
// reason is the beginning of the string
parsed.reason = ToReason(reason.substr(0, last_dash));
}
return parsed;
}
// Populates |collection| with the collection of strings in |attr|.
void ParseCollection(ipp_attribute_t* attr,
std::vector<std::string>* collection) {
int count = ippGetCount(attr);
for (int i = 0; i < count; i++) {
const char* const value = ippGetString(attr, i, nullptr);
if (value) {
collection->push_back(value);
}
}
}
// Parse a field for the CupsJob |job| from IPP attribute |attr| using the
// attribute name |name|.
void ParseField(ipp_attribute_t* attr, base::StringPiece name, CupsJob* job) {
DCHECK(!name.empty());
if (name == kJobId) {
job->id = ippGetInteger(attr, 0);
} else if (name == kJobImpressionsCompleted) {
job->current_pages = ippGetInteger(attr, 0);
} else if (name == kJobState) {
job->state = ToJobState(attr);
} else if (name == kJobStateReasons) {
ParseCollection(attr, &(job->state_reasons));
} else if (name == kJobStateMessage) {
const char* message_string = ippGetString(attr, 0, nullptr);
if (message_string) {
job->state_message = message_string;
}
} else if (name == kTimeAtProcessing) {
job->processing_started = ippGetInteger(attr, 0);
}
}
// Returns a new CupsJob allocated in |jobs| with |printer_id| populated.
CupsJob* NewJob(const std::string& printer_id, std::vector<CupsJob>* jobs) {
jobs->emplace_back();
CupsJob* job = &jobs->back();
job->printer_id = printer_id;
return job;
}
void ParseJobs(ipp_t* response,
const std::string& printer_id,
ipp_attribute_t* starting_attr,
std::vector<CupsJob>* jobs) {
// We know this is a non-empty job section. Start parsing fields for at least
// one job.
CupsJob* current_job = NewJob(printer_id, jobs);
for (ipp_attribute_t* attr = starting_attr; attr != nullptr;
attr = ippNextAttribute(response)) {
const char* const attribute_name = ippGetName(attr);
// Separators indicate a new job. Separators have empty names.
if (!attribute_name || strlen(attribute_name) == 0) {
current_job = NewJob(printer_id, jobs);
continue;
}
// Continue to populate the job fileds.
ParseField(attr, attribute_name, current_job);
}
}
// Extracts PrinterInfo fields from |response| and populates |printer_info|.
// Returns true if at least printer-make-and-model and ipp-versions-supported
// were read.
bool ParsePrinterInfo(ipp_t* response, PrinterInfo* printer_info) {
for (ipp_attribute_t* attr = ippFirstAttribute(response); attr != nullptr;
attr = ippNextAttribute(response)) {
const char* const value = ippGetName(attr);
if (!value) {
continue;
}
base::StringPiece name(value);
if (name == base::StringPiece(kPrinterMakeAndModel)) {
DCHECK_EQ(IPP_TAG_TEXT, ippGetValueTag(attr));
const char* make_and_model_string = ippGetString(attr, 0, nullptr);
if (make_and_model_string) {
printer_info->make_and_model = make_and_model_string;
}
} else if (name == base::StringPiece(kIppVersionsSupported)) {
std::vector<std::string> ipp_versions;
ParseCollection(attr, &ipp_versions);
for (const std::string& version : ipp_versions) {
base::Version major_minor(version);
if (major_minor.IsValid()) {
printer_info->ipp_versions.push_back(major_minor);
}
}
} else if (name == base::StringPiece(kIppFeaturesSupported)) {
std::vector<std::string> features;
ParseCollection(attr, &features);
printer_info->ipp_everywhere = base::Contains(features, kIppEverywhere);
} else if (name == base::StringPiece(kDocumentFormatSupported)) {
ParseCollection(attr, &printer_info->document_formats);
}
}
if (printer_info->ipp_versions.empty()) {
// ipp-versions-supported is missing from the response. This is IPP 1.0.
printer_info->ipp_versions.push_back(base::Version({1, 0}));
}
// All IPP versions require make and model to be populated so we use it to
// verify that we parsed the response.
return !printer_info->make_and_model.empty();
}
// Returns true if |status| represents a complete failure in the IPP request.
bool StatusError(ipp_status_e status) {
return status != IPP_STATUS_OK &&
status != IPP_STATUS_OK_IGNORED_OR_SUBSTITUTED;
}
} // namespace
CupsJob::CupsJob() = default;
CupsJob::CupsJob(const CupsJob& other) = default;
CupsJob::~CupsJob() = default;
PrinterInfo::PrinterInfo() = default;
PrinterInfo::~PrinterInfo() = default;
std::string PrinterUriFromName(const std::string& id) {
return base::StringPrintf("ipp://localhost/printers/%s", id.c_str());
}
void ParseJobsResponse(ipp_t* response,
const std::string& printer_id,
std::vector<CupsJob>* jobs) {
// Advance the position in the response to the jobs section.
ipp_attribute_t* attr = ippFirstAttribute(response);
while (attr != nullptr && ippGetGroupTag(attr) != IPP_TAG_JOB) {
attr = ippNextAttribute(response);
}
if (attr != nullptr) {
ParseJobs(response, printer_id, attr, jobs);
}
}
// Returns an IPP response for a Get-Printer-Attributes request to |http|. For
// print servers, |printer_uri| is used as the printer-uri value.
// |resource_path| specifies the path portion of the server URI.
// |num_attributes| is the number of attributes in |attributes| which should be
// a list of IPP attributes. |status| is updated with status code for the
// request. A successful request will have the |status| IPP_STATUS_OK.
ScopedIppPtr GetPrinterAttributes(http_t* http,
const std::string& printer_uri,
const std::string& resource_path,
int num_attributes,
const char* const* attributes,
ipp_status_t* status) {
DCHECK(http);
// CUPS expects a leading slash for resource names. Add one if it's missing.
std::string rp = !resource_path.empty() && resource_path.front() == '/'
? resource_path
: "/" + resource_path;
auto request = WrapIpp(ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES));
// We support IPP up to 2.2 but are compatible down to v1.1.
ippSetVersion(request.get(), 1, 1);
ippAddString(request.get(), IPP_TAG_OPERATION, IPP_TAG_URI, kPrinterUri,
nullptr, printer_uri.c_str());
ippAddStrings(request.get(), IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
kRequestedAttributes, num_attributes, nullptr, attributes);
DCHECK_EQ(ippValidateAttributes(request.get()), 1);
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
auto response = WrapIpp(cupsDoRequest(http, request.release(), rp.c_str()));
*status = ippGetStatusCode(response.get());
return response;
}
void ParsePrinterStatus(ipp_t* response, PrinterStatus* printer_status) {
*printer_status = PrinterStatus();
for (ipp_attribute_t* attr = ippFirstAttribute(response); attr != nullptr;
attr = ippNextAttribute(response)) {
const char* const value = ippGetName(attr);
if (!value) {
continue;
}
base::StringPiece name(value);
if (name == kPrinterState) {
DCHECK_EQ(IPP_TAG_ENUM, ippGetValueTag(attr));
printer_status->state = static_cast<ipp_pstate_t>(ippGetInteger(attr, 0));
} else if (name == kPrinterStateReasons) {
std::vector<std::string> reason_strings;
ParseCollection(attr, &reason_strings);
for (const std::string& reason : reason_strings) {
printer_status->reasons.push_back(ToPrinterReason(reason));
}
} else if (name == kPrinterStateMessage) {
const char* message_string = ippGetString(attr, 0, nullptr);
if (message_string) {
printer_status->message = message_string;
}
}
}
}
PrinterQueryResult GetPrinterInfo(const std::string& address,
const int port,
const std::string& resource,
bool encrypted,
PrinterInfo* printer_info,
PrinterStatus* printer_status) {
DCHECK(printer_info);
DCHECK(printer_status);
ScopedHttpPtr http = ScopedHttpPtr(httpConnect2(
address.c_str(), port, nullptr, AF_INET,
encrypted ? HTTP_ENCRYPTION_ALWAYS : HTTP_ENCRYPTION_IF_REQUESTED, 0,
kHttpConnectTimeoutMs, nullptr));
if (!http) {
LOG(WARNING) << "Could not connect to host";
return PrinterQueryResult::kUnreachable;
}
// TODO(crbug.com/821497): Use a library to canonicalize the URL.
size_t first_non_slash = resource.find_first_not_of('/');
const std::string path = (first_non_slash == std::string::npos)
? ""
: resource.substr(first_non_slash);
std::string printer_uri =
base::StringPrintf("%s://%s:%d/%s", encrypted ? kIppsScheme : kIppScheme,
address.c_str(), port, path.c_str());
ipp_status_t status;
ScopedIppPtr response = GetPrinterAttributes(
http.get(), printer_uri, resource, kPrinterInfoAndStatus.size(),
kPrinterInfoAndStatus.data(), &status);
if (StatusError(status) || response.get() == nullptr) {
LOG(WARNING) << "Get attributes failure: " << status;
return PrinterQueryResult::kUnknownFailure;
}
ParsePrinterStatus(response.get(), printer_status);
if (ParsePrinterInfo(response.get(), printer_info)) {
return PrinterQueryResult::kSuccess;
}
return PrinterQueryResult::kUnknownFailure;
}
bool GetPrinterStatus(http_t* http,
const std::string& printer_id,
PrinterStatus* printer_status) {
ipp_status_t status;
const std::string printer_uri = PrinterUriFromName(printer_id);
ScopedIppPtr response =
GetPrinterAttributes(http, printer_uri, "/", kPrinterAttributes.size(),
kPrinterAttributes.data(), &status);
if (status != IPP_STATUS_OK)
return false;
ParsePrinterStatus(response.get(), printer_status);
return true;
}
bool GetCupsJobs(http_t* http,
const std::string& printer_id,
int limit,
JobCompletionState which,
std::vector<CupsJob>* jobs) {
DCHECK(http);
auto request = WrapIpp(ippNewRequest(IPP_OP_GET_JOBS));
const std::string printer_uri = PrinterUriFromName(printer_id);
ippAddString(request.get(), IPP_TAG_OPERATION, IPP_TAG_URI, kPrinterUri,
nullptr, printer_uri.c_str());
ippAddInteger(request.get(), IPP_TAG_OPERATION, IPP_TAG_INTEGER, kLimit,
limit);
std::vector<const char*> job_attributes = {
kJobUri, kJobId, kJobState,
kJobStateReasons, kJobStateMessage, kJobImpressionsCompleted,
kTimeAtProcessing};
ippAddStrings(request.get(), IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
kRequestedAttributes, job_attributes.size(), nullptr,
job_attributes.data());
ippAddString(request.get(), IPP_TAG_OPERATION, IPP_TAG_KEYWORD, kWhichJobs,
nullptr, which == COMPLETED ? kCompleted : kNotCompleted);
if (ippValidateAttributes(request.get()) != 1) {
LOG(WARNING) << "Could not validate ipp request: " << cupsLastErrorString();
return false;
}
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
// cupsDoRequest will delete the request.
auto response = WrapIpp(cupsDoRequest(http, request.release(), "/"));
ipp_status_t status = ippGetStatusCode(response.get());
if (status != IPP_STATUS_OK) {
LOG(WARNING) << "IPP Error: " << cupsLastErrorString();
return false;
}
ParseJobsResponse(response.get(), printer_id, jobs);
return true;
}
} // namespace printing