diff --git a/src/crypto/gui/certificatelineedit.cpp b/src/crypto/gui/certificatelineedit.cpp index 18d49632a..05963eccd 100644 --- a/src/crypto/gui/certificatelineedit.cpp +++ b/src/crypto/gui/certificatelineedit.cpp @@ -1,721 +1,724 @@ /* 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 "utils/accessibility.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 #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 }; enum class CursorPositioning { MoveToEnd, KeepPosition, MoveToStart, Default = MoveToEnd, }; 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(CursorPositioning positioning); void editChanged(); void editFinished(); void checkLocate(); void onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector &keys); void openDetailsDialog(); void setTextWithBlockedSignals(const QString &s, CursorPositioning positioning); void showContextMenu(const QPoint &pos); QString errorMessage() const; QIcon statusIcon() const; QString statusToolTip() const; void updateStatusAction(); void updateErrorLabel(); void updateAccessibleNameAndDescription(); public: Status mStatus = Status::Empty; bool mEditingInProgress = false; 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; QAction *const mStatusAction; QAction *const mShowDetailsAction; QPointer mLocateJob; }; 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::keysMayHaveChanged, q, [this]() { updateKey(CursorPositioning::KeepPosition); }); 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), CursorPositioning::KeepPosition); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(CursorPositioning::KeepPosition); }, 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(CursorPositioning::KeepPosition); }, 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(CursorPositioning::Default); } void CertificateLineEdit::Private::openDetailsDialog() { if (!q->key().isNull()) { auto cmd = new Commands::DetailsCommand{q->key()}; 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, CursorPositioning positioning) { QSignalBlocker blocky{&ui.lineEdit}; const auto cursorPos = ui.lineEdit.cursorPosition(); ui.lineEdit.setText(s); switch(positioning) { case CursorPositioning::KeepPosition: ui.lineEdit.setCursorPosition(cursorPos); break; case CursorPositioning::MoveToStart: ui.lineEdit.setCursorPosition(0); break; case CursorPositioning::MoveToEnd: default: ; // setText() already moved the cursor to the end of the line }; } 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(CursorPositioning::Default); if (editingStarted) { Q_EMIT q->editingStarted(); } + if (q->isEmpty()) { + Q_EMIT q->cleared(); + } } void CertificateLineEdit::Private::editFinished() { // perform a first update with the "editing in progress" flag still set updateKey(CursorPositioning::MoveToStart); mEditingInProgress = false; checkLocate(); // perform another update with the "editing in progress" flag cleared // after a key locate may have been started; this makes sure that displaying // an error is delayed until the key locate job has finished updateKey(CursorPositioning::MoveToStart); } 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; if (mLocateJob) { mLocateJob->slotCancel(); mLocateJob.clear(); } auto job = QGpgME::openpgp()->locateKeysJob(); connect(job, &QGpgME::KeyListJob::result, q, [this, job, mailText](const KeyListResult &result, const std::vector &keys) { onLocateJobResult(job, mailText, result, keys); }); if (auto err = job->start({mailText}, /*secretOnly=*/false)) { qCDebug(KLEOPATRA_LOG) << __func__ << "Error: Starting" << job << "for" << mailText << "failed with" << Formatting::errorAsString(err); } else { mLocateJob = job; qCDebug(KLEOPATRA_LOG) << __func__ << "Started" << job << "for" << mailText; } } void CertificateLineEdit::Private::onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector &keys) { if (mLocateJob != job) { qCDebug(KLEOPATRA_LOG) << __func__ << "Ignoring outdated finished" << job << "for" << email; return; } qCDebug(KLEOPATRA_LOG) << __func__ << job << "for" << email << "finished with" << Formatting::errorAsString(result.error()) << "and keys" << keys; mLocateJob.clear(); if (!keys.empty() && !keys.front().isNull()) { KeyCache::mutableInstance()->insert(keys.front()); // inserting the key implicitly triggers an update } else { // explicitly trigger an update to display "no key" error updateKey(CursorPositioning::MoveToStart); } } void CertificateLineEdit::Private::updateKey(CursorPositioning positioning) { static const _detail::ByFingerprint keysHaveSameFingerprint; const auto mailText = ui.lineEdit.text().trimmed(); auto newKey = Key(); auto newGroup = KeyGroup(); if (mailText.isEmpty()) { mStatus = Status::Empty; } else { mFilterModel->setFilterRegularExpression(QRegularExpression::escape(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; } } 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; } } else { mStatus = Status::None; } } mKey = newKey; mGroup = newGroup; if (!mKey.isNull()) { /* FIXME: This needs to be solved by a multiple UID supporting model */ mStatus = Status::Success; ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions)); if (!mEditingInProgress) { setTextWithBlockedSignals(Formatting::summaryLine(mKey), positioning); } } else if (!mGroup.isNull()) { mStatus = Status::Success; ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions)); if (!mEditingInProgress) { setTextWithBlockedSignals(Formatting::summaryLine(mGroup), positioning); } } else { ui.lineEdit.setToolTip({}); } mShowDetailsAction->setEnabled(mStatus == Status::Success); updateStatusAction(); updateErrorLabel(); Q_EMIT q->keyChanged(); } QString CertificateLineEdit::Private::errorMessage() const { switch (mStatus) { case Status::Empty: case Status::Success: return {}; case Status::None: return i18n("No matching certificates or groups found"); case Status::Ambiguous: return i18n("Multiple matching certificates or groups found"); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } QIcon CertificateLineEdit::Private::statusIcon() const { switch (mStatus) { case Status::Empty: return QIcon::fromTheme(QStringLiteral("emblem-unavailable")); case Status::Success: if (!mKey.isNull()) { return Formatting::iconForUid(mKey.userID(0)); } else if (!mGroup.isNull()) { return Formatting::validityIcon(mGroup); } else { qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; return {}; } case Status::None: case Status::Ambiguous: if (mEditingInProgress || mLocateJob) { return QIcon::fromTheme(QStringLiteral("emblem-question")); } else { return QIcon::fromTheme(QStringLiteral("emblem-error")); } default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } QString CertificateLineEdit::Private::statusToolTip() const { switch (mStatus) { case Status::Empty: return {}; case Status::Success: if (!mKey.isNull()) { return Formatting::validity(mKey.userID(0)); } else if (!mGroup.isNull()) { return Formatting::validity(mGroup); } else { qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; return {}; } case Status::None: case Status::Ambiguous: return errorMessage(); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } void CertificateLineEdit::Private::updateStatusAction() { mStatusAction->setIcon(statusIcon()); mStatusAction->setToolTip(statusToolTip()); } namespace { QString decoratedError(const QString &text) { return text.isEmpty() ? QString() : i18nc("@info", "Error: %1", text); } } void CertificateLineEdit::Private::updateErrorLabel() { const auto currentErrorMessage = ui.errorLabel.text(); const auto newErrorMessage = decoratedError(errorMessage()); if (newErrorMessage == currentErrorMessage) { return; } if (currentErrorMessage.isEmpty() && (mEditingInProgress || mLocateJob)) { // 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(); } 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); // position cursor, so that that the start of the summary is visible setTextWithBlockedSignals(Formatting::summaryLine(key), CursorPositioning::MoveToStart); updateKey(CursorPositioning::MoveToStart); } 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; // position cursor, so that that the start of the summary is visible setTextWithBlockedSignals(summary, CursorPositioning::MoveToStart); updateKey(CursorPositioning::MoveToStart); } void CertificateLineEdit::setGroup(const KeyGroup &group) { d->setGroup(group); } bool CertificateLineEdit::isEmpty() const { return d->mStatus == Private::Status::Empty; } bool CertificateLineEdit::isEditingInProgress() const { return d->mEditingInProgress; } 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(CursorPositioning::Default); } 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 fc222d058..0b010c196 100644 --- a/src/crypto/gui/certificatelineedit.h +++ b/src/crypto/gui/certificatelineedit.h @@ -1,97 +1,100 @@ /* 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 user is editing the input. */ bool isEditingInProgress() 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 no longer empty. */ void editingStarted(); + /** Emitted when the input is cleared (i.e. becomes empty). */ + void cleared(); + /** 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 f59b94f7c..91562a11d 100644 --- a/src/crypto/gui/signencryptwidget.cpp +++ b/src/crypto/gui/signencryptwidget.cpp @@ -1,854 +1,856 @@ /* 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 "utils/gui-helper.h" #include #include #include #include #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); setValidIfSMIME(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); setValidIfSMIME(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); setValidIfSMIME(DefaultKeyFilter::Set); } }; } class SignEncryptWidget::Private { SignEncryptWidget *const q; public: struct RecipientWidgets { CertificateLineEdit *edit; KMessageWidget *expiryMessage; }; explicit Private(SignEncryptWidget *qq, bool sigEncExclusive) : q{qq} , mModel{AbstractKeyListModel::createFlatKeyListModel(qq)} , mIsExclusive{sigEncExclusive} { } CertificateLineEdit* addRecipientWidget(); /* Inserts a new recipient widget after widget @p after or at the end * if @p after is null. */ CertificateLineEdit* insertRecipientWidget(CertificateLineEdit *after); void recpRemovalRequested(const RecipientWidgets &recipient); void onProtocolChanged(); void updateCheckBoxes(); ExpiryChecker *expiryChecker(); void updateExpiryMessages(KMessageWidget *w, const GpgME::Key &key, ExpiryChecker::CheckFlags flags); void updateAllExpiryMessages(); public: KeySelectionCombo *mSigSelect = nullptr; KMessageWidget *mSignKeyExpiryMessage = nullptr; KeySelectionCombo *mSelfSelect = nullptr; KMessageWidget *mEncryptToSelfKeyExpiryMessage = nullptr; std::vector mRecpWidgets; QVector mUnknownWidgets; QVector mAddedKeys; QVector mAddedGroups; QVBoxLayout *mRecpLayout = nullptr; Operations mOp; AbstractKeyListModel *mModel = nullptr; QCheckBox *mSymmetric = nullptr; QCheckBox *mSigChk = nullptr; QCheckBox *mEncOtherChk = nullptr; QCheckBox *mEncSelfChk = nullptr; GpgME::Protocol mCurrentProto = GpgME::UnknownProtocol; const bool mIsExclusive; std::unique_ptr mExpiryChecker; }; SignEncryptWidget::SignEncryptWidget(QWidget *parent, bool sigEncExclusive) : QWidget{parent} , d{new Private{this, sigEncExclusive}} { auto lay = new QVBoxLayout(this); lay->setContentsMargins(0, 0, 0, 0); d->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 sigGrp = new QGroupBox{i18nc("@title:group", "Prove authenticity (sign)"), this}; d->mSigChk = new QCheckBox{i18n("Sign as:"), this}; d->mSigChk->setEnabled(haveSecretKeys); d->mSigChk->setChecked(haveSecretKeys); d->mSigSelect = new KeySelectionCombo{this}; d->mSigSelect->setEnabled(d->mSigChk->isChecked()); d->mSignKeyExpiryMessage = new KMessageWidget{this}; d->mSignKeyExpiryMessage->setVisible(false); auto groupLayout = new QGridLayout{sigGrp}; groupLayout->setColumnStretch(1, 1); groupLayout->addWidget(d->mSigChk, 0, 0); groupLayout->addWidget(d->mSigSelect, 0, 1); groupLayout->addWidget(d->mSignKeyExpiryMessage, 1, 1); lay->addWidget(sigGrp); connect(d->mSigChk, &QCheckBox::toggled, this, [this](bool checked) { d->mSigSelect->setEnabled(checked); updateOp(); d->updateExpiryMessages(d->mSignKeyExpiryMessage, signKey(), ExpiryChecker::OwnSigningKey); }); connect(d->mSigSelect, &KeySelectionCombo::currentKeyChanged, this, [this]() { updateOp(); d->updateExpiryMessages(d->mSignKeyExpiryMessage, signKey(), ExpiryChecker::OwnSigningKey); }); } // Recipient selection { auto encBox = new QGroupBox{i18nc("@title:group", "Encrypt"), this}; auto encBoxLay = new QVBoxLayout{encBox}; auto recipientGrid = new QGridLayout; int row = 0; // Own key d->mEncSelfChk = new QCheckBox{i18n("Encrypt for me:"), this}; d->mEncSelfChk->setEnabled(haveSecretKeys && !symmetricOnly); d->mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly); d->mSelfSelect = new KeySelectionCombo{this}; d->mSelfSelect->setEnabled(d->mEncSelfChk->isChecked()); d->mEncryptToSelfKeyExpiryMessage = new KMessageWidget{this}; d->mEncryptToSelfKeyExpiryMessage->setVisible(false); recipientGrid->addWidget(d->mEncSelfChk, row, 0); recipientGrid->addWidget(d->mSelfSelect, row, 1); row++; recipientGrid->addWidget(d->mEncryptToSelfKeyExpiryMessage, row, 1); // Checkbox for other keys row++; d->mEncOtherChk = new QCheckBox{i18n("Encrypt for others:"), this}; d->mEncOtherChk->setEnabled(havePublicKeys && !symmetricOnly); d->mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly); recipientGrid->addWidget(d->mEncOtherChk, row, 0, Qt::AlignTop); connect(d->mEncOtherChk, &QCheckBox::toggled, this, [this](bool checked) { for (const auto &recipient : std::as_const(d->mRecpWidgets)) { recipient.edit->setEnabled(checked); d->updateExpiryMessages(recipient.expiryMessage, checked ? recipient.edit->key() : Key{}, ExpiryChecker::EncryptionKey); } updateOp(); }); d->mRecpLayout = new QVBoxLayout; recipientGrid->addLayout(d->mRecpLayout, row, 1); recipientGrid->setRowStretch(row + 1, 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); }); d->addRecipientWidget(); // Checkbox for password d->mSymmetric = new QCheckBox(i18n("Encrypt with password. Anyone you share the password with can read the data.")); d->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.")); d->mSymmetric->setChecked(symmetricOnly || !havePublicKeys); encBoxLay->addWidget(d->mSymmetric); // Connect it connect(d->mEncSelfChk, &QCheckBox::toggled, this, [this](bool checked) { d->mSelfSelect->setEnabled(checked); updateOp(); d->updateExpiryMessages(d->mEncryptToSelfKeyExpiryMessage, selfKey(), ExpiryChecker::OwnEncryptionKey); }); connect(d->mSelfSelect, &KeySelectionCombo::currentKeyChanged, this, [this]() { updateOp(); d->updateExpiryMessages(d->mEncryptToSelfKeyExpiryMessage, selfKey(), ExpiryChecker::OwnEncryptionKey); }); connect(d->mSymmetric, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp); if (d->mIsExclusive) { connect(d->mEncOtherChk, &QCheckBox::toggled, this, [this](bool value) { if (d->mCurrentProto != GpgME::CMS) { return; } if (value) { d->mSigChk->setChecked(false); } }); connect(d->mEncSelfChk, &QCheckBox::toggled, this, [this](bool value) { if (d->mCurrentProto != GpgME::CMS) { return; } if (value) { d->mSigChk->setChecked(false); } }); connect(d->mSigChk, &QCheckBox::toggled, this, [this](bool value) { if (d->mCurrentProto != GpgME::CMS) { return; } if (value) { d->mEncSelfChk->setChecked(false); d->mEncOtherChk->setChecked(false); } }); } // Ensure that the d->mSigChk is aligned together with the encryption check boxes. d->mSigChk->setMinimumWidth(qMax(d->mEncOtherChk->width(), d->mEncSelfChk->width())); lay->addWidget(encBox); } connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, [this]() { d->updateCheckBoxes(); d->updateAllExpiryMessages(); }); connect(KleopatraApplication::instance(), &KleopatraApplication::configurationChanged, this, [this]() { d->updateCheckBoxes(); d->mExpiryChecker.reset(); d->updateAllExpiryMessages(); }); loadKeys(); d->onProtocolChanged(); updateOp(); } SignEncryptWidget::~SignEncryptWidget() = default; void SignEncryptWidget::setSignAsText(const QString &text) { d->mSigChk->setText(text); } void SignEncryptWidget::setEncryptForMeText(const QString &text) { d->mEncSelfChk->setText(text); } void SignEncryptWidget::setEncryptForOthersText(const QString &text) { d->mEncOtherChk->setText(text); } void SignEncryptWidget::setEncryptWithPasswordText(const QString& text) { d->mSymmetric->setText(text); } CertificateLineEdit *SignEncryptWidget::Private::addRecipientWidget() { return insertRecipientWidget(nullptr); } CertificateLineEdit *SignEncryptWidget::Private::insertRecipientWidget(CertificateLineEdit *after) { Q_ASSERT(!after || mRecpLayout->indexOf(after) != -1); const auto index = after ? mRecpLayout->indexOf(after) + 2 : mRecpLayout->count(); const RecipientWidgets recipient{ new CertificateLineEdit{mModel, new EncryptCertificateFilter{mCurrentProto}, q}, new KMessageWidget{q} }; recipient.edit->setAccessibleNameOfLineEdit(i18nc("text for screen readers", "recipient key")); recipient.edit->setEnabled(mEncOtherChk->isChecked()); recipient.expiryMessage->setVisible(false); if (static_cast(index / 2) < mRecpWidgets.size()) { mRecpWidgets.insert(mRecpWidgets.begin() + index / 2, recipient); } else { mRecpWidgets.push_back(recipient); } if (mRecpLayout->count() > 0) { auto prevWidget = after ? after : mRecpLayout->itemAt(mRecpLayout->count() - 1)->widget(); Kleo::forceSetTabOrder(prevWidget, recipient.edit); Kleo::forceSetTabOrder(recipient.edit, recipient.expiryMessage); } mRecpLayout->insertWidget(index, recipient.edit); mRecpLayout->insertWidget(index + 1, recipient.expiryMessage); connect(recipient.edit, &CertificateLineEdit::keyChanged, q, &SignEncryptWidget::recipientsChanged); connect(recipient.edit, &CertificateLineEdit::editingStarted, q, &SignEncryptWidget::recipientsChanged); + connect(recipient.edit, &CertificateLineEdit::cleared, + q, &SignEncryptWidget::recipientsChanged); connect(recipient.edit, &CertificateLineEdit::certificateSelectionRequested, q, [this, recipient]() { q->certificateSelectionRequested(recipient.edit); }); return recipient.edit; } void SignEncryptWidget::addRecipient(const Key &key) { CertificateLineEdit *certSel = d->addRecipientWidget(); if (!key.isNull()) { certSel->setKey(key); d->mAddedKeys << key; } } void SignEncryptWidget::addRecipient(const KeyGroup &group) { CertificateLineEdit *certSel = d->addRecipientWidget(); if (!group.isNull()) { certSel->setGroup(group); d->mAddedGroups << group; } } void SignEncryptWidget::certificateSelectionRequested(CertificateLineEdit *certificateLineEdit) { CertificateSelectionDialog dlg{this}; dlg.setOptions(CertificateSelectionDialog::Options( CertificateSelectionDialog::MultiSelection | CertificateSelectionDialog::EncryptOnly | CertificateSelectionDialog::optionsFromProtocol(d->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; } CertificateLineEdit *certWidget = nullptr; for (const Key &key : keys) { if (!certWidget) { certWidget = certificateLineEdit; } else { certWidget = d->insertRecipientWidget(certWidget); } certWidget->setKey(key); } for (const KeyGroup &group : groups) { if (!certWidget) { certWidget = certificateLineEdit; } else { certWidget = d->insertRecipientWidget(certWidget); } certWidget->setGroup(group); } } recipientsChanged(); } void SignEncryptWidget::clearAddedRecipients() { for (auto w: std::as_const(d->mUnknownWidgets)) { d->mRecpLayout->removeWidget(w); delete w; } for (auto &key: std::as_const(d->mAddedKeys)) { removeRecipient(key); } for (auto &group: std::as_const(d->mAddedGroups)) { removeRecipient(group); } } void SignEncryptWidget::addUnknownRecipient(const char *keyID) { auto unknownWidget = new UnknownRecipientWidget(keyID); d->mUnknownWidgets << unknownWidget; if (d->mRecpLayout->count() > 0) { auto lastWidget = d->mRecpLayout->itemAt(d->mRecpLayout->count() - 1)->widget(); setTabOrder(lastWidget, unknownWidget); } d->mRecpLayout->addWidget(unknownWidget); connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, [this] () { // Check if any unknown recipient can now be found. for (auto w: d->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(); d->mRecpLayout->removeWidget(w); d->mUnknownWidgets.removeAll(w); delete w; addRecipient(key); } }); } void SignEncryptWidget::recipientsChanged() { const bool hasEmptyRecpWidget = std::any_of(std::cbegin(d->mRecpWidgets), std::cend(d->mRecpWidgets), [](auto w) { return w.edit->isEmpty(); }); if (!hasEmptyRecpWidget) { d->addRecipientWidget(); } updateOp(); for (const auto &recipient : std::as_const(d->mRecpWidgets)) { - if (!recipient.edit->isEditingInProgress()) { + if (!recipient.edit->isEditingInProgress() || recipient.edit->isEmpty()) { d->updateExpiryMessages(recipient.expiryMessage, d->mEncOtherChk->isChecked() ? recipient.edit->key() : Key{}, ExpiryChecker::EncryptionKey); } } } Key SignEncryptWidget::signKey() const { if (d->mSigSelect->isEnabled()) { return d->mSigSelect->currentKey(); } return Key(); } Key SignEncryptWidget::selfKey() const { if (d->mSelfSelect->isEnabled()) { return d->mSelfSelect->currentKey(); } return Key(); } std::vector SignEncryptWidget::recipients() const { std::vector ret; for (const auto &recipient : std::as_const(d->mRecpWidgets)) { const auto *const w = recipient.edit; 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() && !DeVSCompliance::keyIsCompliant(signKey())) { return false; } if (!selfKey().isNull() && !DeVSCompliance::keyIsCompliant(selfKey())) { return false; } for (const auto &key: recipients()) { if (!DeVSCompliance::keyIsCompliant(key)) { return false; } } return true; } static QString expiryMessage(const ExpiryChecker::Result &result) { if (result.expiration.certificate.isNull()) { return {}; } switch (result.expiration.status) { case ExpiryChecker::Expired: return i18nc("@info", "This certificate is expired."); case ExpiryChecker::ExpiresSoon: { if (result.expiration.duration.count() == 0) { return i18nc("@info", "This certificate expires today."); } else { return i18ncp("@info", "This certificate expires tomorrow.", "This certificate expires in %1 days.", result.expiration.duration.count()); } } case ExpiryChecker::NoSuitableSubkey: if (result.checkFlags & ExpiryChecker::EncryptionKey) { return i18nc("@info", "This certificate cannot be used for encryption."); } else { return i18nc("@info", "This certificate cannot be used for signing."); } case ExpiryChecker::InvalidKey: case ExpiryChecker::InvalidCheckFlags: break; // wrong usage of ExpiryChecker; can be ignored case ExpiryChecker::NotNearExpiry: ; } return {}; } void SignEncryptWidget::updateOp() { const Key sigKey = signKey(); const std::vector recp = recipients(); Operations op = NoOperation; if (!sigKey.isNull()) { op |= Sign; } if (!recp.empty() || encryptSymmetric()) { op |= Encrypt; } d->mOp = op; Q_EMIT operationChanged(d->mOp); Q_EMIT keysChanged(); } SignEncryptWidget::Operations SignEncryptWidget::currentOp() const { return d->mOp; } namespace { bool recipientWidgetHasFocus(QWidget *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::Private::recpRemovalRequested(const RecipientWidgets &recipient) { if (!recipient.edit) { return; } const int emptyEdits = std::count_if(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), [](const auto &r) { return r.edit->isEmpty(); }); if (emptyEdits > 1) { if (recipientWidgetHasFocus(recipient.edit) || recipientWidgetHasFocus(recipient.expiryMessage)) { const int index = mRecpLayout->indexOf(recipient.edit); const auto focusWidget = (index < mRecpLayout->count() - 2) ? mRecpLayout->itemAt(index + 2)->widget() : mRecpLayout->itemAt(mRecpLayout->count() - 3)->widget(); focusWidget->setFocus(); } mRecpLayout->removeWidget(recipient.expiryMessage); mRecpLayout->removeWidget(recipient.edit); const auto it = std::find_if(std::begin(mRecpWidgets), std::end(mRecpWidgets), [recipient](const auto &r) { return r.edit == recipient.edit; }); mRecpWidgets.erase(it); recipient.expiryMessage->deleteLater(); recipient.edit->deleteLater(); } } void SignEncryptWidget::removeRecipient(const GpgME::Key &key) { for (const auto &recipient: std::as_const(d->mRecpWidgets)) { const auto editKey = recipient.edit->key(); if (key.isNull() && editKey.isNull()) { d->recpRemovalRequested(recipient); return; } if (editKey.primaryFingerprint() && key.primaryFingerprint() && !strcmp(editKey.primaryFingerprint(), key.primaryFingerprint())) { d->recpRemovalRequested(recipient); return; } } } void SignEncryptWidget::removeRecipient(const KeyGroup &group) { for (const auto &recipient: std::as_const(d->mRecpWidgets)) { const auto editGroup = recipient.edit->group(); if (group.isNull() && editGroup.isNull()) { d->recpRemovalRequested(recipient); return; } if (editGroup.name() == group.name()) { d->recpRemovalRequested(recipient); return; } } } bool SignEncryptWidget::encryptSymmetric() const { return d->mSymmetric->isChecked(); } void SignEncryptWidget::loadKeys() { KConfigGroup keys(KSharedConfig::openConfig(), "SignEncryptKeys"); auto cache = KeyCache::instance(); d->mSigSelect->setDefaultKey(keys.readEntry("SigningKey", QString())); d->mSelfSelect->setDefaultKey(keys.readEntry("EncryptKey", QString())); } void SignEncryptWidget::saveOwnKeys() const { KConfigGroup keys(KSharedConfig::openConfig(), "SignEncryptKeys"); auto sigKey = d->mSigSelect->currentKey(); auto encKey = d->mSelfSelect->currentKey(); if (!sigKey.isNull()) { keys.writeEntry("SigningKey", sigKey.primaryFingerprint()); } if (!encKey.isNull()) { keys.writeEntry("EncryptKey", encKey.primaryFingerprint()); } } void SignEncryptWidget::setSigningChecked(bool value) { d->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(); d->mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly); d->mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly); d->mSymmetric->setChecked(symmetricOnly || !havePublicKeys); } else { d->mEncSelfChk->setChecked(false); d->mEncOtherChk->setChecked(false); d->mSymmetric->setChecked(false); } } void SignEncryptWidget::setProtocol(GpgME::Protocol proto) { if (d->mCurrentProto == proto) { return; } d->mCurrentProto = proto; d->onProtocolChanged(); } void Kleo::SignEncryptWidget::Private::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 (const auto &recipient : std::as_const(mRecpWidgets)) { recipient.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() != NoOperation && std::all_of(std::cbegin(d->mRecpWidgets), std::cend(d->mRecpWidgets), [](const auto &r) { return !r.edit->isEnabled() || r.edit->hasAcceptableInput(); }); } bool SignEncryptWidget::validate() { CertificateLineEdit *firstUnresolvedRecipient = nullptr; QStringList unresolvedRecipients; for (const auto &recipient: std::as_const(d->mRecpWidgets)) { if (recipient.edit->isEnabled() && !recipient.edit->hasAcceptableInput()) { if (!firstUnresolvedRecipient) { firstUnresolvedRecipient = recipient.edit; } unresolvedRecipients.push_back(recipient.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::Private::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); } } ExpiryChecker *Kleo::SignEncryptWidget::Private::expiryChecker() { if (!mExpiryChecker) { mExpiryChecker.reset(new ExpiryChecker{ExpiryCheckerConfig{}.settings()}); } return mExpiryChecker.get(); } void SignEncryptWidget::Private::updateExpiryMessages(KMessageWidget *messageWidget, const GpgME::Key &key, ExpiryChecker::CheckFlags flags) { messageWidget->setCloseButtonVisible(false); if (!Settings{}.showExpiryNotifications() || key.isNull()) { messageWidget->setVisible(false); } else { const auto result = expiryChecker()->checkKey(key, flags); const auto message = expiryMessage(result); messageWidget->setText(message); messageWidget->setVisible(!message.isEmpty()); } } void SignEncryptWidget::Private::updateAllExpiryMessages() { updateExpiryMessages(mSignKeyExpiryMessage, q->signKey(), ExpiryChecker::OwnSigningKey); updateExpiryMessages(mEncryptToSelfKeyExpiryMessage, q->selfKey(), ExpiryChecker::OwnEncryptionKey); for (const auto &recipient : std::as_const(mRecpWidgets)) { if (recipient.edit->isEnabled()) { updateExpiryMessages(recipient.expiryMessage, recipient.edit->key(), ExpiryChecker::EncryptionKey); } } }