File indexing completed on 2024-06-23 05:13:49

0001 /*  crypto/gui/certificatelineedit.cpp
0002 
0003     This file is part of Kleopatra, the KDE keymanager
0004     SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik
0005     SPDX-FileContributor: Intevation GmbH
0006     SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH
0007     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "certificatelineedit.h"
0013 
0014 #include "commands/detailscommand.h"
0015 #include "dialogs/groupdetailsdialog.h"
0016 #include "utils/accessibility.h"
0017 #include "view/errorlabel.h"
0018 
0019 #include <QAccessible>
0020 #include <QAction>
0021 #include <QCompleter>
0022 #include <QPushButton>
0023 #include <QSignalBlocker>
0024 
0025 #include "kleopatra_debug.h"
0026 
0027 #include <Libkleo/Debug>
0028 #include <Libkleo/Formatting>
0029 #include <Libkleo/KeyCache>
0030 #include <Libkleo/KeyFilter>
0031 #include <Libkleo/KeyGroup>
0032 #include <Libkleo/KeyList>
0033 #include <Libkleo/KeyListModel>
0034 #include <Libkleo/KeyListSortFilterProxyModel>
0035 
0036 #include <KLocalizedString>
0037 
0038 #include <gpgme++/key.h>
0039 #include <gpgme++/keylistresult.h>
0040 
0041 #include <QGpgME/KeyListJob>
0042 #include <QGpgME/Protocol>
0043 
0044 #include <QHBoxLayout>
0045 #include <QLineEdit>
0046 #include <QMenu>
0047 #include <QToolButton>
0048 
0049 using namespace Kleo;
0050 using namespace GpgME;
0051 
0052 Q_DECLARE_METATYPE(GpgME::Key)
0053 Q_DECLARE_METATYPE(KeyGroup)
0054 
0055 static QStringList s_lookedUpKeys;
0056 
0057 namespace
0058 {
0059 class CompletionProxyModel : public KeyListSortFilterProxyModel
0060 {
0061     Q_OBJECT
0062 
0063 public:
0064     CompletionProxyModel(QObject *parent = nullptr)
0065         : KeyListSortFilterProxyModel(parent)
0066     {
0067     }
0068 
0069     int columnCount(const QModelIndex &parent = QModelIndex()) const override
0070     {
0071         Q_UNUSED(parent)
0072         // pretend that there is only one column to workaround a bug in
0073         // QAccessibleTable which provides the accessibility interface for the
0074         // completion pop-up
0075         return 1;
0076     }
0077 
0078     QVariant data(const QModelIndex &idx, int role) const override
0079     {
0080         if (!idx.isValid()) {
0081             return QVariant();
0082         }
0083 
0084         switch (role) {
0085         case Qt::DecorationRole: {
0086             const auto key = KeyListSortFilterProxyModel::data(idx, KeyList::KeyRole).value<GpgME::Key>();
0087             if (!key.isNull()) {
0088                 return Kleo::Formatting::iconForUid(key.userID(0));
0089             }
0090 
0091             const auto group = KeyListSortFilterProxyModel::data(idx, KeyList::GroupRole).value<KeyGroup>();
0092             if (!group.isNull()) {
0093                 return QIcon::fromTheme(QStringLiteral("group"));
0094             }
0095 
0096             Q_ASSERT(!key.isNull() || !group.isNull());
0097             return QVariant();
0098         }
0099         default:
0100             return KeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role);
0101         }
0102     }
0103 
0104 private:
0105     bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
0106     {
0107         const auto leftKey = sourceModel()->data(left, KeyList::KeyRole).value<GpgME::Key>();
0108         const auto leftGroup = leftKey.isNull() ? sourceModel()->data(left, KeyList::GroupRole).value<KeyGroup>() : KeyGroup{};
0109         const auto rightKey = sourceModel()->data(right, KeyList::KeyRole).value<GpgME::Key>();
0110         const auto rightGroup = rightKey.isNull() ? sourceModel()->data(right, KeyList::GroupRole).value<KeyGroup>() : KeyGroup{};
0111 
0112         // shouldn't happen, but still put null entries at the end
0113         if (leftKey.isNull() && leftGroup.isNull()) {
0114             return false;
0115         }
0116         if (rightKey.isNull() && rightGroup.isNull()) {
0117             return true;
0118         }
0119 
0120         // first sort by the displayed name and/or email address
0121         const auto leftNameAndOrEmail = leftGroup.isNull() ? Formatting::nameAndEmailForSummaryLine(leftKey) : leftGroup.name();
0122         const auto rightNameAndOrEmail = rightGroup.isNull() ? Formatting::nameAndEmailForSummaryLine(rightKey) : rightGroup.name();
0123         const int cmp = QString::localeAwareCompare(leftNameAndOrEmail, rightNameAndOrEmail);
0124         if (cmp) {
0125             return cmp < 0;
0126         }
0127         // then sort groups before certificates
0128         if (!leftGroup.isNull() && !rightKey.isNull()) {
0129             return true; // left is group, right is certificate
0130         }
0131         if (!leftKey.isNull() && !rightGroup.isNull()) {
0132             return false; // left is certificate, right is group
0133         }
0134 
0135         // if both are groups (with identical names) sort them by their ID
0136         if (!leftGroup.isNull() && !rightGroup.isNull()) {
0137             return leftGroup.id() < rightGroup.id();
0138         }
0139 
0140         // sort certificates with same name/email by validity and creation time
0141         const auto lUid = leftKey.userID(0);
0142         const auto rUid = rightKey.userID(0);
0143         if (lUid.validity() != rUid.validity()) {
0144             return lUid.validity() > rUid.validity();
0145         }
0146 
0147         /* Both have the same validity, check which one is newer. */
0148         time_t leftTime = 0;
0149         for (const GpgME::Subkey &s : leftKey.subkeys()) {
0150             if (s.isBad()) {
0151                 continue;
0152             }
0153             if (s.creationTime() > leftTime) {
0154                 leftTime = s.creationTime();
0155             }
0156         }
0157         time_t rightTime = 0;
0158         for (const GpgME::Subkey &s : rightKey.subkeys()) {
0159             if (s.isBad()) {
0160                 continue;
0161             }
0162             if (s.creationTime() > rightTime) {
0163                 rightTime = s.creationTime();
0164             }
0165         }
0166         if (rightTime != leftTime) {
0167             return leftTime > rightTime;
0168         }
0169 
0170         // as final resort we compare the fingerprints
0171         return strcmp(leftKey.primaryFingerprint(), rightKey.primaryFingerprint()) < 0;
0172     }
0173 };
0174 
0175 auto createSeparatorAction(QObject *parent)
0176 {
0177     auto action = new QAction{parent};
0178     action->setSeparator(true);
0179     return action;
0180 }
0181 } // namespace
0182 
0183 class CertificateLineEdit::Private
0184 {
0185     CertificateLineEdit *q;
0186 
0187 public:
0188     enum class Status {
0189         Empty, //< text is empty
0190         Success, //< a certificate or group is set
0191         None, //< entered text does not match any certificates or groups
0192         Ambiguous, //< entered text matches multiple certificates or groups
0193     };
0194     enum class CursorPositioning {
0195         MoveToEnd,
0196         KeepPosition,
0197         MoveToStart,
0198         Default = MoveToEnd,
0199     };
0200 
0201     explicit Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyUsage::Flags usage, KeyFilter *filter);
0202 
0203     QString text() const;
0204 
0205     void setKey(const GpgME::Key &key);
0206 
0207     void setGroup(const KeyGroup &group);
0208 
0209     void setKeyFilter(const std::shared_ptr<KeyFilter> &filter);
0210 
0211     void setAccessibleName(const QString &s);
0212 
0213 private:
0214     void updateKey(CursorPositioning positioning);
0215     void editChanged();
0216     void editFinished();
0217     void checkLocate();
0218     void onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector<GpgME::Key> &keys);
0219     void openDetailsDialog();
0220     void setTextWithBlockedSignals(const QString &s, CursorPositioning positioning);
0221     void showContextMenu(const QPoint &pos);
0222     QString errorMessage() const;
0223     QIcon statusIcon() const;
0224     QString statusToolTip() const;
0225     void updateStatusAction();
0226     void updateErrorLabel();
0227     void updateAccessibleNameAndDescription();
0228 
0229 public:
0230     Status mStatus = Status::Empty;
0231     bool mEditingInProgress = false;
0232     GpgME::Key mKey;
0233     KeyGroup mGroup;
0234 
0235     struct Ui {
0236         explicit Ui(QWidget *parent)
0237             : lineEdit{parent}
0238             , button{parent}
0239             , errorLabel{parent}
0240         {
0241         }
0242 
0243         QLineEdit lineEdit;
0244         QToolButton button;
0245         ErrorLabel errorLabel;
0246     } ui;
0247 
0248 private:
0249     QString mAccessibleName;
0250     KeyListSortFilterProxyModel *const mFilterModel;
0251     CompletionProxyModel *const mCompleterFilterModel;
0252     QCompleter *mCompleter = nullptr;
0253     std::shared_ptr<KeyFilter> mFilter;
0254     QAction *const mStatusAction;
0255     QAction *const mShowDetailsAction;
0256     QPointer<QGpgME::Job> mLocateJob;
0257     Formatting::IconProvider mIconProvider;
0258 };
0259 
0260 CertificateLineEdit::Private::Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyUsage::Flags usage, KeyFilter *filter)
0261     : q{qq}
0262     , ui{qq}
0263     , mFilterModel{new KeyListSortFilterProxyModel{qq}}
0264     , mCompleterFilterModel{new CompletionProxyModel{qq}}
0265     , mCompleter{new QCompleter{qq}}
0266     , mFilter{std::shared_ptr<KeyFilter>{filter}}
0267     , mStatusAction{new QAction{qq}}
0268     , mShowDetailsAction{new QAction{qq}}
0269     , mIconProvider{usage}
0270 {
0271     ui.lineEdit.setPlaceholderText(i18n("Please enter a name or email address..."));
0272     ui.lineEdit.setClearButtonEnabled(true);
0273     ui.lineEdit.setContextMenuPolicy(Qt::CustomContextMenu);
0274     ui.lineEdit.addAction(mStatusAction, QLineEdit::LeadingPosition);
0275 
0276     mCompleterFilterModel->setKeyFilter(mFilter);
0277     mCompleterFilterModel->setSourceModel(model);
0278     // initialize dynamic sorting
0279     mCompleterFilterModel->sort(0);
0280     mCompleter->setModel(mCompleterFilterModel);
0281     mCompleter->setFilterMode(Qt::MatchContains);
0282     mCompleter->setCaseSensitivity(Qt::CaseInsensitive);
0283     ui.lineEdit.setCompleter(mCompleter);
0284 
0285     ui.button.setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new")));
0286     ui.button.setToolTip(i18n("Show certificate list"));
0287     ui.button.setAccessibleName(i18n("Show certificate list"));
0288 
0289     ui.errorLabel.setVisible(false);
0290 
0291     auto vbox = new QVBoxLayout{q};
0292     vbox->setContentsMargins(0, 0, 0, 0);
0293 
0294     auto l = new QHBoxLayout;
0295     l->setContentsMargins(0, 0, 0, 0);
0296     l->addWidget(&ui.lineEdit);
0297     l->addWidget(&ui.button);
0298 
0299     vbox->addLayout(l);
0300     vbox->addWidget(&ui.errorLabel);
0301 
0302     q->setFocusPolicy(ui.lineEdit.focusPolicy());
0303     q->setFocusProxy(&ui.lineEdit);
0304 
0305     mShowDetailsAction->setIcon(QIcon::fromTheme(QStringLiteral("help-about")));
0306     mShowDetailsAction->setText(i18nc("@action:inmenu", "Show Details"));
0307     mShowDetailsAction->setEnabled(false);
0308 
0309     mFilterModel->setSourceModel(model);
0310     mFilterModel->setFilterKeyColumn(KeyList::Summary);
0311     if (filter) {
0312         mFilterModel->setKeyFilter(mFilter);
0313     }
0314 
0315     connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() {
0316         updateKey(CursorPositioning::KeepPosition);
0317     });
0318     connect(KeyCache::instance().get(), &Kleo::KeyCache::groupUpdated, q, [this](const KeyGroup &group) {
0319         if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) {
0320             setTextWithBlockedSignals(Formatting::summaryLine(group), CursorPositioning::KeepPosition);
0321             // queue the update to ensure that the model has been updated
0322             QMetaObject::invokeMethod(
0323                 q,
0324                 [this]() {
0325                     updateKey(CursorPositioning::KeepPosition);
0326                 },
0327                 Qt::QueuedConnection);
0328         }
0329     });
0330     connect(KeyCache::instance().get(), &Kleo::KeyCache::groupRemoved, q, [this](const KeyGroup &group) {
0331         if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) {
0332             mGroup = KeyGroup();
0333             QSignalBlocker blocky{&ui.lineEdit};
0334             ui.lineEdit.clear();
0335             // queue the update to ensure that the model has been updated
0336             QMetaObject::invokeMethod(
0337                 q,
0338                 [this]() {
0339                     updateKey(CursorPositioning::KeepPosition);
0340                 },
0341                 Qt::QueuedConnection);
0342         }
0343     });
0344     connect(&ui.lineEdit, &QLineEdit::editingFinished, q, [this]() {
0345         // queue the call of editFinished() to ensure that QCompleter::activated is handled first
0346         QMetaObject::invokeMethod(
0347             q,
0348             [this]() {
0349                 editFinished();
0350             },
0351             Qt::QueuedConnection);
0352     });
0353     connect(&ui.lineEdit, &QLineEdit::textChanged, q, [this]() {
0354         editChanged();
0355     });
0356     connect(&ui.lineEdit, &QLineEdit::customContextMenuRequested, q, [this](const QPoint &pos) {
0357         showContextMenu(pos);
0358     });
0359     connect(mStatusAction, &QAction::triggered, q, [this]() {
0360         openDetailsDialog();
0361     });
0362     connect(mShowDetailsAction, &QAction::triggered, q, [this]() {
0363         openDetailsDialog();
0364     });
0365     connect(&ui.button, &QToolButton::clicked, q, &CertificateLineEdit::certificateSelectionRequested);
0366     connect(mCompleter, qOverload<const QModelIndex &>(&QCompleter::activated), q, [this](const QModelIndex &index) {
0367         Key key = mCompleter->completionModel()->data(index, KeyList::KeyRole).value<Key>();
0368         auto group = mCompleter->completionModel()->data(index, KeyList::GroupRole).value<KeyGroup>();
0369         if (!key.isNull()) {
0370             q->setKey(key);
0371         } else if (!group.isNull()) {
0372             q->setGroup(group);
0373         } else {
0374             qCDebug(KLEOPATRA_LOG) << "Activated item is neither key nor group";
0375         }
0376         // queue the call of editFinished() to ensure that QLineEdit finished its own work
0377         QMetaObject::invokeMethod(
0378             q,
0379             [this]() {
0380                 editFinished();
0381             },
0382             Qt::QueuedConnection);
0383     });
0384     updateKey(CursorPositioning::Default);
0385 }
0386 
0387 void CertificateLineEdit::Private::openDetailsDialog()
0388 {
0389     if (!q->key().isNull()) {
0390         auto cmd = new Commands::DetailsCommand{q->key()};
0391         cmd->setParentWidget(q);
0392         cmd->start();
0393     } else if (!q->group().isNull()) {
0394         auto dlg = new Dialogs::GroupDetailsDialog{q};
0395         dlg->setAttribute(Qt::WA_DeleteOnClose);
0396         dlg->setGroup(q->group());
0397         dlg->show();
0398     }
0399 }
0400 
0401 void CertificateLineEdit::Private::setTextWithBlockedSignals(const QString &s, CursorPositioning positioning)
0402 {
0403     QSignalBlocker blocky{&ui.lineEdit};
0404     const auto cursorPos = ui.lineEdit.cursorPosition();
0405     ui.lineEdit.setText(s);
0406     switch (positioning) {
0407     case CursorPositioning::KeepPosition:
0408         ui.lineEdit.setCursorPosition(cursorPos);
0409         break;
0410     case CursorPositioning::MoveToStart:
0411         ui.lineEdit.setCursorPosition(0);
0412         break;
0413     case CursorPositioning::MoveToEnd:
0414     default:; // setText() already moved the cursor to the end of the line
0415     };
0416 }
0417 
0418 void CertificateLineEdit::Private::showContextMenu(const QPoint &pos)
0419 {
0420     if (QMenu *menu = ui.lineEdit.createStandardContextMenu()) {
0421         auto *const firstStandardAction = menu->actions().value(0);
0422         menu->insertActions(firstStandardAction, {mShowDetailsAction, createSeparatorAction(menu)});
0423         menu->setAttribute(Qt::WA_DeleteOnClose);
0424         menu->popup(ui.lineEdit.mapToGlobal(pos));
0425     }
0426 }
0427 
0428 CertificateLineEdit::CertificateLineEdit(AbstractKeyListModel *model, KeyUsage::Flags usage, KeyFilter *filter, QWidget *parent)
0429     : QWidget{parent}
0430     , d{new Private{this, model, usage, filter}}
0431 {
0432     /* Take ownership of the model to prevent double deletion when the
0433      * filter models are deleted */
0434     model->setParent(parent ? parent : this);
0435 }
0436 
0437 CertificateLineEdit::~CertificateLineEdit() = default;
0438 
0439 void CertificateLineEdit::Private::editChanged()
0440 {
0441     const bool editingStarted = !mEditingInProgress;
0442     mEditingInProgress = true;
0443     updateKey(CursorPositioning::Default);
0444     if (editingStarted) {
0445         Q_EMIT q->editingStarted();
0446     }
0447     if (q->isEmpty()) {
0448         Q_EMIT q->cleared();
0449     }
0450 }
0451 
0452 void CertificateLineEdit::Private::editFinished()
0453 {
0454     // perform a first update with the "editing in progress" flag still set
0455     updateKey(CursorPositioning::MoveToStart);
0456     mEditingInProgress = false;
0457     checkLocate();
0458     // perform another update with the "editing in progress" flag cleared
0459     // after a key locate may have been started; this makes sure that displaying
0460     // an error is delayed until the key locate job has finished
0461     updateKey(CursorPositioning::MoveToStart);
0462 }
0463 
0464 void CertificateLineEdit::Private::checkLocate()
0465 {
0466     if (mStatus != Status::None) {
0467         // try to locate key only if text matches no local certificates or groups
0468         return;
0469     }
0470 
0471     // Only check once per mailbox
0472     const auto mailText = ui.lineEdit.text().trimmed();
0473     if (mailText.isEmpty() || s_lookedUpKeys.contains(mailText)) {
0474         return;
0475     }
0476     s_lookedUpKeys << mailText;
0477     if (mLocateJob) {
0478         mLocateJob->slotCancel();
0479         mLocateJob.clear();
0480     }
0481     auto job = QGpgME::openpgp()->locateKeysJob();
0482     connect(job, &QGpgME::KeyListJob::result, q, [this, job, mailText](const KeyListResult &result, const std::vector<GpgME::Key> &keys) {
0483         onLocateJobResult(job, mailText, result, keys);
0484     });
0485     if (auto err = job->start({mailText}, /*secretOnly=*/false)) {
0486         qCDebug(KLEOPATRA_LOG) << __func__ << "Error: Starting" << job << "for" << mailText << "failed with" << Formatting::errorAsString(err);
0487     } else {
0488         mLocateJob = job;
0489         qCDebug(KLEOPATRA_LOG) << __func__ << "Started" << job << "for" << mailText;
0490     }
0491 }
0492 
0493 void CertificateLineEdit::Private::onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector<GpgME::Key> &keys)
0494 {
0495     if (mLocateJob != job) {
0496         qCDebug(KLEOPATRA_LOG) << __func__ << "Ignoring outdated finished" << job << "for" << email;
0497         return;
0498     }
0499     qCDebug(KLEOPATRA_LOG) << __func__ << job << "for" << email << "finished with" << Formatting::errorAsString(result.error()) << "and keys" << keys;
0500     mLocateJob.clear();
0501     if (!keys.empty() && !keys.front().isNull()) {
0502         KeyCache::mutableInstance()->insert(keys.front());
0503         // inserting the key implicitly triggers an update
0504     } else {
0505         // explicitly trigger an update to display "no key" error
0506         updateKey(CursorPositioning::MoveToStart);
0507     }
0508 }
0509 
0510 void CertificateLineEdit::Private::updateKey(CursorPositioning positioning)
0511 {
0512     static const _detail::ByFingerprint<std::equal_to> keysHaveSameFingerprint;
0513 
0514     const auto mailText = ui.lineEdit.text().trimmed();
0515     auto newKey = Key();
0516     auto newGroup = KeyGroup();
0517     if (mailText.isEmpty()) {
0518         mStatus = Status::Empty;
0519     } else {
0520         mFilterModel->setFilterRegularExpression(QRegularExpression::escape(mailText));
0521         if (mFilterModel->rowCount() > 1) {
0522             // keep current key or group if they still match
0523             if (!mKey.isNull()) {
0524                 for (int row = 0; row < mFilterModel->rowCount(); ++row) {
0525                     const QModelIndex index = mFilterModel->index(row, 0);
0526                     Key key = mFilterModel->key(index);
0527                     if (!key.isNull() && keysHaveSameFingerprint(key, mKey)) {
0528                         newKey = mKey;
0529                         break;
0530                     }
0531                 }
0532             } else if (!mGroup.isNull()) {
0533                 newGroup = mGroup;
0534                 for (int row = 0; row < mFilterModel->rowCount(); ++row) {
0535                     const QModelIndex index = mFilterModel->index(row, 0);
0536                     KeyGroup group = mFilterModel->group(index);
0537                     if (!group.isNull() && group.source() == mGroup.source() && group.id() == mGroup.id()) {
0538                         newGroup = mGroup;
0539                         break;
0540                     }
0541                 }
0542             }
0543             if (newKey.isNull() && newGroup.isNull()) {
0544                 mStatus = Status::Ambiguous;
0545             }
0546         } else if (mFilterModel->rowCount() == 1) {
0547             const auto index = mFilterModel->index(0, 0);
0548             newKey = mFilterModel->data(index, KeyList::KeyRole).value<Key>();
0549             newGroup = mFilterModel->data(index, KeyList::GroupRole).value<KeyGroup>();
0550             Q_ASSERT(!newKey.isNull() || !newGroup.isNull());
0551             if (newKey.isNull() && newGroup.isNull()) {
0552                 mStatus = Status::None;
0553             }
0554         } else {
0555             mStatus = Status::None;
0556         }
0557     }
0558     mKey = newKey;
0559     mGroup = newGroup;
0560 
0561     if (!mKey.isNull()) {
0562         /* FIXME: This needs to be solved by a multiple UID supporting model */
0563         mStatus = Status::Success;
0564         ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions));
0565         if (!mEditingInProgress) {
0566             setTextWithBlockedSignals(Formatting::summaryLine(mKey), positioning);
0567         }
0568     } else if (!mGroup.isNull()) {
0569         mStatus = Status::Success;
0570         ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions));
0571         if (!mEditingInProgress) {
0572             setTextWithBlockedSignals(Formatting::summaryLine(mGroup), positioning);
0573         }
0574     } else {
0575         ui.lineEdit.setToolTip({});
0576     }
0577 
0578     mShowDetailsAction->setEnabled(mStatus == Status::Success);
0579     updateStatusAction();
0580     updateErrorLabel();
0581 
0582     Q_EMIT q->keyChanged();
0583 }
0584 
0585 QString CertificateLineEdit::Private::errorMessage() const
0586 {
0587     switch (mStatus) {
0588     case Status::Empty:
0589     case Status::Success:
0590         return {};
0591     case Status::None:
0592         return i18n("No matching certificates or groups found");
0593     case Status::Ambiguous:
0594         return i18n("Multiple matching certificates or groups found");
0595     default:
0596         qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast<int>(mStatus);
0597         Q_ASSERT(!"Invalid status");
0598     };
0599     return {};
0600 }
0601 
0602 QIcon CertificateLineEdit::Private::statusIcon() const
0603 {
0604     switch (mStatus) {
0605     case Status::Empty:
0606         return QIcon::fromTheme(QStringLiteral("emblem-unavailable"));
0607     case Status::Success:
0608         if (!mKey.isNull()) {
0609             return mIconProvider.icon(mKey);
0610         } else if (!mGroup.isNull()) {
0611             return mIconProvider.icon(mGroup);
0612         } else {
0613             qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group.";
0614             return {};
0615         }
0616     case Status::None:
0617     case Status::Ambiguous:
0618         if (mEditingInProgress || mLocateJob) {
0619             return QIcon::fromTheme(QStringLiteral("emblem-question"));
0620         } else {
0621             return QIcon::fromTheme(QStringLiteral("emblem-error"));
0622         }
0623     default:
0624         qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast<int>(mStatus);
0625         Q_ASSERT(!"Invalid status");
0626     };
0627     return {};
0628 }
0629 
0630 QString CertificateLineEdit::Private::statusToolTip() const
0631 {
0632     switch (mStatus) {
0633     case Status::Empty:
0634         return {};
0635     case Status::Success:
0636         if (!mKey.isNull()) {
0637             return Formatting::validity(mKey.userID(0));
0638         } else if (!mGroup.isNull()) {
0639             return Formatting::validity(mGroup);
0640         } else {
0641             qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group.";
0642             return {};
0643         }
0644     case Status::None:
0645     case Status::Ambiguous:
0646         return errorMessage();
0647     default:
0648         qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast<int>(mStatus);
0649         Q_ASSERT(!"Invalid status");
0650     };
0651     return {};
0652 }
0653 
0654 void CertificateLineEdit::Private::updateStatusAction()
0655 {
0656     mStatusAction->setIcon(statusIcon());
0657     mStatusAction->setToolTip(statusToolTip());
0658 }
0659 
0660 namespace
0661 {
0662 QString decoratedError(const QString &text)
0663 {
0664     return text.isEmpty() ? QString() : i18nc("@info", "Error: %1", text);
0665 }
0666 }
0667 
0668 void CertificateLineEdit::Private::updateErrorLabel()
0669 {
0670     const auto currentErrorMessage = ui.errorLabel.text();
0671     const auto newErrorMessage = decoratedError(errorMessage());
0672     if (newErrorMessage == currentErrorMessage) {
0673         return;
0674     }
0675     if (currentErrorMessage.isEmpty() && (mEditingInProgress || mLocateJob)) {
0676         // delay showing the error message until editing is finished, so that we
0677         // do not annoy the user with an error message while they are still
0678         // entering the recipient;
0679         // on the other hand, we clear the error message immediately if it does
0680         // not apply anymore and we update the error message immediately if it
0681         // changed
0682         return;
0683     }
0684     ui.errorLabel.setVisible(!newErrorMessage.isEmpty());
0685     ui.errorLabel.setText(newErrorMessage);
0686     updateAccessibleNameAndDescription();
0687 }
0688 
0689 void CertificateLineEdit::Private::setAccessibleName(const QString &s)
0690 {
0691     mAccessibleName = s;
0692     updateAccessibleNameAndDescription();
0693 }
0694 
0695 void CertificateLineEdit::Private::updateAccessibleNameAndDescription()
0696 {
0697     // fall back to default accessible name if accessible name wasn't set explicitly
0698     if (mAccessibleName.isEmpty()) {
0699         mAccessibleName = getAccessibleName(&ui.lineEdit);
0700     }
0701     const bool errorShown = ui.errorLabel.isVisible();
0702 
0703     // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute);
0704     // emulate this by setting the error message as accessible description of the input field
0705     const auto description = errorShown ? ui.errorLabel.text() : QString{};
0706     if (ui.lineEdit.accessibleDescription() != description) {
0707         ui.lineEdit.setAccessibleDescription(description);
0708     }
0709 
0710     // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute);
0711     // screen readers say something like "invalid data" if this state is set;
0712     // emulate this by adding "invalid data" to the accessible name of the input field
0713     const auto name = errorShown ? mAccessibleName + QLatin1StringView{", "} + invalidEntryText() //
0714                                  : mAccessibleName;
0715     if (ui.lineEdit.accessibleName() != name) {
0716         ui.lineEdit.setAccessibleName(name);
0717     }
0718 }
0719 
0720 Key CertificateLineEdit::key() const
0721 {
0722     if (isEnabled()) {
0723         return d->mKey;
0724     } else {
0725         return Key();
0726     }
0727 }
0728 
0729 KeyGroup CertificateLineEdit::group() const
0730 {
0731     if (isEnabled()) {
0732         return d->mGroup;
0733     } else {
0734         return KeyGroup();
0735     }
0736 }
0737 
0738 QString CertificateLineEdit::Private::text() const
0739 {
0740     return ui.lineEdit.text().trimmed();
0741 }
0742 
0743 QString CertificateLineEdit::text() const
0744 {
0745     return d->text();
0746 }
0747 
0748 void CertificateLineEdit::Private::setKey(const Key &key)
0749 {
0750     mKey = key;
0751     mGroup = KeyGroup();
0752     qCDebug(KLEOPATRA_LOG) << "Setting Key. " << Formatting::summaryLine(key);
0753     // position cursor, so that that the start of the summary is visible
0754     setTextWithBlockedSignals(Formatting::summaryLine(key), CursorPositioning::MoveToStart);
0755     updateKey(CursorPositioning::MoveToStart);
0756 }
0757 
0758 void CertificateLineEdit::setKey(const Key &key)
0759 {
0760     d->setKey(key);
0761 }
0762 
0763 void CertificateLineEdit::Private::setGroup(const KeyGroup &group)
0764 {
0765     mGroup = group;
0766     mKey = Key();
0767     const QString summary = Formatting::summaryLine(group);
0768     qCDebug(KLEOPATRA_LOG) << "Setting KeyGroup. " << summary;
0769     // position cursor, so that that the start of the summary is visible
0770     setTextWithBlockedSignals(summary, CursorPositioning::MoveToStart);
0771     updateKey(CursorPositioning::MoveToStart);
0772 }
0773 
0774 void CertificateLineEdit::setGroup(const KeyGroup &group)
0775 {
0776     d->setGroup(group);
0777 }
0778 
0779 bool CertificateLineEdit::isEmpty() const
0780 {
0781     return d->mStatus == Private::Status::Empty;
0782 }
0783 
0784 bool CertificateLineEdit::isEditingInProgress() const
0785 {
0786     return d->mEditingInProgress;
0787 }
0788 
0789 bool CertificateLineEdit::hasAcceptableInput() const
0790 {
0791     return d->mStatus == Private::Status::Empty //
0792         || d->mStatus == Private::Status::Success;
0793 }
0794 
0795 void CertificateLineEdit::Private::setKeyFilter(const std::shared_ptr<KeyFilter> &filter)
0796 {
0797     mFilter = filter;
0798     mFilterModel->setKeyFilter(filter);
0799     mCompleterFilterModel->setKeyFilter(mFilter);
0800     updateKey(CursorPositioning::Default);
0801 }
0802 
0803 void CertificateLineEdit::setKeyFilter(const std::shared_ptr<KeyFilter> &filter)
0804 {
0805     d->setKeyFilter(filter);
0806 }
0807 
0808 void CertificateLineEdit::setAccessibleNameOfLineEdit(const QString &name)
0809 {
0810     d->setAccessibleName(name);
0811 }
0812 
0813 #include "certificatelineedit.moc"
0814 
0815 #include "moc_certificatelineedit.cpp"