diff --git a/src/crypto/gui/certificatelineedit.cpp b/src/crypto/gui/certificatelineedit.cpp index 45cc4b2f8..45b189ba9 100644 --- a/src/crypto/gui/certificatelineedit.cpp +++ b/src/crypto/gui/certificatelineedit.cpp @@ -1,620 +1,616 @@ /* crypto/gui/certificatelineedit.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "certificatelineedit.h" #include "commands/detailscommand.h" #include "dialogs/groupdetailsdialog.h" #include "view/errorlabel.h" #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) Q_DECLARE_METATYPE(KeyGroup) static QStringList s_lookedUpKeys; namespace { class CompletionProxyModel : public KeyListSortFilterProxyModel { Q_OBJECT public: CompletionProxyModel(QObject *parent = nullptr) : KeyListSortFilterProxyModel(parent) { } int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent) // pretend that there is only one column to workaround a bug in // QAccessibleTable which provides the accessibility interface for the // completion pop-up return 1; } QVariant data(const QModelIndex &idx, int role) const override { if (!idx.isValid()) { return QVariant(); } switch (role) { case Qt::DecorationRole: { const auto key = KeyListSortFilterProxyModel::data(idx, KeyList::KeyRole).value(); if (!key.isNull()) { return Kleo::Formatting::iconForUid(key.userID(0)); } const auto group = KeyListSortFilterProxyModel::data(idx, KeyList::GroupRole).value(); if (!group.isNull()) { return QIcon::fromTheme(QStringLiteral("group")); } Q_ASSERT(!key.isNull() || !group.isNull()); return QVariant(); } default: return KeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role); } } }; auto createSeparatorAction(QObject *parent) { auto action = new QAction{parent}; action->setSeparator(true); return action; } } // namespace class CertificateLineEdit::Private { CertificateLineEdit *q; public: enum class Status { Empty, //< text is empty Success, //< a certificate or group is set None, //< entered text does not match any certificates or groups Ambiguous, //< entered text matches multiple certificates or groups }; explicit Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter); QString text() const; void setKey(const GpgME::Key &key); void setGroup(const KeyGroup &group); void setKeyFilter(const std::shared_ptr &filter); void setAccessibleName(const QString &s); private: void updateKey(); void editChanged(); void editFinished(); void checkLocate(); void openDetailsDialog(); void setTextWithBlockedSignals(const QString &s); void showContextMenu(const QPoint &pos); QString errorMessage() const; void updateErrorLabel(); void updateAccessibleNameAndDescription(); public: Status mStatus = Status::Empty; GpgME::Key mKey; KeyGroup mGroup; struct Ui { explicit Ui(QWidget *parent) : lineEdit{parent} , button{parent} , errorLabel{parent} {} QLineEdit lineEdit; QToolButton button; ErrorLabel errorLabel; } ui; private: QString mAccessibleName; KeyListSortFilterProxyModel *const mFilterModel; KeyListSortFilterProxyModel *const mCompleterFilterModel; QCompleter *mCompleter = nullptr; std::shared_ptr mFilter; bool mEditingInProgress = false; QAction *const mStatusAction; QAction *const mShowDetailsAction; }; CertificateLineEdit::Private::Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter) : q{qq} , ui{qq} , mFilterModel{new KeyListSortFilterProxyModel{qq}} , mCompleterFilterModel{new CompletionProxyModel{qq}} , mCompleter{new QCompleter{qq}} , mFilter{std::shared_ptr{filter}} , mStatusAction{new QAction{qq}} , mShowDetailsAction{new QAction{qq}} { ui.lineEdit.setPlaceholderText(i18n("Please enter a name or email address...")); ui.lineEdit.setClearButtonEnabled(true); ui.lineEdit.setContextMenuPolicy(Qt::CustomContextMenu); ui.lineEdit.addAction(mStatusAction, QLineEdit::LeadingPosition); mCompleterFilterModel->setKeyFilter(mFilter); mCompleterFilterModel->setSourceModel(model); mCompleter->setModel(mCompleterFilterModel); mCompleter->setFilterMode(Qt::MatchContains); mCompleter->setCaseSensitivity(Qt::CaseInsensitive); ui.lineEdit.setCompleter(mCompleter); ui.button.setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new"))); ui.button.setToolTip(i18n("Show certificate list")); ui.button.setAccessibleName(i18n("Show certificate list")); ui.errorLabel.setVisible(false); auto vbox = new QVBoxLayout{q}; vbox->setContentsMargins(0, 0, 0, 0); auto l = new QHBoxLayout; l->setContentsMargins(0, 0, 0, 0); l->addWidget(&ui.lineEdit); l->addWidget(&ui.button); vbox->addLayout(l); vbox->addWidget(&ui.errorLabel); q->setFocusPolicy(ui.lineEdit.focusPolicy()); q->setFocusProxy(&ui.lineEdit); mShowDetailsAction->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); mShowDetailsAction->setText(i18nc("@action:inmenu", "Show Details")); mShowDetailsAction->setEnabled(false); mFilterModel->setSourceModel(model); mFilterModel->setFilterKeyColumn(KeyList::Summary); if (filter) { mFilterModel->setKeyFilter(mFilter); } connect(KeyCache::instance().get(), &Kleo::KeyCache::keyListingDone, q, [this]() { updateKey(); }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupUpdated, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { setTextWithBlockedSignals(Formatting::summaryLine(group)); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(); }, Qt::QueuedConnection); } }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupRemoved, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { mGroup = KeyGroup(); QSignalBlocker blocky{&ui.lineEdit}; ui.lineEdit.clear(); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(); }, Qt::QueuedConnection); } }); connect(&ui.lineEdit, &QLineEdit::editingFinished, q, [this]() { // queue the call of editFinished() to ensure that QCompleter::activated is handled first QMetaObject::invokeMethod(q, [this]() { editFinished(); }, Qt::QueuedConnection); }); connect(&ui.lineEdit, &QLineEdit::textChanged, q, [this]() { editChanged(); }); connect(&ui.lineEdit, &QLineEdit::customContextMenuRequested, q, [this](const QPoint &pos) { showContextMenu(pos); }); connect(mStatusAction, &QAction::triggered, q, [this]() { openDetailsDialog(); }); connect(mShowDetailsAction, &QAction::triggered, q, [this]() { openDetailsDialog(); }); connect(&ui.button, &QToolButton::clicked, q, &CertificateLineEdit::certificateSelectionRequested); connect(mCompleter, qOverload(&QCompleter::activated), q, [this] (const QModelIndex &index) { Key key = mCompleter->completionModel()->data(index, KeyList::KeyRole).value(); auto group = mCompleter->completionModel()->data(index, KeyList::GroupRole).value(); if (!key.isNull()) { q->setKey(key); } else if (!group.isNull()) { q->setGroup(group); } else { qCDebug(KLEOPATRA_LOG) << "Activated item is neither key nor group"; } }); updateKey(); } void CertificateLineEdit::Private::openDetailsDialog() { if (!q->key().isNull()) { auto cmd = new Commands::DetailsCommand{q->key(), nullptr}; cmd->setParentWidget(q); cmd->start(); } else if (!q->group().isNull()) { auto dlg = new Dialogs::GroupDetailsDialog{q}; dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setGroup(q->group()); dlg->show(); } } void CertificateLineEdit::Private::setTextWithBlockedSignals(const QString &s) { QSignalBlocker blocky{&ui.lineEdit}; ui.lineEdit.setText(s); } void CertificateLineEdit::Private::showContextMenu(const QPoint &pos) { if (QMenu *menu = ui.lineEdit.createStandardContextMenu()) { auto *const firstStandardAction = menu->actions().value(0); menu->insertActions(firstStandardAction, {mShowDetailsAction, createSeparatorAction(menu)}); menu->setAttribute(Qt::WA_DeleteOnClose); menu->popup(ui.lineEdit.mapToGlobal(pos)); } } CertificateLineEdit::CertificateLineEdit(AbstractKeyListModel *model, KeyFilter *filter, QWidget *parent) : QWidget{parent} , d{new Private{this, model, filter}} { /* Take ownership of the model to prevent double deletion when the * filter models are deleted */ model->setParent(parent ? parent : this); } CertificateLineEdit::~CertificateLineEdit() = default; void CertificateLineEdit::Private::editChanged() { const bool editingStarted = !mEditingInProgress; mEditingInProgress = true; updateKey(); if (editingStarted) { Q_EMIT q->editingStarted(); } } void CertificateLineEdit::Private::editFinished() { mEditingInProgress = false; updateKey(); if (!q->key().isNull()) { setTextWithBlockedSignals(Formatting::summaryLine(q->key())); } else if (!q->group().isNull()) { setTextWithBlockedSignals(Formatting::summaryLine(q->group())); } else if (mStatus == Status::None) { checkLocate(); } } void CertificateLineEdit::Private::checkLocate() { if (mStatus != Status::None) { // try to locate key only if text matches no local certificates or groups return; } // Only check once per mailbox const auto mailText = ui.lineEdit.text().trimmed(); if (mailText.isEmpty() || s_lookedUpKeys.contains(mailText)) { return; } s_lookedUpKeys << mailText; qCDebug(KLEOPATRA_LOG) << "Lookup job for" << mailText; auto job = QGpgME::openpgp()->locateKeysJob(); job->start({mailText}, /*secretOnly=*/false); } void CertificateLineEdit::Private::updateKey() { static const _detail::ByFingerprint keysHaveSameFingerprint; const auto mailText = ui.lineEdit.text().trimmed(); auto newKey = Key(); auto newGroup = KeyGroup(); if (mailText.isEmpty()) { mStatus = Status::Empty; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-unavailable"))); mStatusAction->setToolTip({}); } else { mFilterModel->setFilterFixedString(mailText); if (mFilterModel->rowCount() > 1) { // keep current key or group if they still match if (!mKey.isNull()) { for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); Key key = mFilterModel->key(index); if (!key.isNull() && keysHaveSameFingerprint(key, mKey)) { newKey = mKey; break; } } } else if (!mGroup.isNull()) { newGroup = mGroup; for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); KeyGroup group = mFilterModel->group(index); if (!group.isNull() && group.source() == mGroup.source() && group.id() == mGroup.id()) { newGroup = mGroup; break; } } } if (newKey.isNull() && newGroup.isNull()) { mStatus = Status::Ambiguous; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-question"))); mStatusAction->setToolTip(i18n("Multiple matching certificates or groups found")); } } else if (mFilterModel->rowCount() == 1) { const auto index = mFilterModel->index(0, 0); newKey = mFilterModel->data(index, KeyList::KeyRole).value(); newGroup = mFilterModel->data(index, KeyList::GroupRole).value(); Q_ASSERT(!newKey.isNull() || !newGroup.isNull()); if (newKey.isNull() && newGroup.isNull()) { mStatus = Status::None; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error"))); mStatusAction->setToolTip(i18n("No matching certificates or groups found")); } } else { mStatus = Status::None; mStatusAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error"))); mStatusAction->setToolTip(i18n("No matching certificates or groups found")); } } mKey = newKey; mGroup = newGroup; if (!mKey.isNull()) { /* FIXME: This needs to be solved by a multiple UID supporting model */ mStatus = Status::Success; mStatusAction->setIcon(Formatting::iconForUid(mKey.userID(0))); mStatusAction->setToolTip(Formatting::validity(mKey.userID(0))); ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions)); } else if (!mGroup.isNull()) { mStatus = Status::Success; mStatusAction->setIcon(Formatting::validityIcon(mGroup)); mStatusAction->setToolTip(Formatting::validity(mGroup)); ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions)); } else { ui.lineEdit.setToolTip({}); } mShowDetailsAction->setEnabled(mStatus == Status::Success); updateErrorLabel(); Q_EMIT q->keyChanged(); - - if (mailText.isEmpty()) { - Q_EMIT q->wantsRemoval(q); - } } QString CertificateLineEdit::Private::errorMessage() const { switch (mStatus) { case Status::Empty: case Status::Success: return {}; case Status::None: return i18n("Error: No matching certificates or groups found"); case Status::Ambiguous: return i18n("Error: Multiple matching certificates or groups found"); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; } void CertificateLineEdit::Private::updateErrorLabel() { const auto currentErrorMessage = ui.errorLabel.text(); const auto newErrorMessage = errorMessage(); if (newErrorMessage == currentErrorMessage) { return; } if (currentErrorMessage.isEmpty() && mEditingInProgress) { // 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.errorLabel.setVisible(!newErrorMessage.isEmpty()); ui.errorLabel.setText(newErrorMessage); updateAccessibleNameAndDescription(); } void CertificateLineEdit::Private::setAccessibleName(const QString &s) { mAccessibleName = s; updateAccessibleNameAndDescription(); } 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"); } } void CertificateLineEdit::Private::updateAccessibleNameAndDescription() { // fall back to default accessible name if accessible name wasn't set explicitly if (mAccessibleName.isEmpty()) { mAccessibleName = getAccessibleName(&ui.lineEdit); } const bool errorShown = ui.errorLabel.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.errorLabel.text() : QString{}; if (ui.lineEdit.accessibleDescription() != description) { ui.lineEdit.setAccessibleDescription(description); } // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute); // screen readers say something like "invalid data" if this state is set; // emulate this by adding "invalid data" to the accessible name of the input field const auto name = errorShown ? mAccessibleName + QLatin1String{", "} + invalidEntryText() : mAccessibleName; if (ui.lineEdit.accessibleName() != name) { ui.lineEdit.setAccessibleName(name); } } Key CertificateLineEdit::key() const { if (isEnabled()) { return d->mKey; } else { return Key(); } } KeyGroup CertificateLineEdit::group() const { if (isEnabled()) { return d->mGroup; } else { return KeyGroup(); } } QString CertificateLineEdit::Private::text() const { return ui.lineEdit.text().trimmed(); } QString CertificateLineEdit::text() const { return d->text(); } void CertificateLineEdit::Private::setKey(const Key &key) { mKey = key; mGroup = KeyGroup(); qCDebug(KLEOPATRA_LOG) << "Setting Key. " << Formatting::summaryLine(key); setTextWithBlockedSignals(Formatting::summaryLine(key)); updateKey(); } void CertificateLineEdit::setKey(const Key &key) { d->setKey(key); } void CertificateLineEdit::Private::setGroup(const KeyGroup &group) { mGroup = group; mKey = Key(); const QString summary = Formatting::summaryLine(group); qCDebug(KLEOPATRA_LOG) << "Setting KeyGroup. " << summary; setTextWithBlockedSignals(summary); updateKey(); } void CertificateLineEdit::setGroup(const KeyGroup &group) { d->setGroup(group); } bool CertificateLineEdit::isEmpty() const { return d->mStatus == Private::Status::Empty; } bool CertificateLineEdit::hasAcceptableInput() const { return d->mStatus == Private::Status::Empty || d->mStatus == Private::Status::Success; } void CertificateLineEdit::Private::setKeyFilter(const std::shared_ptr &filter) { mFilter = filter; mFilterModel->setKeyFilter(filter); mCompleterFilterModel->setKeyFilter(mFilter); updateKey(); } void CertificateLineEdit::setKeyFilter(const std::shared_ptr &filter) { d->setKeyFilter(filter); } void CertificateLineEdit::setAccessibleNameOfLineEdit(const QString &name) { d->setAccessibleName(name); } #include "certificatelineedit.moc" diff --git a/src/crypto/gui/certificatelineedit.h b/src/crypto/gui/certificatelineedit.h index 4d12c5b76..00d3664bb 100644 --- a/src/crypto/gui/certificatelineedit.h +++ b/src/crypto/gui/certificatelineedit.h @@ -1,97 +1,94 @@ /* crypto/gui/certificatelineedit.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include namespace GpgME { class Key; } namespace Kleo { class AbstractKeyListModel; class KeyFilter; class KeyGroup; /** Line edit and completion based Certificate Selection Widget. * * Shows the status of the selection with a status label and icon. * * The widget will use a single line HBox Layout. For larger dialog * see certificateslectiondialog. */ class CertificateLineEdit: public QWidget { Q_OBJECT public: /** Create the certificate selection line. * * If parent is not NULL the model is not taken * over but the parent argument used as the parent of the model. * * @param model: The keylistmodel to use. * @param parent: The usual widget parent. * @param filter: The filters to use. See certificateselectiondialog. */ explicit CertificateLineEdit(AbstractKeyListModel *model, KeyFilter *filter = nullptr, QWidget *parent = nullptr); ~CertificateLineEdit() override; /** Get the selected key */ GpgME::Key key() const; KeyGroup group() const; /** The current text */ QString text() const; /** Check if the text is empty */ bool isEmpty() const; /** Returns true if the field is empty or if a key or group is selected. */ bool hasAcceptableInput() const; /** Set the preselected Key for this widget. */ void setKey(const GpgME::Key &key); /** Set the preselected group for this widget. */ void setGroup(const KeyGroup &group); /** Set the used keyfilter. */ void setKeyFilter(const std::shared_ptr &filter); void setAccessibleNameOfLineEdit(const QString &name); Q_SIGNALS: /** Emitted when the selected key changed. */ void keyChanged(); - /** Emitted when the entry is empty and editing is finished. */ - void wantsRemoval(CertificateLineEdit *w); - /** Emitted when the entry is no longer empty. */ void editingStarted(); /** Emitted when the certificate selection dialog is requested. */ void certificateSelectionRequested(); private: class Private; std::unique_ptr const d; }; } diff --git a/src/crypto/gui/signencryptwidget.cpp b/src/crypto/gui/signencryptwidget.cpp index cb7feae33..09115e2a5 100644 --- a/src/crypto/gui/signencryptwidget.cpp +++ b/src/crypto/gui/signencryptwidget.cpp @@ -1,684 +1,682 @@ /* crypto/gui/signencryptwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "signencryptwidget.h" #include "kleopatra_debug.h" #include "certificatelineedit.h" #include "fileoperationspreferences.h" #include "kleopatraapplication.h" #include "settings.h" #include "unknownrecipientwidget.h" #include "dialogs/certificateselectiondialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Dialogs; using namespace GpgME; namespace { class SignCertificateFilter: public DefaultKeyFilter { public: SignCertificateFilter(GpgME::Protocol proto) : DefaultKeyFilter() { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); setCanSign(DefaultKeyFilter::Set); if (proto == GpgME::OpenPGP) { setIsOpenPGP(DefaultKeyFilter::Set); } else if (proto == GpgME::CMS) { setIsOpenPGP(DefaultKeyFilter::NotSet); } } }; class EncryptCertificateFilter: public DefaultKeyFilter { public: EncryptCertificateFilter(GpgME::Protocol proto): DefaultKeyFilter() { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanEncrypt(DefaultKeyFilter::Set); if (proto == GpgME::OpenPGP) { setIsOpenPGP(DefaultKeyFilter::Set); } else if (proto == GpgME::CMS) { setIsOpenPGP(DefaultKeyFilter::NotSet); } } }; class EncryptSelfCertificateFilter: public EncryptCertificateFilter { public: EncryptSelfCertificateFilter(GpgME::Protocol proto): EncryptCertificateFilter(proto) { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanEncrypt(DefaultKeyFilter::Set); setHasSecret(DefaultKeyFilter::Set); } }; } SignEncryptWidget::SignEncryptWidget(QWidget *parent, bool sigEncExclusive) : QWidget(parent), mModel(AbstractKeyListModel::createFlatKeyListModel(this)), mIsExclusive(sigEncExclusive) { auto lay = new QVBoxLayout(this); lay->setContentsMargins(0, 0, 0, 0); mModel->useKeyCache(true, KeyList::IncludeGroups); const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty(); const bool havePublicKeys = !KeyCache::instance()->keys().empty(); const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly(); /* The signature selection */ auto sigLay = new QHBoxLayout; auto sigGrp = new QGroupBox(i18nc("@title:group", "Prove authenticity (sign)")); mSigChk = new QCheckBox(i18n("Sign as:")); mSigChk->setEnabled(haveSecretKeys); mSigChk->setChecked(haveSecretKeys); mSigSelect = new KeySelectionCombo(); mSigSelect->setEnabled(mSigChk->isChecked()); sigLay->addWidget(mSigChk); sigLay->addWidget(mSigSelect, 1); sigGrp->setLayout(sigLay); lay->addWidget(sigGrp); connect(mSigChk, &QCheckBox::toggled, mSigSelect, &QWidget::setEnabled); connect(mSigChk, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp); connect(mSigSelect, &KeySelectionCombo::currentKeyChanged, this, &SignEncryptWidget::updateOp); // Recipient selection auto encBoxLay = new QVBoxLayout; auto encBox = new QGroupBox(i18nc("@title:group", "Encrypt")); encBox->setLayout(encBoxLay); auto recipientGrid = new QGridLayout; // Own key mEncSelfChk = new QCheckBox(i18n("Encrypt for me:")); mEncSelfChk->setEnabled(haveSecretKeys && !symmetricOnly); mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly); mSelfSelect = new KeySelectionCombo(); mSelfSelect->setEnabled(mEncSelfChk->isChecked()); recipientGrid->addWidget(mEncSelfChk, 0, 0); recipientGrid->addWidget(mSelfSelect, 0, 1); // Checkbox for other keys mEncOtherChk = new QCheckBox(i18n("Encrypt for others:")); mEncOtherChk->setEnabled(havePublicKeys && !symmetricOnly); mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly); recipientGrid->addWidget(mEncOtherChk, 1, 0, Qt::AlignTop); connect(mEncOtherChk, &QCheckBox::toggled, this, [this](bool toggled) { for (CertificateLineEdit *edit : std::as_const(mRecpWidgets)) { edit->setEnabled(toggled); } updateOp(); }); mRecpLayout = new QVBoxLayout; recipientGrid->addLayout(mRecpLayout, 1, 1); recipientGrid->setRowStretch(2, 1); // Scroll area for other keys auto recipientWidget = new QWidget; auto recipientScroll = new QScrollArea; recipientWidget->setLayout(recipientGrid); recipientScroll->setWidget(recipientWidget); recipientScroll->setWidgetResizable(true); recipientScroll->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow); recipientScroll->setFrameStyle(QFrame::NoFrame); recipientScroll->setFocusPolicy(Qt::NoFocus); recipientGrid->setContentsMargins(0, 0, 0, 0); encBoxLay->addWidget(recipientScroll, 1); auto bar = recipientScroll->verticalScrollBar(); connect (bar, &QScrollBar::rangeChanged, this, [bar] (int, int max) { bar->setValue(max); }); addRecipientWidget(); // Checkbox for password mSymmetric = new QCheckBox(i18n("Encrypt with password. Anyone you share the password with can read the data.")); mSymmetric->setToolTip(i18nc("Tooltip information for symmetric encryption", "Additionally to the keys of the recipients you can encrypt your data with a password. " "Anyone who has the password can read the data without any secret key. " "Using a password is less secure then public key cryptography. Even if you pick a very strong password.")); mSymmetric->setChecked(symmetricOnly || !havePublicKeys); encBoxLay->addWidget(mSymmetric); // Connect it connect(mEncSelfChk, &QCheckBox::toggled, mSelfSelect, &QWidget::setEnabled); connect(mEncSelfChk, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp); connect(mSymmetric, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp); connect(mSelfSelect, &KeySelectionCombo::currentKeyChanged, this, &SignEncryptWidget::updateOp); if (mIsExclusive) { connect(mEncOtherChk, &QCheckBox::toggled, this, [this](bool value) { if (mCurrentProto != GpgME::CMS) { return; } if (value) { mSigChk->setChecked(false); } }); connect(mEncSelfChk, &QCheckBox::toggled, this, [this](bool value) { if (mCurrentProto != GpgME::CMS) { return; } if (value) { mSigChk->setChecked(false); } }); connect(mSigChk, &QCheckBox::toggled, this, [this](bool value) { if (mCurrentProto != GpgME::CMS) { return; } if (value) { mEncSelfChk->setChecked(false); mEncOtherChk->setChecked(false); } }); } // Ensure that the mSigChk is aligned togehter with the encryption check boxes. mSigChk->setMinimumWidth(qMax(mEncOtherChk->width(), mEncSelfChk->width())); lay->addWidget(encBox); connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, &SignEncryptWidget::updateCheckBoxes); connect(KleopatraApplication::instance(), &KleopatraApplication::configurationChanged, this, &SignEncryptWidget::updateCheckBoxes); loadKeys(); onProtocolChanged(); updateOp(); } void SignEncryptWidget::setSignAsText(const QString &text) { mSigChk->setText(text); } void SignEncryptWidget::setEncryptForMeText(const QString &text) { mEncSelfChk->setText(text); } void SignEncryptWidget::setEncryptForOthersText(const QString &text) { mEncOtherChk->setText(text); } void SignEncryptWidget::setEncryptWithPasswordText(const QString& text) { mSymmetric->setText(text); } CertificateLineEdit *SignEncryptWidget::addRecipientWidget() { auto certSel = new CertificateLineEdit(mModel, new EncryptCertificateFilter(mCurrentProto), this); certSel->setAccessibleNameOfLineEdit(i18nc("text for screen readers", "recipient key")); certSel->setEnabled(mEncOtherChk->isChecked()); mRecpWidgets << certSel; if (mRecpLayout->count() > 0) { auto lastWidget = mRecpLayout->itemAt(mRecpLayout->count() - 1)->widget(); setTabOrder(lastWidget, certSel); } mRecpLayout->addWidget(certSel); connect(certSel, &CertificateLineEdit::keyChanged, this, &SignEncryptWidget::recipientsChanged); - connect(certSel, &CertificateLineEdit::wantsRemoval, - this, &SignEncryptWidget::recpRemovalRequested); connect(certSel, &CertificateLineEdit::editingStarted, this, &SignEncryptWidget::recipientsChanged); connect(certSel, &CertificateLineEdit::certificateSelectionRequested, this, [this, certSel]() { certificateSelectionRequested(certSel); }); return certSel; } void SignEncryptWidget::addRecipient(const Key &key) { CertificateLineEdit *certSel = addRecipientWidget(); if (!key.isNull()) { certSel->setKey(key); mAddedKeys << key; } } void SignEncryptWidget::addRecipient(const KeyGroup &group) { CertificateLineEdit *certSel = addRecipientWidget(); if (!group.isNull()) { certSel->setGroup(group); mAddedGroups << group; } } void SignEncryptWidget::certificateSelectionRequested(CertificateLineEdit *certificateLineEdit) { CertificateSelectionDialog dlg{this}; dlg.setOptions(CertificateSelectionDialog::Options( CertificateSelectionDialog::MultiSelection | CertificateSelectionDialog::EncryptOnly | CertificateSelectionDialog::optionsFromProtocol(mCurrentProto) | CertificateSelectionDialog::IncludeGroups)); if (!certificateLineEdit->key().isNull()) { const auto key = certificateLineEdit->key(); const auto name = QString::fromUtf8(key.userID(0).name()); const auto email = QString::fromUtf8(key.userID(0).email()); dlg.setStringFilter(!name.isEmpty() ? name : email); } else if (!certificateLineEdit->group().isNull()) { dlg.setStringFilter(certificateLineEdit->group().name()); } else { dlg.setStringFilter(certificateLineEdit->text()); } if (dlg.exec()) { const std::vector keys = dlg.selectedCertificates(); const std::vector groups = dlg.selectedGroups(); if (keys.size() == 0 && groups.size() == 0) { return; } bool isFirstItem = true; for (const Key &key : keys) { if (isFirstItem) { certificateLineEdit->setKey(key); isFirstItem = false; } else { addRecipient(key); } } for (const KeyGroup &group : groups) { if (isFirstItem) { certificateLineEdit->setGroup(group); isFirstItem = false; } else { addRecipient(group); } } } recipientsChanged(); } void SignEncryptWidget::clearAddedRecipients() { for (auto w: std::as_const(mUnknownWidgets)) { mRecpLayout->removeWidget(w); delete w; } for (auto &key: std::as_const(mAddedKeys)) { removeRecipient(key); } for (auto &group: std::as_const(mAddedGroups)) { removeRecipient(group); } } void SignEncryptWidget::addUnknownRecipient(const char *keyID) { auto unknownWidget = new UnknownRecipientWidget(keyID); mUnknownWidgets << unknownWidget; if (mRecpLayout->count() > 0) { auto lastWidget = mRecpLayout->itemAt(mRecpLayout->count() - 1)->widget(); setTabOrder(lastWidget, unknownWidget); } mRecpLayout->addWidget(unknownWidget); connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, [this] () { // Check if any unknown recipient can now be found. for (auto w: mUnknownWidgets) { auto key = KeyCache::instance()->findByKeyIDOrFingerprint(w->keyID().toLatin1().constData()); if (key.isNull()) { std::vector subids; subids.push_back(std::string(w->keyID().toLatin1().constData())); for (const auto &subkey: KeyCache::instance()->findSubkeysByKeyID(subids)) { key = subkey.parent(); } } if (key.isNull()) { continue; } // Key is now available replace by line edit. qCDebug(KLEOPATRA_LOG) << "Removing widget for keyid: " << w->keyID(); mRecpLayout->removeWidget(w); mUnknownWidgets.removeAll(w); delete w; addRecipient(key); } }); } void SignEncryptWidget::recipientsChanged() { const bool hasEmptyRecpWidget = std::any_of(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), [](auto w) { return w->isEmpty(); }); if (!hasEmptyRecpWidget) { addRecipientWidget(); } updateOp(); } Key SignEncryptWidget::signKey() const { if (mSigSelect->isEnabled()) { return mSigSelect->currentKey(); } return Key(); } Key SignEncryptWidget::selfKey() const { if (mSelfSelect->isEnabled()) { return mSelfSelect->currentKey(); } return Key(); } std::vector SignEncryptWidget::recipients() const { std::vector ret; for (const CertificateLineEdit *w : std::as_const(mRecpWidgets)) { if (!w->isEnabled()) { // If one is disabled, all are disabled. break; } const Key k = w->key(); const KeyGroup g = w->group(); if (!k.isNull()) { ret.push_back(k); } else if (!g.isNull()) { const auto keys = g.keys(); std::copy(keys.begin(), keys.end(), std::back_inserter(ret)); } } const Key k = selfKey(); if (!k.isNull()) { ret.push_back(k); } return ret; } bool SignEncryptWidget::isDeVsAndValid() const { if (!signKey().isNull() && (!IS_DE_VS(signKey()) || keyValidity(signKey()) < GpgME::UserID::Validity::Full)) { return false; } if (!selfKey().isNull() && (!IS_DE_VS(selfKey()) || keyValidity(selfKey()) < GpgME::UserID::Validity::Full)) { return false; } for (const auto &key: recipients()) { if (!IS_DE_VS(key) || keyValidity(key) < GpgME::UserID::Validity::Full) { return false; } } return true; } void SignEncryptWidget::updateOp() { const Key sigKey = signKey(); const std::vector recp = recipients(); QString newOp; if (!sigKey.isNull() && (!recp.empty() || encryptSymmetric())) { newOp = i18nc("@action", "Sign / Encrypt"); } else if (!recp.empty() || encryptSymmetric()) { newOp = i18nc("@action", "Encrypt"); } else if (!sigKey.isNull()) { newOp = i18nc("@action", "Sign"); } else { newOp = QString(); } mOp = newOp; Q_EMIT operationChanged(mOp); Q_EMIT keysChanged(); } QString SignEncryptWidget::currentOp() const { return mOp; } namespace { bool recipientWidgetHasFocus(CertificateLineEdit *w) { // check if w (or its focus proxy) or a child widget of w has focus return w->hasFocus() || w->isAncestorOf(qApp->focusWidget()); } } void SignEncryptWidget::recpRemovalRequested(CertificateLineEdit *w) { if (!w) { return; } const int emptyEdits = std::count_if(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), [](auto w) { return w->isEmpty(); }); if (emptyEdits > 1) { if (recipientWidgetHasFocus(w)) { const int index = mRecpLayout->indexOf(w); const auto focusWidget = (index < mRecpLayout->count() - 1) ? mRecpLayout->itemAt(index + 1)->widget() : mRecpLayout->itemAt(mRecpLayout->count() - 2)->widget(); focusWidget->setFocus(); } mRecpLayout->removeWidget(w); mRecpWidgets.removeAll(w); w->deleteLater(); } } void SignEncryptWidget::removeRecipient(const GpgME::Key &key) { for (CertificateLineEdit *edit: std::as_const(mRecpWidgets)) { const auto editKey = edit->key(); if (key.isNull() && editKey.isNull()) { recpRemovalRequested(edit); return; } if (editKey.primaryFingerprint() && key.primaryFingerprint() && !strcmp(editKey.primaryFingerprint(), key.primaryFingerprint())) { recpRemovalRequested(edit); return; } } } void SignEncryptWidget::removeRecipient(const KeyGroup &group) { for (CertificateLineEdit *edit: std::as_const(mRecpWidgets)) { const auto editGroup = edit->group(); if (group.isNull() && editGroup.isNull()) { recpRemovalRequested(edit); return; } if (editGroup.name() == group.name()) { recpRemovalRequested(edit); return; } } } bool SignEncryptWidget::encryptSymmetric() const { return mSymmetric->isChecked(); } void SignEncryptWidget::loadKeys() { KConfigGroup keys(KSharedConfig::openConfig(), "SignEncryptKeys"); auto cache = KeyCache::instance(); mSigSelect->setDefaultKey(keys.readEntry("SigningKey", QString())); mSelfSelect->setDefaultKey(keys.readEntry("EncryptKey", QString())); } void SignEncryptWidget::saveOwnKeys() const { KConfigGroup keys(KSharedConfig::openConfig(), "SignEncryptKeys"); auto sigKey = mSigSelect->currentKey(); auto encKey = mSelfSelect->currentKey(); if (!sigKey.isNull()) { keys.writeEntry("SigningKey", sigKey.primaryFingerprint()); } if (!encKey.isNull()) { keys.writeEntry("EncryptKey", encKey.primaryFingerprint()); } } void SignEncryptWidget::setSigningChecked(bool value) { mSigChk->setChecked(value && !KeyCache::instance()->secretKeys().empty()); } void SignEncryptWidget::setEncryptionChecked(bool checked) { if (checked) { const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty(); const bool havePublicKeys = !KeyCache::instance()->keys().empty(); const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly(); mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly); mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly); mSymmetric->setChecked(symmetricOnly || !havePublicKeys); } else { mEncSelfChk->setChecked(false); mEncOtherChk->setChecked(false); mSymmetric->setChecked(false); } } void SignEncryptWidget::setProtocol(GpgME::Protocol proto) { if (mCurrentProto == proto) { return; } mCurrentProto = proto; onProtocolChanged(); } void Kleo::SignEncryptWidget::onProtocolChanged() { mSigSelect->setKeyFilter(std::shared_ptr(new SignCertificateFilter(mCurrentProto))); mSelfSelect->setKeyFilter(std::shared_ptr(new EncryptSelfCertificateFilter(mCurrentProto))); const auto encFilter = std::shared_ptr(new EncryptCertificateFilter(mCurrentProto)); for (CertificateLineEdit *edit : std::as_const(mRecpWidgets)) { edit->setKeyFilter(encFilter); } if (mIsExclusive) { mSymmetric->setDisabled(mCurrentProto == GpgME::CMS); if (mSymmetric->isChecked() && mCurrentProto == GpgME::CMS) { mSymmetric->setChecked(false); } if (mSigChk->isChecked() && mCurrentProto == GpgME::CMS && (mEncSelfChk->isChecked() || mEncOtherChk->isChecked())) { mSigChk->setChecked(false); } } } bool SignEncryptWidget::isComplete() const { return !currentOp().isEmpty() && std::all_of(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), [](auto w) { return !w->isEnabled() || w->hasAcceptableInput(); }); } bool SignEncryptWidget::validate() { CertificateLineEdit *firstUnresolvedRecipient = nullptr; QStringList unresolvedRecipients; for (const auto edit: std::as_const(mRecpWidgets)) { if (edit->isEnabled() && !edit->hasAcceptableInput()) { if (!firstUnresolvedRecipient) { firstUnresolvedRecipient = edit; } unresolvedRecipients.push_back(edit->text().toHtmlEscaped()); } } if (!unresolvedRecipients.isEmpty()) { KMessageBox::errorList(this, i18n("Could not find a key for the following recipients:"), unresolvedRecipients, i18n("Failed to find some keys")); } if (firstUnresolvedRecipient) { firstUnresolvedRecipient->setFocus(); } return unresolvedRecipients.isEmpty(); } void SignEncryptWidget::updateCheckBoxes() { const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty(); const bool havePublicKeys = !KeyCache::instance()->keys().empty(); const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly(); mSigChk->setEnabled(haveSecretKeys); mEncSelfChk->setEnabled(haveSecretKeys && !symmetricOnly); mEncOtherChk->setEnabled(havePublicKeys && !symmetricOnly); if (symmetricOnly) { mEncSelfChk->setChecked(false); mEncOtherChk->setChecked(false); mSymmetric->setChecked(true); } }