diff --git a/libkcups/JobModel.h b/libkcups/JobModel.h --- a/libkcups/JobModel.h +++ b/libkcups/JobModel.h @@ -48,7 +48,8 @@ RoleJobReleaseEnabled, RoleJobRestartEnabled, RoleJobPrinter, - RoleJobOriginatingHostName + RoleJobOriginatingHostName, + RoleJobAuthenticationRequired }; enum JobAction { diff --git a/libkcups/JobModel.cpp b/libkcups/JobModel.cpp --- a/libkcups/JobModel.cpp +++ b/libkcups/JobModel.cpp @@ -155,6 +155,8 @@ KCUPS_JOB_K_OCTETS, KCUPS_JOB_K_OCTETS_PROCESSED, KCUPS_JOB_STATE, + KCUPS_JOB_STATE_REASONS, + KCUPS_JOB_HOLD_UNTIL, KCUPS_TIME_AT_COMPLETED, KCUPS_TIME_AT_CREATION, KCUPS_TIME_AT_PROCESSING, @@ -345,6 +347,8 @@ colStatus->setData(pages, RoleJobPages); } + colStatus->setData(job.authenticationRequired(), RoleJobAuthenticationRequired); + // internal dest name & column const QString destName = job.printer(); if (colStatus->data(RoleJobPrinter).toString() != destName) { diff --git a/libkcups/KCupsConnection.h b/libkcups/KCupsConnection.h --- a/libkcups/KCupsConnection.h +++ b/libkcups/KCupsConnection.h @@ -83,6 +83,8 @@ #define KCUPS_JOB_MEDIA_SHEETS_COMPLETED QLatin1String("job-media-sheets-completed") #define KCUPS_JOB_PRESERVED QLatin1String("job-preserved") #define KCUPS_JOB_STATE QLatin1String("job-state") +#define KCUPS_JOB_STATE_REASONS QLatin1String("job-state-reasons") +#define KCUPS_JOB_HOLD_UNTIL QLatin1String("job-hold-until") #define KCUPS_JOB_SHEETS_DEFAULT QLatin1String("job-sheets-default") #define KCUPS_JOB_SHEETS_SUPPORTED QLatin1String("job-sheets-supported") #define KCUPS_JOB_SHEETS_DEFAULT QLatin1String("job-sheets-default") @@ -109,6 +111,9 @@ #define KCUPS_NOTIFY_LEASE_DURATION QLatin1String("notify-lease-duration") #define KCUPS_NOTIFY_SUBSCRIPTION_ID QLatin1String("notify-subscription-id") +#define KCUPS_AUTH_INFO QLatin1String("auth-info") +#define KCUPS_AUTH_INFO_REQUIRED QLatin1String("auth-info-required") + typedef QList ReturnArguments; class KIppRequest; diff --git a/libkcups/KCupsJob.h b/libkcups/KCupsJob.h --- a/libkcups/KCupsJob.h +++ b/libkcups/KCupsJob.h @@ -50,7 +50,10 @@ static QString iconName(ipp_jstate_t state); ipp_jstate_t state() const; QString stateMsg() const; + QString stateReason() const; + QString holdUntil() const; + bool authenticationRequired() const; bool reprintEnabled() const; static bool cancelEnabled(ipp_jstate_t state); diff --git a/libkcups/KCupsJob.cpp b/libkcups/KCupsJob.cpp --- a/libkcups/KCupsJob.cpp +++ b/libkcups/KCupsJob.cpp @@ -164,6 +164,21 @@ return m_arguments[KCUPS_JOB_PRINTER_STATE_MESSAGE].toString(); } +QString KCupsJob::stateReason() const +{ + return m_arguments[KCUPS_JOB_STATE_REASONS].toString(); +} + +QString KCupsJob::holdUntil() const +{ + return m_arguments[KCUPS_JOB_HOLD_UNTIL].toString(); +} + +bool KCupsJob::authenticationRequired() const +{ + return stateReason() == QStringLiteral("cups-held-for-authentication") || holdUntil() == QStringLiteral("auth-info-required"); +} + bool KCupsJob::reprintEnabled() const { if (state() >= IPP_JOB_STOPPED && preserved()) { diff --git a/libkcups/KCupsPrinter.h b/libkcups/KCupsPrinter.h --- a/libkcups/KCupsPrinter.h +++ b/libkcups/KCupsPrinter.h @@ -58,6 +58,7 @@ QStringList jobSheetsSupported() const; QStringList requestingUserNameAllowed() const; QStringList requestingUserNameDenied() const; + QStringList authInfoRequired() const; QString uriSupported() const; Status state() const; diff --git a/libkcups/KCupsPrinter.cpp b/libkcups/KCupsPrinter.cpp --- a/libkcups/KCupsPrinter.cpp +++ b/libkcups/KCupsPrinter.cpp @@ -142,6 +142,11 @@ return m_arguments[QLatin1String(KCUPS_REQUESTING_USER_NAME_DENIED)].toStringList(); } +QStringList KCupsPrinter::authInfoRequired() const +{ + return m_arguments[QLatin1String(KCUPS_AUTH_INFO_REQUIRED)].toStringList(); +} + QString KCupsPrinter::uriSupported() const { return m_arguments[QLatin1String(KCUPS_PRINTER_URI_SUPPORTED)].toString(); diff --git a/libkcups/KCupsRequest.h b/libkcups/KCupsRequest.h --- a/libkcups/KCupsRequest.h +++ b/libkcups/KCupsRequest.h @@ -283,6 +283,8 @@ */ void moveJob(const QString &fromPrinterName, int jobId, const QString &toPrinterName); + void authenticateJob(const QString &printerName, const QStringList authInfo, int jobId); + signals: void device(const QString &device_class, const QString &device_id, diff --git a/libkcups/KCupsRequest.cpp b/libkcups/KCupsRequest.cpp --- a/libkcups/KCupsRequest.cpp +++ b/libkcups/KCupsRequest.cpp @@ -537,6 +537,16 @@ process(request); } +void KCupsRequest::authenticateJob(const QString &printerName, const QStringList authInfo, int jobId) +{ + KIppRequest request(IPP_OP_CUPS_AUTHENTICATE_JOB, QLatin1String("/jobs/")); + request.addPrinterUri(printerName); + request.addInteger(IPP_TAG_OPERATION, IPP_TAG_INTEGER, KCUPS_JOB_ID, jobId); + request.addStringList(IPP_TAG_OPERATION, IPP_TAG_TEXT, KCUPS_AUTH_INFO, authInfo); + + process(request); +} + void KCupsRequest::invokeMethod(const char *method, const QVariant &arg1, const QVariant &arg2, diff --git a/printqueue/PrintQueueUi.h b/printqueue/PrintQueueUi.h --- a/printqueue/PrintQueueUi.h +++ b/printqueue/PrintQueueUi.h @@ -62,6 +62,7 @@ void holdJob(); void resumeJob(); void reprintJob(); + void authenticateJob(); int columnCount(const QModelIndex &parent = QModelIndex()) const; void updateButtons(); diff --git a/printqueue/PrintQueueUi.cpp b/printqueue/PrintQueueUi.cpp --- a/printqueue/PrintQueueUi.cpp +++ b/printqueue/PrintQueueUi.cpp @@ -42,6 +42,10 @@ #include #include #include +#include +#include +#include +#include #define PRINTER_ICON_SIZE 92 @@ -263,59 +267,81 @@ if (!ui->jobsView->indexAt(point).isValid() || m_preparingMenu) { return; } - m_preparingMenu = true; - bool moveTo = false; - QItemSelection selection; // we need to map the selection to source to get the real indexes - selection = m_proxyModel->mapSelectionToSource(ui->jobsView->selectionModel()->selection()); + QItemSelection selection = m_proxyModel->mapSelectionToSource(ui->jobsView->selectionModel()->selection()); // if the selection is empty the user clicked on an empty space - if (!selection.indexes().isEmpty()) { - const QModelIndexList indexes = selection.indexes(); - for (const QModelIndex &index : indexes) { - if (index.column() == 0 && index.flags() & Qt::ItemIsDragEnabled) { + if (selection.indexes().isEmpty()) { + return; + } + + m_preparingMenu = true; + + // context menu + auto menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + + bool moveTo = false; + bool authenticate = false; + const QModelIndexList indexes = selection.indexes(); + for (const QModelIndex &index : indexes) { + if (index.column() == JobModel::Columns::ColStatus) { + if (index.flags() & Qt::ItemIsDragEnabled) { // Found a move to item moveTo = true; - break; } - } - // if we can move a job create the menu - if (moveTo) { - // context menu - auto menu = new QMenu(this); - // move to menu - auto moveToMenu = new QMenu(i18n("Move to"), this); - - // get printers we can move to - QPointer request = new KCupsRequest; - request->getPrinters({ KCUPS_PRINTER_NAME, KCUPS_PRINTER_INFO }); - request->waitTillFinished(); - if (!request) { - return; + if (index.data(JobModel::Role::RoleJobAuthenticationRequired).toBool()) { + // Found an item which requires authentication + authenticate = true; } - const KCupsPrinters printers = request->printers(); - request->deleteLater(); + } + } - for (const KCupsPrinter &printer : printers) { - // If there is a printer and it's not the current one add it - // as a new destination - if (printer.name() != m_destName) { - QAction *action = moveToMenu->addAction(printer.info()); - action->setData(printer.name()); - } - } + // if we can move a job create the menu + if (moveTo) { + // move to menu + auto moveToMenu = new QMenu(i18n("Move to"), menu); + + // get printers we can move to + QPointer request = new KCupsRequest; + request->getPrinters({ KCUPS_PRINTER_NAME, KCUPS_PRINTER_INFO }); + request->waitTillFinished(); + if (!request) { + return; + } + const KCupsPrinters printers = request->printers(); + request->deleteLater(); - if (!moveToMenu->isEmpty()) { - menu->addMenu(moveToMenu); - // show the menu on the right point - QAction *action = menu->exec(ui->jobsView->mapToGlobal(point)); - if (action) { - // move the job - modifyJob(JobModel::Move, action->data().toString()); - } + for (const KCupsPrinter &printer : printers) { + // If there is a printer and it's not the current one add it + // as a new destination + if (printer.name() != m_destName) { + QAction *action = moveToMenu->addAction(printer.info()); + action->setData(printer.name()); + connect(action, &QAction::triggered, this, [=] () { + this->modifyJob(JobModel::Move, action->data().toString()); + }); } } + + if (!moveToMenu->isEmpty()) { + menu->addMenu(moveToMenu); + } } + + // Authenticate + if (authenticate) { + QAction *action = menu->addAction(i18n("Authenticate")); + connect(action, &QAction::triggered, this, &PrintQueueUi::authenticateJob); + } + + if (menu->isEmpty()) { + delete menu; + } else { + // show the menu on the right point + menu->popup(ui->jobsView->mapToGlobal(point)); + } + m_preparingMenu = false; } @@ -559,6 +585,74 @@ modifyJob(JobModel::Reprint); } +void PrintQueueUi::authenticateJob() +{ + QScopedPointer request(new KCupsRequest); + request->getPrinterAttributes(m_destName, m_isClass, { KCUPS_PRINTER_URI_SUPPORTED, KCUPS_AUTH_INFO_REQUIRED }); + request->waitTillFinished(); + if (request->hasError() || request->printers().size() != 1) { + qWarning() << "Ignoring request, printer not found or error" << m_destName << request->errorMsg(); + } + + const KCupsPrinter printer = request->printers().first(); + + KIO::AuthInfo info; + info.keepPassword = true; + info.prompt = i18n("Enter credentials to print from %1", m_destName); + info.url = QUrl(printer.uriSupported()); + if (printer.authInfoRequired().contains(QStringLiteral("domain"))) { + info.setExtraField(QStringLiteral("domain"), QStringLiteral("")); + } + + QScopedPointer passwdServerClient(new KPasswdServerClient()); + + auto winId = static_cast(this->winId()); + auto usertime = static_cast(KUserTimestamp::userTimestamp()); + if (passwdServerClient->checkAuthInfo(&info, winId, usertime)) { + // at this stage we don't know if stored credentials are correct + // trying blindly is risky, it may block this user's account + passwdServerClient->removeAuthInfo(info.url.host(), info.url.scheme(), info.username); + } + const int passwordDialogErrorCode = passwdServerClient->queryAuthInfo(&info, QString(), winId, usertime); + if (passwordDialogErrorCode != KJob::NoError) { + // user cancelled or kiod_kpasswdserver not running + qDebug() << "queryAuthInfo error code" << passwordDialogErrorCode; + return; + } + + QStringList authInfo; + for (QString &authInfoRequired : printer.authInfoRequired()) { + if (authInfoRequired == QStringLiteral("domain")) { + authInfo << info.getExtraField(QStringLiteral("domain")).toString(); + } else if (authInfoRequired == QStringLiteral("username")) { + authInfo << info.username; + } else if (authInfoRequired == QStringLiteral("password")) { + authInfo << info.password; + } else { + qWarning() << "No auth info for: " << authInfoRequired; + authInfo << QString(); + } + } + + // we need to map the selection to source to get the real indexes + QItemSelection selection = m_proxyModel->mapSelectionToSource(ui->jobsView->selectionModel()->selection()); + const QModelIndexList indexes = selection.indexes(); + for (const QModelIndex &index : indexes) { + if (index.column() == JobModel::Columns::ColStatus) { + QScopedPointer authRequest(new KCupsRequest); + authRequest->authenticateJob(m_destName, authInfo, index.data(JobModel::Role::RoleJobId).toInt()); + authRequest->waitTillFinished(); + if (authRequest->hasError()) { + qWarning() << "Error authenticating jobs" << authRequest->error() << authRequest->errorMsg(); + // remove cache on fail + passwdServerClient->removeAuthInfo(info.url.host(), info.url.scheme(), info.username); + return; + } + } + } + +} + void PrintQueueUi::whichJobsIndexChanged(int index) { switch (index) {