diff --git a/src/commands/refreshcertificatecommand.cpp b/src/commands/refreshcertificatecommand.cpp index 6a88bbf4c..983f0fedc 100644 --- a/src/commands/refreshcertificatecommand.cpp +++ b/src/commands/refreshcertificatecommand.cpp @@ -1,293 +1,433 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/refreshcertificatecommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "refreshcertificatecommand.h" #include "command_p.h" #include #include #include +// note: QGPGME_SUPPORTS_KEY_REFRESH implies QGPGME_SUPPORTS_WKDLOOKUP #ifdef QGPGME_SUPPORTS_KEY_REFRESH #include #include +#include +#include #endif #include #include "kleopatra_debug.h" using namespace Kleo; using namespace GpgME; +static QDebug operator<<(QDebug s, const std::string &string) +{ + return s << QString::fromStdString(string); +} + +namespace +{ +enum class JobType +{ + ReceiveKeys, + // WKDLookup, + RefreshKeys, +}; + +struct JobData +{ + GpgME::Protocol protocol; + JobType type; + std::string email; + QPointer job; + std::vector connections; +}; + +bool operator==(const JobData &lhs, const JobData &rhs) +{ + return lhs.job == rhs.job; +} + +struct JobResultData +{ + JobResultData(GpgME::Protocol p, JobType t, const std::string &e, const GpgME::ImportResult &r) + : protocol{p} + , type{t} + , email{e} + , error{r.error()} + , importResult{r} + {} + // JobResultData(GpgME::Protocol p, JobType t, const std::string &e, const QGpgME::WKDLookupResult &r) + // : protocol{p} + // , type{t} + // , email{e} + // , error{r.error()} + // , wkdResult{r} + // {} + GpgME::Protocol protocol; + JobType type; + std::string email; + GpgME::Error error; + GpgME::ImportResult importResult; + // QGpgME::WKDLookupResult wkdResult; + // Kleo::AuditLogEntry auditLog; +}; +} + class RefreshCertificateCommand::Private : public Command::Private { friend class ::RefreshCertificateCommand; RefreshCertificateCommand *q_func() const { return static_cast(q); } public: explicit Private(RefreshCertificateCommand *qq); ~Private() override; void start(); void cancel(); #ifdef QGPGME_SUPPORTS_KEY_REFRESH - std::unique_ptr startOpenPGPJob(); - std::unique_ptr startSMIMEJob(); + void startReceiveKeysJob(); + void startWKDLookupJob(const std::string &email); + void startOpenPGPJobs(); + + void startRefreshSMIMEKeysJob(); +#endif + + void onReceiveKeysJobResult(const ImportResult &result, QGpgME::Job *finishedJob = nullptr); +#ifdef QGPGME_SUPPORTS_WKDLOOKUP + void onWKDLookupJobResult(const QGpgME::WKDLookupResult &result, QGpgME::Job *finishedJob = nullptr); #endif - void onOpenPGPJobResult(const ImportResult &result); - void onSMIMEJobResult(const Error &err); + void onRefreshKeysJobResult(const Error &err, QGpgME::Job *finishedJob = nullptr); + void onJobResult(const JobResultData &result); + + void tryToFinish(); + void processResults(); + void showError(const Error &err); private: Key key; -#ifdef QGPGME_SUPPORTS_KEY_REFRESH - QPointer job; -#endif + bool waitForMoreJobs = false; + std::vector jobs; + std::vector results; }; RefreshCertificateCommand::Private *RefreshCertificateCommand::d_func() { return static_cast(d.get()); } const RefreshCertificateCommand::Private *RefreshCertificateCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() RefreshCertificateCommand::Private::Private(RefreshCertificateCommand *qq) : Command::Private{qq} { } RefreshCertificateCommand::Private::~Private() = default; namespace { Key getKey(const std::vector &keys) { if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Expected exactly one key, but got" << keys.size(); return {}; } const Key key = keys.front(); if (key.protocol() == GpgME::UnknownProtocol) { qCWarning(KLEOPATRA_LOG) << "Key has unknown protocol"; return {}; } return key; } } void RefreshCertificateCommand::Private::start() { key = getKey(keys()); if (key.isNull()) { finished(); return; } #ifdef QGPGME_SUPPORTS_KEY_REFRESH - std::unique_ptr refreshJob; switch (key.protocol()) { case GpgME::OpenPGP: - refreshJob = startOpenPGPJob(); + startReceiveKeysJob(); break; case GpgME::CMS: - refreshJob = startSMIMEJob(); + startRefreshSMIMEKeysJob(); break; default: ; // cannot happen ;-) } - if (!refreshJob) { - finished(); - return; - } - job = refreshJob.release(); + tryToFinish(); #else KMessageBox::error(parentWidgetOrView(), i18n("The backend does not support updating individual certificates.")); finished(); #endif } +static void disconnectConnection(const QMetaObject::Connection &connection) +{ + // trivial function for disconnecting a signal-slot connection because + // using a lambda seems to confuse older GCC / MinGW and unnecessarily + // capturing 'this' generates warnings + QObject::disconnect(connection); +} + void RefreshCertificateCommand::Private::cancel() { + const auto jobsToCancel = jobs; + std::for_each(std::begin(jobsToCancel), std::end(jobsToCancel), [](const auto &job) { + qCDebug(KLEOPATRA_LOG) << "Canceling job" << job.job; + std::for_each(std::cbegin(job.connections), std::cend(job.connections), &disconnectConnection); + job.job->slotCancel(); + // onJobFinished(resultForCanceledJob(job), job.job); + }); +} + #ifdef QGPGME_SUPPORTS_KEY_REFRESH - if (job) { - job->slotCancel(); +void RefreshCertificateCommand::Private::startReceiveKeysJob() +{ + std::unique_ptr receiveJob{QGpgME::openpgp()->receiveKeysJob()}; + Q_ASSERT(receiveJob); + + std::vector connections = { + connect(receiveJob.get(), &QGpgME::ReceiveKeysJob::result, q, [this](const GpgME::ImportResult &result) { + onReceiveKeysJobResult(result); + }), + connect(receiveJob.get(), &QGpgME::Job::progress, q, &Command::progress), + }; + + const GpgME::Error err = receiveJob->start({QString::fromLatin1(key.primaryFingerprint())}); + if (err.code()) { + onJobResult({GpgME::CMS, JobType::ReceiveKeys, "", ImportResult{err}}); + //, AuditLogEntry{}}); + } else { + // increaseProgressMaximum(); + jobs.push_back({GpgME::CMS, JobType::ReceiveKeys, "", receiveJob.release(), connections}); } - job.clear(); -#endif } -#ifdef QGPGME_SUPPORTS_KEY_REFRESH -std::unique_ptr RefreshCertificateCommand::Private::startOpenPGPJob() +void RefreshCertificateCommand::Private::startRefreshSMIMEKeysJob() { - std::unique_ptr refreshJob{QGpgME::openpgp()->receiveKeysJob()}; + Q_ASSERT(jobs.empty()); + std::unique_ptr refreshJob{QGpgME::smime()->refreshKeysJob()}; Q_ASSERT(refreshJob); - connect(refreshJob.get(), &QGpgME::ReceiveKeysJob::result, - q, [this](const GpgME::ImportResult &result) { - onOpenPGPJobResult(result); - }); - connect(refreshJob.get(), &QGpgME::Job::progress, - q, &Command::progress); + std::vector connections = { + connect(refreshJob.get(), &QGpgME::RefreshKeysJob::result, q, [this](const GpgME::Error &err) { + onRefreshKeysJobResult(err); + }), + connect(refreshJob.get(), &QGpgME::Job::progress, q, &Command::progress), + }; - const GpgME::Error err = refreshJob->start({QString::fromLatin1(key.primaryFingerprint())}); - if (err) { - showError(err); - return {}; + const GpgME::Error err = refreshJob->start({key}); + if (err.code()) { + onJobResult({GpgME::CMS, JobType::RefreshKeys, "", ImportResult{err}}); + //, AuditLogEntry{}}); + } else { + // increaseProgressMaximum(); + jobs.push_back({GpgME::CMS, JobType::RefreshKeys, "", refreshJob.release(), connections}); } - Q_EMIT q->info(i18nc("@info:status", "Updating key...")); +} +#endif - return refreshJob; +// #ifdef QGPGME_SUPPORTS_WKDLOOKUP +// std::unique_ptr RefreshCertificateCommand::Private::startWKDLookupJob() +// { +// const auto job = createWKDLookupJob(); +// if (!job) { +// qCDebug(KLEOPATRA_LOG) << "Failed to create WKDLookupJob"; +// return; +// } +// connect(job, &WKDLookupJob::result, +// q, [this](const WKDLookupResult &result) { slotWKDLookupResult(result); }); +// if (const Error err = job->start(str)) { +// keyListing.result.mergeWith(KeyListResult{err}); +// } else { +// keyListing.wkdJob = job; +// } +// } +// #endif +// +void RefreshCertificateCommand::Private::onReceiveKeysJobResult(const GpgME::ImportResult &result, QGpgME::Job *finishedJob) +{ + if (!finishedJob) { + finishedJob = qobject_cast(q->sender()); + } + Q_ASSERT(finishedJob); + qCDebug(KLEOPATRA_LOG) << q << __func__ << finishedJob; + + auto it = std::find_if(std::cbegin(jobs), std::cend(jobs), + [finishedJob](const auto &job) { return job.job == finishedJob; }); + Q_ASSERT(it != std::cend(jobs)); + if (it == std::cend(jobs)) { + qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Finished job not found"; + return; + } + + const auto job = *it; + jobs.erase(std::remove(std::begin(jobs), std::end(jobs), job), std::end(jobs)); + // increaseProgressValue(); + + onJobResult({job.protocol, job.type, "", result}); } -std::unique_ptr RefreshCertificateCommand::Private::startSMIMEJob() +void RefreshCertificateCommand::Private::onRefreshKeysJobResult(const GpgME::Error &err, QGpgME::Job *finishedJob) { - std::unique_ptr refreshJob{QGpgME::smime()->refreshKeysJob()}; - Q_ASSERT(refreshJob); + if (!finishedJob) { + finishedJob = qobject_cast(q->sender()); + } + onReceiveKeysJobResult(ImportResult{err}, finishedJob); +} - connect(refreshJob.get(), &QGpgME::RefreshKeysJob::result, - q, [this](const GpgME::Error &err) { - onSMIMEJobResult(err); - }); - connect(refreshJob.get(), &QGpgME::Job::progress, - q, &Command::progress); +void RefreshCertificateCommand::Private::onJobResult(const JobResultData &result) +{ + qCDebug(KLEOPATRA_LOG) << q << __func__ << result.email << "Result:" << result.error.asString(); + results.push_back(result); + + // if (importFailed(result)) { + // showError(result); + // } + // + tryToFinish(); +} - const GpgME::Error err = refreshJob->start({key}); - if (err) { - showError(err); - return {}; +void RefreshCertificateCommand::Private::tryToFinish() +{ + qCDebug(KLEOPATRA_LOG) << q << __func__; + if (!jobs.empty()) { + qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are unfinished jobs -> keep going"; + return; } - Q_EMIT q->info(i18nc("@info:status", "Updating certificate...")); - return refreshJob; + processResults(); } -#endif namespace { static auto informationOnChanges(const ImportResult &result) { QString text; // if additional keys have been retrieved via WKD, then most of the below // details are just a guess and may concern the additional keys instead of // the refresh keys; this could only be clarified by a thorough comparison of // unrefreshed and refreshed key if (result.numUnchanged() == result.numConsidered()) { // if numUnchanged < numConsidered, then it is not clear whether the refreshed key // hasn't changed or whether another key retrieved via WKD hasn't changed text = i18n("The key hasn't changed."); } else if (result.newRevocations() > 0) { // it is possible that a revoked key has been newly imported via WKD, // but it is much more likely that the refreshed key was revoked text = i18n("The key has been revoked."); } else { // it doesn't make much sense to list below details if the key has been revoked text = i18n("The key has been updated."); QStringList details; if (result.newUserIDs() > 0) { details.push_back(i18n("New user IDs: %1", result.newUserIDs())); } if (result.newSubkeys() > 0) { details.push_back(i18n("New subkeys: %1", result.newSubkeys())); } if (result.newSignatures() > 0) { details.push_back(i18n("New signatures: %1", result.newSignatures())); } if (!details.empty()) { text += QLatin1String{"

