Page MenuHome GnuPG

No OneTemporary

diff --git a/src/commands/revokekeycommand.cpp b/src/commands/revokekeycommand.cpp
index 8fc0f6a89..09ab3b0f6 100644
--- a/src/commands/revokekeycommand.cpp
+++ b/src/commands/revokekeycommand.cpp
@@ -1,226 +1,246 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/revokekeycommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config-kleopatra.h>
#include "command_p.h"
#include "dialogs/revokekeydialog.h"
#include "revokekeycommand.h"
#include <Libkleo/Formatting>
#include <KLocalizedString>
#include <QGpgME/RevokeKeyJob>
#include "kleopatra_debug.h"
#include <QGpgME/Protocol>
-#include <global.h>
using namespace Kleo;
using namespace GpgME;
class RevokeKeyCommand::Private : public Command::Private
{
friend class ::RevokeKeyCommand;
RevokeKeyCommand *q_func() const
{
return static_cast<RevokeKeyCommand *>(q);
}
public:
explicit Private(RevokeKeyCommand *qq, KeyListController *c = nullptr);
~Private() override;
void start();
void cancel();
private:
void ensureDialogCreated();
void onDialogAccepted();
void onDialogRejected();
std::unique_ptr<QGpgME::RevokeKeyJob> startJob();
void onJobResult(const Error &err);
void showError(const Error &err);
private:
Key key;
QPointer<RevokeKeyDialog> dialog;
QPointer<QGpgME::RevokeKeyJob> job;
};
RevokeKeyCommand::Private *RevokeKeyCommand::d_func()
{
return static_cast<Private *>(d.get());
}
const RevokeKeyCommand::Private *RevokeKeyCommand::d_func() const
{
return static_cast<const Private *>(d.get());
}
#define d d_func()
#define q q_func()
RevokeKeyCommand::Private::Private(RevokeKeyCommand *qq, KeyListController *c)
: Command::Private{qq, c}
{
}
RevokeKeyCommand::Private::~Private() = default;
namespace
{
Key getKey(const std::vector<Key> &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::OpenPGP) {
qCWarning(KLEOPATRA_LOG) << "Expected OpenPGP key, but got" << Formatting::displayName(key.protocol()) << "key";
return {};
}
return key;
}
}
void RevokeKeyCommand::Private::start()
{
key = getKey(keys());
if (key.isNull()) {
finished();
return;
}
if (key.isRevoked()) {
information(i18nc("@info", "This key has already been revoked."));
finished();
return;
}
ensureDialogCreated();
Q_ASSERT(dialog);
dialog->setKey(key);
dialog->show();
}
void RevokeKeyCommand::Private::cancel()
{
if (job) {
job->slotCancel();
}
job.clear();
}
void RevokeKeyCommand::Private::ensureDialogCreated()
{
if (dialog) {
return;
}
dialog = new RevokeKeyDialog;
applyWindowID(dialog);
dialog->setAttribute(Qt::WA_DeleteOnClose);
connect(dialog, &QDialog::accepted, q, [this]() {
onDialogAccepted();
});
connect(dialog, &QDialog::rejected, q, [this]() {
onDialogRejected();
});
}
void RevokeKeyCommand::Private::onDialogAccepted()
{
auto revokeJob = startJob();
if (!revokeJob) {
finished();
return;
}
job = revokeJob.release();
}
void RevokeKeyCommand::Private::onDialogRejected()
{
canceled();
}
+namespace
+{
+std::vector<std::string> toStdStrings(const QStringList &l)
+{
+ std::vector<std::string> v;
+ v.reserve(l.size());
+ std::transform(std::begin(l), std::end(l), std::back_inserter(v), std::mem_fn(&QString::toStdString));
+ return v;
+}
+
+auto descriptionToLines(const QString &description)
+{
+ std::vector<std::string> lines;
+ if (!description.isEmpty()) {
+ lines = toStdStrings(description.split(QLatin1Char('\n')));
+ }
+ return lines;
+}
+}
+
std::unique_ptr<QGpgME::RevokeKeyJob> RevokeKeyCommand::Private::startJob()
{
std::unique_ptr<QGpgME::RevokeKeyJob> revokeJob{QGpgME::openpgp()->revokeKeyJob()};
Q_ASSERT(revokeJob);
connect(revokeJob.get(), &QGpgME::RevokeKeyJob::result, q, [this](const GpgME::Error &err) {
onJobResult(err);
});
connect(revokeJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress);
- const GpgME::Error err = revokeJob->start(key, GpgME::RevocationReason::Unspecified, {});
+ const auto description = descriptionToLines(dialog->description());
+ const GpgME::Error err = revokeJob->start(key, dialog->reason(), description);
if (err) {
showError(err);
return {};
}
Q_EMIT q->info(i18nc("@info:status", "Revoking key..."));
return revokeJob;
}
void RevokeKeyCommand::Private::onJobResult(const Error &err)
{
if (err) {
showError(err);
finished();
return;
}
if (!err.isCanceled()) {
information(i18nc("@info", "The key was revoked successfully."), i18nc("@title:window", "Key Revoked"));
}
finished();
}
void RevokeKeyCommand::Private::showError(const Error &err)
{
error(xi18nc("@info",
"<para>An error occurred during the revocation:</para>"
"<para><message>%1</message></para>",
Formatting::errorAsString(err)),
i18nc("@title:window", "Revocation Failed"));
}
RevokeKeyCommand::RevokeKeyCommand(QAbstractItemView *v, KeyListController *c)
: Command{v, new Private{this, c}}
{
}
RevokeKeyCommand::RevokeKeyCommand(const GpgME::Key &key)
: Command{key, new Private{this}}
{
}
RevokeKeyCommand::~RevokeKeyCommand() = default;
void RevokeKeyCommand::doStart()
{
d->start();
}
void RevokeKeyCommand::doCancel()
{
d->cancel();
}
#undef d
#undef q
#include "moc_revokekeycommand.cpp"
diff --git a/src/dialogs/revokekeydialog.cpp b/src/dialogs/revokekeydialog.cpp
index 27dde480d..baa48f6f9 100644
--- a/src/dialogs/revokekeydialog.cpp
+++ b/src/dialogs/revokekeydialog.cpp
@@ -1,135 +1,310 @@
/* -*- mode: c++; c-basic-offset:4 -*-
dialogs/revokekeydialog.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "revokekeydialog.h"
#include "utils/accessibility.h"
#include "view/errorlabel.h"
#include <Libkleo/Formatting>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KMessageBox>
#include <KSharedConfig>
#include <QApplication>
#include <QButtonGroup>
#include <QDialogButtonBox>
#include <QFocusEvent>
#include <QGroupBox>
#include <QLabel>
#include <QPushButton>
#include <QRadioButton>
#include <QRegularExpression>
#include <QTextEdit>
#include <QVBoxLayout>
#include <gpgme++/global.h>
#include <gpgme++/key.h>
#include <kleopatra_debug.h>
using namespace Kleo;
using namespace GpgME;
+namespace
+{
+class TextEdit : public QTextEdit
+{
+ Q_OBJECT
+public:
+ using QTextEdit::QTextEdit;
+
+Q_SIGNALS:
+ void editingFinished();
+
+protected:
+ void focusOutEvent(QFocusEvent *event) override
+ {
+ Qt::FocusReason reason = event->reason();
+ if (reason != Qt::PopupFocusReason || !(QApplication::activePopupWidget() && QApplication::activePopupWidget()->parentWidget() == this)) {
+ Q_EMIT editingFinished();
+ }
+
+ QTextEdit::focusOutEvent(event);
+ }
+ QSize minimumSizeHint() const override
+ {
+ return {0, fontMetrics().height() * 3};
+ }
+};
+}
+
class RevokeKeyDialog::Private
{
friend class ::Kleo::RevokeKeyDialog;
RevokeKeyDialog *const q;
struct {
QLabel *infoLabel = nullptr;
+ QLabel *descriptionLabel = nullptr;
+ TextEdit *description = nullptr;
+ ErrorLabel *descriptionError = nullptr;
QDialogButtonBox *buttonBox = nullptr;
} ui;
Key key;
+ QButtonGroup reasonGroup;
+ bool descriptionEditingInProgress = false;
+ QString descriptionAccessibleName;
public:
Private(RevokeKeyDialog *qq)
: q(qq)
{
q->setWindowTitle(i18nc("title:window", "Revoke Certificate"));
auto mainLayout = new QVBoxLayout{q};
ui.infoLabel = new QLabel{q};
- mainLayout->addWidget(ui.infoLabel);
+ auto infoGroupBox = new QGroupBox{i18nc("@title:group", "Information")};
+ infoGroupBox->setFlat(true);
+ auto infoLayout = new QVBoxLayout;
+ infoGroupBox->setLayout(infoLayout);
+ infoLayout->addWidget(ui.infoLabel);
+ mainLayout->addWidget(infoGroupBox);
+
+ auto groupBox = new QGroupBox{i18nc("@title:group", "Reason for revocation"), q};
+ groupBox->setFlat(true);
+
+ reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "No reason specified"), q}, static_cast<int>(RevocationReason::Unspecified));
+ reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "Certificate has been compromised"), q}, static_cast<int>(RevocationReason::Compromised));
+ reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "Certificate is superseded"), q}, static_cast<int>(RevocationReason::Superseded));
+ reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "Certificate is no longer used"), q}, static_cast<int>(RevocationReason::NoLongerUsed));
+ reasonGroup.button(static_cast<int>(RevocationReason::Unspecified))->setChecked(true);
+
+ {
+ auto boxLayout = new QVBoxLayout{groupBox};
+ for (auto radio : reasonGroup.buttons()) {
+ boxLayout->addWidget(radio);
+ }
+ }
+
+ mainLayout->addWidget(groupBox);
+
+ {
+ ui.descriptionLabel = new QLabel{i18nc("@label:textbox", "Description (optional):"), q};
+ ui.description = new TextEdit{q};
+ ui.description->setAcceptRichText(false);
+ // do not accept Tab as input; this is better for accessibility and
+ // tabulators are not really that useful in the description
+ ui.description->setTabChangesFocus(true);
+ ui.descriptionLabel->setBuddy(ui.description);
+ ui.descriptionError = new ErrorLabel{q};
+ ui.descriptionError->setVisible(false);
+
+ mainLayout->addWidget(ui.descriptionLabel);
+ mainLayout->addWidget(ui.description);
+ mainLayout->addWidget(ui.descriptionError);
+ }
+
+ connect(ui.description, &TextEdit::editingFinished, q, [this]() {
+ onDescriptionEditingFinished();
+ });
+ connect(ui.description, &TextEdit::textChanged, q, [this]() {
+ onDescriptionTextChanged();
+ });
ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
auto okButton = ui.buttonBox->button(QDialogButtonBox::Ok);
okButton->setText(i18nc("@action:button", "Revoke Certificate"));
okButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete-remove")));
mainLayout->addWidget(ui.buttonBox);
connect(ui.buttonBox, &QDialogButtonBox::accepted, q, [this]() {
checkAccept();
});
connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject);
restoreGeometry();
}
~Private()
{
saveGeometry();
}
private:
void saveGeometry()
{
KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("RevokeKeyDialog"));
cfgGroup.writeEntry("Size", q->size());
cfgGroup.sync();
}
void restoreGeometry(const QSize &defaultSize = {})
{
KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("RevokeKeyDialog"));
const QSize size = cfgGroup.readEntry("Size", defaultSize);
if (size.isValid()) {
q->resize(size);
} else {
q->resize(q->minimumSizeHint());
}
}
void checkAccept()
{
- q->accept();
+ if (!descriptionHasAcceptableInput()) {
+ KMessageBox::error(q, descriptionErrorMessage());
+ } else {
+ q->accept();
+ }
+ }
+
+ bool descriptionHasAcceptableInput() const
+ {
+ return !q->description().contains(QLatin1StringView{"\n\n"});
+ }
+
+ QString descriptionErrorMessage() const
+ {
+ QString message;
+
+ if (!descriptionHasAcceptableInput()) {
+ message = i18n("Error: The description must not contain empty lines.");
+ }
+ return message;
+ }
+
+ void updateDescriptionError()
+ {
+ const auto currentErrorMessage = ui.descriptionError->text();
+ const auto newErrorMessage = descriptionErrorMessage();
+ if (newErrorMessage == currentErrorMessage) {
+ return;
+ }
+ if (currentErrorMessage.isEmpty() && descriptionEditingInProgress) {
+ // delay showing the error message until editing is finished, so that we
+ // do not annoy the user with an error message while they are still
+ // entering the recipient;
+ // on the other hand, we clear the error message immediately if it does
+ // not apply anymore and we update the error message immediately if it
+ // changed
+ return;
+ }
+ ui.descriptionError->setVisible(!newErrorMessage.isEmpty());
+ ui.descriptionError->setText(newErrorMessage);
+ updateAccessibleNameAndDescription();
+ }
+
+ void updateAccessibleNameAndDescription()
+ {
+ // fall back to default accessible name if accessible name wasn't set explicitly
+ if (descriptionAccessibleName.isEmpty()) {
+ descriptionAccessibleName = getAccessibleName(ui.description);
+ }
+ const bool errorShown = ui.descriptionError->isVisible();
+
+ // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute);
+ // emulate this by setting the error message as accessible description of the input field
+ const auto description = errorShown ? ui.descriptionError->text() : QString{};
+ if (ui.description->accessibleDescription() != description) {
+ ui.description->setAccessibleDescription(description);
+ }
+
+ // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute);
+ // screen readers say something like "invalid entry" if this state is set;
+ // emulate this by adding "invalid entry" to the accessible name of the input field
+ // and its label
+ const auto name = errorShown ? descriptionAccessibleName + QLatin1StringView{", "} + invalidEntryText() //
+ : descriptionAccessibleName;
+ if (ui.descriptionLabel->accessibleName() != name) {
+ ui.descriptionLabel->setAccessibleName(name);
+ }
+ if (ui.description->accessibleName() != name) {
+ ui.description->setAccessibleName(name);
+ }
+ }
+
+ void onDescriptionTextChanged()
+ {
+ descriptionEditingInProgress = true;
+ updateDescriptionError();
+ }
+
+ void onDescriptionEditingFinished()
+ {
+ descriptionEditingInProgress = false;
+ updateDescriptionError();
}
};
RevokeKeyDialog::RevokeKeyDialog(QWidget *parent, Qt::WindowFlags f)
: QDialog{parent, f}
, d{new Private{this}}
{
}
RevokeKeyDialog::~RevokeKeyDialog() = default;
void RevokeKeyDialog::setKey(const GpgME::Key &key)
{
d->key = key;
d->ui.infoLabel->setText(
xi18nc("@info",
"<para>You are about to revoke the following certificate:</para><para>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;%1</para><para><emphasis "
"strong='true'>The "
"revocation will take effect "
"immediately and "
"cannot be reverted.</emphasis></para><para>Consequences: <list><item>You cannot sign anything anymore with this certificate.</item><item>You "
"can still decrypt everything encrypted for this certificate.</item><item>Other people can no longer encrypt for this certificate after "
"receiving the revocation.</item><item>You cannot certify other certificates anymore with this certificate.</item></list></para>")
.arg(Formatting::summaryLine(key)));
}
+GpgME::RevocationReason RevokeKeyDialog::reason() const
+{
+ return static_cast<RevocationReason>(d->reasonGroup.checkedId());
+}
+
+QString RevokeKeyDialog::description() const
+{
+ static const QRegularExpression whitespaceAtEndOfLine{QStringLiteral(R"([ \t\r]+\n)")};
+ static const QRegularExpression trailingWhitespace{QStringLiteral(R"(\s*$)")};
+ return d->ui.description->toPlainText().remove(whitespaceAtEndOfLine).remove(trailingWhitespace);
+}
+
+#include "revokekeydialog.moc"
+
#include "moc_revokekeydialog.cpp"
diff --git a/src/dialogs/revokekeydialog.h b/src/dialogs/revokekeydialog.h
index 1d8847e25..876198e09 100644
--- a/src/dialogs/revokekeydialog.h
+++ b/src/dialogs/revokekeydialog.h
@@ -1,41 +1,45 @@
/* -*- mode: c++; c-basic-offset:4 -*-
dialogs/revokekeydialog.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <config-kleopatra.h>
#include <QDialog>
#include <memory>
namespace GpgME
{
class Key;
+enum class RevocationReason;
}
namespace Kleo
{
class RevokeKeyDialog : public QDialog
{
Q_OBJECT
public:
explicit RevokeKeyDialog(QWidget *parent = nullptr, Qt::WindowFlags f = {});
~RevokeKeyDialog() override;
void setKey(const GpgME::Key &key);
+ GpgME::RevocationReason reason() const;
+ QString description() const;
+
private:
class Private;
const std::unique_ptr<Private> d;
};
} // namespace Kleo

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 18, 11:12 PM (11 h, 39 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
6e/2e/68f8bf5d48f632318f9b7e424ebb

Event Timeline