| // 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 <map> |
| #include <memory> |
| |
| #include "base/logging.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/version.h" |
| #include "printing/backend/cups_deleters.h" |
| #include "printing/backend/cups_ipp_util.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, 4> kPrinterInfo{ |
| {kPrinterMakeAndModel, kIppVersionsSupported, kIppFeaturesSupported, |
| kDocumentFormatSupported}}; |
| |
| using ScopedHttpPtr = std::unique_ptr<http_t, HttpDeleter>; |
| |
| // 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::NONE}, |
| {kMediaNeeded, PReason::MEDIA_NEEDED}, |
| {kMediaJam, PReason::MEDIA_JAM}, |
| {kMovingToPaused, PReason::MOVING_TO_PAUSED}, |
| {kPaused, PReason::PAUSED}, |
| {kShutdown, PReason::SHUTDOWN}, |
| {kConnectingToDevice, PReason::CONNECTING_TO_DEVICE}, |
| {kTimedOut, PReason::TIMED_OUT}, |
| {kStopping, PReason::STOPPING}, |
| {kStoppedPartly, PReason::STOPPED_PARTLY}, |
| {kTonerLow, PReason::TONER_LOW}, |
| {kTonerEmpty, PReason::TONER_EMPTY}, |
| {kSpoolAreaFull, PReason::SPOOL_AREA_FULL}, |
| {kCoverOpen, PReason::COVER_OPEN}, |
| {kInterlockOpen, PReason::INTERLOCK_OPEN}, |
| {kDoorOpen, PReason::DOOR_OPEN}, |
| {kInputTrayMissing, PReason::INPUT_TRAY_MISSING}, |
| {kMediaLow, PReason::MEDIA_LOW}, |
| {kMediaEmpty, PReason::MEDIA_EMPTY}, |
| {kOutputTrayMissing, PReason::OUTPUT_TRAY_MISSING}, |
| {kOutputAreaAlmostFull, PReason::OUTPUT_AREA_ALMOST_FULL}, |
| {kOutputAreaFull, PReason::OUTPUT_AREA_FULL}, |
| {kMarkerSupplyLow, PReason::MARKER_SUPPLY_LOW}, |
| {kMarkerSupplyEmpty, PReason::MARKER_SUPPLY_EMPTY}, |
| {kMarkerWasteAlmostFull, PReason::MARKER_WASTE_ALMOST_FULL}, |
| {kMarkerWasteFull, PReason::MARKER_WASTE_FULL}, |
| {kFuserOverTemp, PReason::FUSER_OVER_TEMP}, |
| {kFuserUnderTemp, PReason::FUSER_UNDER_TEMP}, |
| {kOpcNearEol, PReason::OPC_NEAR_EOL}, |
| {kOpcLifeOver, PReason::OPC_LIFE_OVER}, |
| {kDeveloperLow, PReason::DEVELOPER_LOW}, |
| {kDeveloperEmpty, PReason::DEVELOPER_EMPTY}, |
| {kInterpreterResourceUnavailable, |
| PReason::INTERPRETER_RESOURCE_UNAVAILABLE}, |
| }; |
| 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::UNKNOWN_REASON; |
| } |
| |
| // 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::ERROR; |
| |
| if (severity == kSeverityWarn) |
| return PSeverity::WARNING; |
| |
| if (severity == kSeverityReport) |
| return PSeverity::REPORT; |
| |
| return PSeverity::UNKNOWN_SEVERITY; |
| } |
| |
| // 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::NONE; |
| parsed.severity = PSeverity::UNKNOWN_SEVERITY; |
| return parsed; |
| } |
| |
| size_t last_dash = reason.rfind('-'); |
| auto severity = PSeverity::UNKNOWN_SEVERITY; |
| 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::UNKNOWN_SEVERITY) { |
| // Severity is unknown. No severity in the reason. |
| // Per spec, if there is no severity, severity is error. |
| parsed.severity = PSeverity::ERROR; |
| 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++) { |
| base::StringPiece value = ippGetString(attr, i, nullptr); |
| collection->push_back(value.as_string()); |
| } |
| } |
| |
| // 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) { |
| job->state_message = ippGetString(attr, 0, nullptr); |
| } 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)) { |
| base::StringPiece attribute_name = ippGetName(attr); |
| // Separators indicate a new job. Separators have empty names. |
| if (attribute_name.empty()) { |
| current_job = NewJob(printer_id, jobs); |
| continue; |
| } |
| |
| // Continue to populate the job fileds. |
| ParseField(attr, attribute_name, current_job); |
| } |
| } |
| |
| // Returns the uri for printer with |id| as served by CUPS. Assumes that |id| |
| // is a valid CUPS printer name and performs no error checking or escaping. |
| std::string PrinterUriFromName(const std::string& id) { |
| return base::StringPrintf("ipp://localhost/printers/%s", id.c_str()); |
| } |
| |
| // 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)) { |
| base::StringPiece name = ippGetName(attr); |
| if (name == base::StringPiece(kPrinterMakeAndModel)) { |
| DCHECK_EQ(IPP_TAG_TEXT, ippGetValueTag(attr)); |
| printer_info->make_and_model = ippGetString(attr, 0, nullptr); |
| } 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::ContainsValue(features, kIppEverywhere); |
| } else if (name == base::StringPiece(kDocumentFormatSupported)) { |
| ParseCollection(attr, &printer_info->document_formats); |
| } |
| } |
| |
| return !printer_info->make_and_model.empty() && |
| !printer_info->ipp_versions.empty(); |
| } |
| |
| } // namespace |
| |
| CupsJob::CupsJob() = default; |
| |
| CupsJob::CupsJob(const CupsJob& other) = default; |
| |
| CupsJob::~CupsJob() = default; |
| |
| PrinterStatus::PrinterStatus() = default; |
| |
| PrinterStatus::PrinterStatus(const PrinterStatus& other) = default; |
| |
| PrinterStatus::~PrinterStatus() = default; |
| |
| PrinterInfo::PrinterInfo() = default; |
| |
| PrinterInfo::~PrinterInfo() = default; |
| |
| 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) { |
| base::AssertBlockingAllowedDeprecated(); |
| 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); |
| |
| auto response = WrapIpp(cupsDoRequest(http, request.release(), rp.c_str())); |
| *status = ippGetStatusCode(response.get()); |
| |
| return response; |
| } |
| |
| void ParsePrinterStatus(ipp_t* response, PrinterStatus* printer_status) { |
| for (ipp_attribute_t* attr = ippFirstAttribute(response); attr != nullptr; |
| attr = ippNextAttribute(response)) { |
| base::StringPiece name = ippGetName(attr); |
| if (name.empty()) { |
| continue; |
| } |
| |
| 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) { |
| printer_status->message = ippGetString(attr, 0, nullptr); |
| } |
| } |
| } |
| |
| bool GetPrinterInfo(const std::string& address, |
| const int port, |
| const std::string& resource, |
| bool encrypted, |
| PrinterInfo* printer_info) { |
| base::AssertBlockingAllowedDeprecated(); |
| |
| 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 false; |
| } |
| |
| // 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, |
| kPrinterInfo.size(), kPrinterInfo.data(), &status); |
| if (status != IPP_STATUS_OK || response.get() == nullptr) { |
| LOG(WARNING) << "Get attributes failure: " << status; |
| return false; |
| } |
| |
| return ParsePrinterInfo(response.get(), printer_info); |
| } |
| |
| bool GetPrinterStatus(http_t* http, |
| const std::string& printer_id, |
| PrinterStatus* printer_status) { |
| base::AssertBlockingAllowedDeprecated(); |
| |
| 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) { |
| base::AssertBlockingAllowedDeprecated(); |
| 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; |
| } |
| |
| // 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 |