"} + details.join(QLatin1String{"
"}); } } text = QLatin1String{"

"} + text + QLatin1String{"

"}; if (result.numImported() > 0) { text += QLatin1String{"

"} + i18np("Additionally, one new key has been retrieved.", "Additionally, %1 new keys have been retrieved.", result.numImported()) + QLatin1String{"

"}; } return text; } } -void RefreshCertificateCommand::Private::onOpenPGPJobResult(const ImportResult &result) +void RefreshCertificateCommand::Private::processResults() { - if (result.error()) { - showError(result.error()); - finished(); - return; - } - - if (!result.error().isCanceled()) { - information(informationOnChanges(result), - i18nc("@title:window", "Key Updated")); - } - finished(); -} - -void RefreshCertificateCommand::Private::onSMIMEJobResult(const Error &err) -{ - if (err) { - showError(err); - finished(); - return; - } - - if (!err.isCanceled()) { - information(i18nc("@info", "The certificate has been updated."), - i18nc("@title:window", "Certificate Updated")); + Q_ASSERT(results.size() == 1); + for (const auto &r : results) { + if (r.error.isCanceled()) { + // do nothing + } else if (r.error) { + showError(r.error); + } else { + switch (r.type) { + case JobType::ReceiveKeys: + information(informationOnChanges(r.importResult), + i18nc("@title:window", "Key Updated")); + break; + case JobType::RefreshKeys: + information(i18nc("@info", "The certificate has been updated."), + i18nc("@title:window", "Certificate Updated")); + + } + } } finished(); } void RefreshCertificateCommand::Private::showError(const Error &err) { error(xi18nc("@info", "An error occurred while updating the certificate:" "%1", QString::fromLocal8Bit(err.asString())), i18nc("@title:window", "Update Failed")); } RefreshCertificateCommand::RefreshCertificateCommand(const GpgME::Key &key) : Command{key, new Private{this}} { } RefreshCertificateCommand::~RefreshCertificateCommand() = default; void RefreshCertificateCommand::doStart() { d->start(); } void RefreshCertificateCommand::doCancel() { d->cancel(); } #undef d #undef q #include "moc_refreshcertificatecommand.cpp"