#include "printing/backend/cups_jobs.h"
#include <cups/ipp.h>
#include <array>
#include <cstring>
#include <memory>
#include <string>
#include <string_view>
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/version.h"
#include "build/build_config.h"
#include "printing/backend/cups_deleters.h"
#include "printing/backend/cups_helper.h"
#include "printing/backend/cups_ipp_helper.h"
#include "printing/printer_status.h"
#if BUILDFLAG(IS_LINUX)
#include "printing/backend/cups_weak_functions.h"
#endif
namespace printing {
namespace {
using PReason = PrinterStatus::PrinterReason::Reason;
using PSeverity = PrinterStatus::PrinterReason::Severity;
constexpr char kPrinterUri[] = "printer-uri";
constexpr char kPrinterState[] = "printer-state";
constexpr char kPrinterStateReasons[] = "printer-state-reasons";
constexpr char kPrinterStateMessage[] = "printer-state-message";
constexpr std::string_view kPrinterMakeAndModel = "printer-make-and-model";
constexpr std::string_view kIppVersionsSupported = "ipp-versions-supported";
constexpr std::string_view kIppFeaturesSupported = "ipp-features-supported";
constexpr std::string_view kDocumentFormatSupported =
"document-format-supported";
constexpr std::string_view kDocumentFormatPreferred =
"document-format-preferred";
constexpr std::string_view kDocumentFormatDefault = "document-format-default";
constexpr std::string_view kOauthAuthorizationServerUri =
"oauth-authorization-server-uri";
constexpr std::string_view kOauthAuthorizationScope =
"oauth-authorization-scope";
constexpr std::string_view kUrfSupported = "urf-supported";
constexpr std::string_view kPdfVersions = "pdf-versions";
constexpr std::string_view kMopriaCertified = "mopria-certified";
constexpr std::string_view kPrinterKind = "printer-kind";
constexpr char kJobUri[] = "job-uri";
constexpr char kJobId[] = "job-id";
constexpr char kJobState[] = "job-state";
constexpr char kJobStateReasons[] = "job-state-reasons";
constexpr char kJobStateMessage[] = "job-state-message";
constexpr char kJobImpressionsCompleted[] = "job-impressions-completed";
constexpr char kTimeAtProcessing[] = "time-at-processing";
constexpr char kRequestedAttributes[] = "requested-attributes";
constexpr char kWhichJobs[] = "which-jobs";
constexpr char kLimit[] = "limit";
constexpr char kCompleted[] = "completed";
constexpr char kNotCompleted[] = "not-completed";
constexpr char kIppEverywhere[] = "ipp-everywhere";
constexpr char kJobCompletedWithErrors[] = "job-completed-with-errors";
constexpr char kCupsHeldForAuthentication[] = "cups-held-for-authentication";
constexpr char kJobCanceledByUser[] = "job-canceled-by-user";
constexpr char kJobCanceledByOperator[] = "job-canceled-by-operator";
constexpr char kJobCanceledAtDevice[] = "job-canceled-at-device";
constexpr char kJobCompletedSuccessfully[] = "job-completed-successfully";
constexpr char kSeverityReport[] = "report";
constexpr char kSeverityWarn[] = "warning";
constexpr char kSeverityError[] = "error";
constexpr char kNone[] = "none";
constexpr char kMediaNeeded[] = "media-needed";
constexpr char kMediaJam[] = "media-jam";
constexpr char kMovingToPaused[] = "moving-to-paused";
constexpr char kPaused[] = "paused";
constexpr char kShutdown[] = "shutdown";
constexpr char kConnectingToDevice[] = "connecting-to-device";
constexpr char kTimedOut[] = "timed-out";
constexpr char kStopping[] = "stopping";
constexpr char kStoppedPartly[] = "stopped-partly";
constexpr char kTonerLow[] = "toner-low";
constexpr char kTonerEmpty[] = "toner-empty";
constexpr char kSpoolAreaFull[] = "spool-area-full";
constexpr char kCoverOpen[] = "cover-open";
constexpr char kInterlockOpen[] = "interlock-open";
constexpr char kDoorOpen[] = "door-open";
constexpr char kInputTrayMissing[] = "input-tray-missing";
constexpr char kMediaLow[] = "media-low";
constexpr char kMediaEmpty[] = "media-empty";
constexpr char kOutputTrayMissing[] = "output-tray-missing";
constexpr char kOutputAreaAlmostFull[] = "output-area-almost-full";
constexpr char kOutputAreaFull[] = "output-area-full";
constexpr char kMarkerSupplyLow[] = "marker-supply-low";
constexpr char kMarkerSupplyEmpty[] = "marker-supply-empty";
constexpr char kMarkerWasteAlmostFull[] = "marker-waste-almost-full";
constexpr char kMarkerWasteFull[] = "marker-waste-full";
constexpr char kFuserOverTemp[] = "fuser-over-temp";
constexpr char kFuserUnderTemp[] = "fuser-under-temp";
constexpr char kOpcNearEol[] = "opc-near-eol";
constexpr char kOpcLifeOver[] = "opc-life-over";
constexpr char kDeveloperLow[] = "developer-low";
constexpr char kDeveloperEmpty[] = "developer-empty";
constexpr char kInterpreterResourceUnavailable[] =
"interpreter-resource-unavailable";
constexpr char kCupsPkiExpired[] = "cups-pki-expired";
constexpr char kIppScheme[] = "ipp";
constexpr char kIppsScheme[] = "ipps";
constexpr int kHttpConnectTimeoutMs = 1000;
constexpr std::array<const char* const, 3> kPrinterAttributes{
{kPrinterState, kPrinterStateReasons, kPrinterStateMessage}};
constexpr std::array<const char* const, 15> kPrinterInfoAndStatus{
{kPrinterMakeAndModel.data(), kIppVersionsSupported.data(),
kIppFeaturesSupported.data(), kDocumentFormatSupported.data(),
kPrinterState, kPrinterStateReasons, kPrinterStateMessage,
kOauthAuthorizationServerUri.data(), kOauthAuthorizationScope.data(),
kDocumentFormatPreferred.data(), kDocumentFormatDefault.data(),
kUrfSupported.data(), kPdfVersions.data(), kMopriaCertified.data(),
kPrinterKind.data()}};
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;
}
}
PrinterStatus::PrinterReason::Reason ToReason(std::string_view reason) {
static constexpr auto kLabelToReasonMap =
base::MakeFixedFlatMap<std::string_view, 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},
{kCupsPkiExpired, PReason::kCupsPkiExpired},
});
const auto entry = kLabelToReasonMap.find(reason);
return entry != kLabelToReasonMap.end() ? entry->second
: PReason::kUnknownReason;
}
PSeverity ToSeverity(std::string_view severity) {
if (severity == kSeverityError)
return PSeverity::kError;
if (severity == kSeverityWarn)
return PSeverity::kWarning;
if (severity == kSeverityReport)
return PSeverity::kReport;
return PSeverity::kUnknownSeverity;
}
PrinterStatus::PrinterReason ToPrinterReason(std::string_view 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 != std::string_view::npos) {
severity = ToSeverity(reason.substr(last_dash + 1));
}
if (severity == PSeverity::kUnknownSeverity) {
parsed.severity = PSeverity::kError;
parsed.reason = ToReason(reason);
} else {
parsed.severity = severity;
parsed.reason = ToReason(reason.substr(0, last_dash));
}
return parsed;
}
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);
}
}
}
void ParseField(ipp_attribute_t* attr, std::string_view 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);
}
}
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) {
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);
if (!attribute_name || strlen(attribute_name) == 0) {
current_job = NewJob(printer_id, jobs);
continue;
}
ParseField(attr, attribute_name, current_job);
}
}
bool ParsePrinterInfo(ipp_t* response, PrinterInfo* printer_info) {
bool oauth_error = false;
for (ipp_attribute_t* attr = ippFirstAttribute(response); attr != nullptr;
attr = ippNextAttribute(response)) {
const char* const value = ippGetName(attr);
if (!value) {
continue;
}
std::string_view name(value);
if (name == kPrinterMakeAndModel) {
int tag = ippGetValueTag(attr);
if (tag != IPP_TAG_TEXT && tag != IPP_TAG_TEXTLANG) {
LOG(WARNING) << "printer-make-and-model value tag is " << tag << ".";
}
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 == 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 == kIppFeaturesSupported) {
std::vector<std::string> features;
ParseCollection(attr, &features);
printer_info->ipp_features = features;
printer_info->ipp_everywhere = base::Contains(features, kIppEverywhere);
} else if (name == kDocumentFormatSupported) {
ParseCollection(attr, &printer_info->document_formats);
} else if (name == kOauthAuthorizationServerUri) {
int tag = ippGetValueTag(attr);
if (tag != IPP_TAG_URI) {
LOG(WARNING) << "oauth-authorization-server-uri value tag is " << tag
<< ".";
}
const char* oauth_server_string = ippGetString(attr, 0, nullptr);
if (oauth_server_string) {
printer_info->oauth_server = oauth_server_string;
} else {
oauth_error = true;
LOG(WARNING) << "Cannot parse oauth-authorization-server-uri.";
}
} else if (name == kOauthAuthorizationScope) {
int tag = ippGetValueTag(attr);
if (tag != IPP_TAG_NAME) {
LOG(WARNING) << "oauth-authorization-scope value tag is " << tag << ".";
}
const char* oauth_scope_string = ippGetString(attr, 0, nullptr);
if (oauth_scope_string) {
printer_info->oauth_scope = oauth_scope_string;
} else {
oauth_error = true;
LOG(WARNING) << "Cannot parse oauth-authorization-scope.";
}
} else if (name == kDocumentFormatPreferred) {
int tag = ippGetValueTag(attr);
if (tag != IPP_TAG_TEXT && tag != IPP_TAG_TEXTLANG) {
LOG(WARNING) << "document-format-preferred value tag is " << tag << ".";
}
const char* document_format_preferred_string =
ippGetString(attr, 0, nullptr);
if (document_format_preferred_string) {
printer_info->document_format_preferred =
document_format_preferred_string;
}
} else if (name == kDocumentFormatDefault) {
int tag = ippGetValueTag(attr);
if (tag != IPP_TAG_TEXT && tag != IPP_TAG_TEXTLANG) {
LOG(WARNING) << "document-format-default value tag is " << tag << ".";
}
const char* document_format_default_string =
ippGetString(attr, 0, nullptr);
if (document_format_default_string) {
printer_info->document_format_default = document_format_default_string;
}
} else if (name == kUrfSupported) {
ParseCollection(attr, &printer_info->urf_supported);
} else if (name == kPdfVersions) {
ParseCollection(attr, &printer_info->pdf_versions);
} else if (name == kMopriaCertified) {
int tag = ippGetValueTag(attr);
if (tag != IPP_TAG_TEXT && tag != IPP_TAG_TEXTLANG) {
LOG(WARNING) << "mopria-certified value tag is " << tag << ".";
}
const char* mopria_certified_string = ippGetString(attr, 0, nullptr);
if (mopria_certified_string) {
printer_info->mopria_certified = mopria_certified_string;
}
} else if (name == kPrinterKind) {
ParseCollection(attr, &printer_info->printer_kind);
}
}
if (printer_info->ipp_versions.empty()) {
printer_info->ipp_versions.push_back(base::Version({1, 0}));
}
if (!printer_info->oauth_scope.empty() &&
printer_info->oauth_server.empty()) {
oauth_error = true;
}
if (oauth_error) {
printer_info->oauth_server.clear();
printer_info->oauth_scope.clear();
}
return !printer_info->make_and_model.empty();
}
bool StatusError(ipp_status_e status) {
return status != IPP_STATUS_OK &&
status != IPP_STATUS_OK_IGNORED_OR_SUBSTITUTED;
}
}
CupsJob::CupsJob() = default;
CupsJob::CupsJob(const CupsJob& other) = default;
CupsJob::~CupsJob() = default;
bool CupsJob::ContainsStateReason(CupsJob::JobStateReason reason) const {
return base::Contains(state_reasons, ToJobStateReasonString(reason));
}
PrinterInfo::PrinterInfo() = default;
PrinterInfo::PrinterInfo(const PrinterInfo& other) = default;
PrinterInfo::~PrinterInfo() = default;
const std::string_view ToJobStateReasonString(
CupsJob::JobStateReason state_reason) {
switch (state_reason) {
case CupsJob::JobStateReason::kJobCompletedWithErrors:
return kJobCompletedWithErrors;
case CupsJob::JobStateReason::kCupsHeldForAuthentication:
return kCupsHeldForAuthentication;
case CupsJob::JobStateReason::kJobCanceledByUser:
return kJobCanceledByUser;
case CupsJob::JobStateReason::kJobCanceledByOperator:
return kJobCanceledByOperator;
case CupsJob::JobStateReason::kJobCanceledAtDevice:
return kJobCanceledAtDevice;
case CupsJob::JobStateReason::kJobCompletedSuccessfully:
return kJobCompletedSuccessfully;
}
return "";
}
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) {
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);
}
}
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);
std::string rp = !resource_path.empty() && resource_path.front() == '/'
? resource_path
: "/" + resource_path;
auto request = WrapIpp(ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES));
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;
}
std::string_view 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);
http_addrlist_t* addr_list = httpAddrGetList(
address.c_str(), AF_UNSPEC, base::NumberToString(port).c_str());
if (!addr_list) {
LOG(WARNING) << "Unable to resolve IP address from hostname " << address
<< ": " << cupsLastErrorString();
return PrinterQueryResult::kHostnameResolution;
}
ScopedHttpPtr http = HttpConnect2(
address.c_str(), port, addr_list, AF_UNSPEC,
encrypted ? HTTP_ENCRYPTION_ALWAYS : HTTP_ENCRYPTION_IF_REQUESTED, 0,
kHttpConnectTimeoutMs, nullptr);
if (!http) {
LOG(WARNING) << "Could not connect to host " << address << ":" << port
<< ": " << cupsLastErrorString();
return PrinterQueryResult::kUnreachable;
}
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) << "Failed to get attributes from " << printer_uri << ": "
<< base::StringPrintf("0x%04x", 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) {
LOG(WARNING) << "Failed to get printer status from " << printer_uri << ": "
<< cupsLastErrorString();
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 Get-Jobs ipp request: "
<< cupsLastErrorString();
return false;
}
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
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;
}
}