Commit 7aae07cb authored by Ingo Klöcker's avatar Ingo Klöcker
Browse files

Prevent empty lines in the revocation comment

The edit interface that is used for revoking own OpenPGP keys allows
entering a comment/description consisting of multiple lines of text.
The text ends with an empty line. Therefore, it is not possible to
add revocation comments that contain empty lines. Any whitespace at the
end of lines and any trailing whitespace at the end of the entered text
is stripped.

GnuPG-bug-id: 5859
parent 228cccad
Pipeline #159550 passed with stage
in 3 minutes and 31 seconds
......@@ -11,18 +11,23 @@
#include <config-kleopatra.h>
#include "revokekeydialog.h"
#include "view/errorlabel.h"
#include <Libkleo/Formatting>
#include <KConfigGroup>
#include <KGuiItem>
#include <KLocalizedString>
#include <KMessageBox>
#include <KSeparator>
#include <KSharedConfig>
#include <KStandardGuiItem>
#include <QAccessible>
#include <QApplication>
#include <QButtonGroup>
#include <QDialogButtonBox>
#include <QFocusEvent>
#include <QGroupBox>
#include <QLabel>
#include <QPushButton>
......@@ -41,6 +46,47 @@
using namespace Kleo;
using namespace GpgME;
namespace
{
QString getAccessibleName(QObject *object)
{
QString name;
if (const auto *const iface = QAccessible::queryAccessibleInterface(object)) {
name = iface->text(QAccessible::Name);
}
return name;
}
QString invalidEntryText()
{
return i18nc("text for screen readers to indicate that the associated object, "
"such as a form field, has an error",
"invalid entry");
}
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);
}
};
}
class RevokeKeyDialog::Private
{
friend class ::Kleo::RevokeKeyDialog;
......@@ -48,12 +94,16 @@ class RevokeKeyDialog::Private
struct {
QLabel *infoLabel = nullptr;
QTextEdit *description = 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)
......@@ -90,18 +140,26 @@ public:
#endif
{
auto label = new QLabel{i18nc("@label:textbox", "Description (optional):"), q};
ui.description = new QTextEdit{q};
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);
label->setBuddy(ui.description);
ui.descriptionLabel->setBuddy(ui.description);
ui.descriptionError = new ErrorLabel{q};
ui.descriptionError->setVisible(false);
mainLayout->addWidget(label);
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 Key"));
......@@ -109,7 +167,7 @@ public:
mainLayout->addWidget(ui.buttonBox);
connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept);
connect(ui.buttonBox, &QDialogButtonBox::accepted, q, [this]() { checkAccept(); });
connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject);
restoreGeometry();
......@@ -136,6 +194,92 @@ private:
q->resize(size);
}
}
void checkAccept()
{
if (!descriptionHasAcceptableInput()) {
KMessageBox::sorry(q, descriptionErrorMessage());
} else {
q->accept();
}
}
bool descriptionHasAcceptableInput() const
{
return !q->description().contains(QLatin1String{"\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 + QLatin1String{", "} + 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)
......@@ -163,6 +307,9 @@ GpgME::RevocationReason RevokeKeyDialog::reason() const
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(trailingWhitespace);
return d->ui.description->toPlainText().remove(whitespaceAtEndOfLine).remove(trailingWhitespace);
}
#include "revokekeydialog.moc"
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment