File indexing completed on 2024-06-23 05:14:03

0001 /*  dialogs/certifywidget.cpp
0002 
0003     This file is part of Kleopatra, the KDE keymanager
0004     SPDX-FileCopyrightText: 2019, 2021 g10 Code GmbH
0005     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include <config-kleopatra.h>
0011 
0012 #include "certifywidget.h"
0013 
0014 #include "view/infofield.h"
0015 #include <utils/accessibility.h>
0016 #include <utils/expiration.h>
0017 #include <utils/gui-helper.h>
0018 
0019 #include <settings.h>
0020 
0021 #include "kleopatra_debug.h"
0022 
0023 #include <KConfigGroup>
0024 #include <KDateComboBox>
0025 #include <KLocalizedString>
0026 #include <KMessageBox>
0027 #include <KMessageWidget>
0028 #include <KSeparator>
0029 #include <KSharedConfig>
0030 
0031 #include <Libkleo/Algorithm>
0032 #include <Libkleo/DefaultKeyFilter>
0033 #include <Libkleo/Formatting>
0034 #include <Libkleo/KeyCache>
0035 #include <Libkleo/KeyHelpers>
0036 #include <Libkleo/KeySelectionCombo>
0037 #include <Libkleo/Predicates>
0038 #include <Libkleo/TreeWidget>
0039 
0040 #include <QGpgME/ChangeOwnerTrustJob>
0041 #include <QGpgME/Protocol>
0042 
0043 #include <QAction>
0044 #include <QCheckBox>
0045 #include <QHBoxLayout>
0046 #include <QIcon>
0047 #include <QLabel>
0048 #include <QLineEdit>
0049 #include <QParallelAnimationGroup>
0050 #include <QPropertyAnimation>
0051 #include <QPushButton>
0052 #include <QScrollArea>
0053 #include <QToolButton>
0054 #include <QVBoxLayout>
0055 
0056 #include <gpgme++/key.h>
0057 
0058 Q_DECLARE_METATYPE(GpgME::UserID)
0059 
0060 using namespace Kleo;
0061 using namespace GpgME;
0062 
0063 static QDebug operator<<(QDebug s, const GpgME::UserID &userID)
0064 {
0065     return s << Formatting::prettyUserID(userID);
0066 }
0067 
0068 namespace
0069 {
0070 
0071 // Maybe move this in its own file
0072 // based on code from StackOverflow
0073 class AnimatedExpander : public QWidget
0074 {
0075     Q_OBJECT
0076 public:
0077     explicit AnimatedExpander(const QString &title, const QString &accessibleTitle = {}, QWidget *parent = nullptr);
0078     void setContentLayout(QLayout *contentLayout);
0079     bool isExpanded() const;
0080     void setExpanded(bool expanded);
0081 
0082 private:
0083     static const int animationDuration = 300;
0084 
0085     QGridLayout mainLayout;
0086     QToolButton toggleButton;
0087     QFrame headerLine;
0088     QParallelAnimationGroup toggleAnimation;
0089     QWidget contentArea;
0090 };
0091 
0092 AnimatedExpander::AnimatedExpander(const QString &title, const QString &accessibleTitle, QWidget *parent)
0093     : QWidget{parent}
0094 {
0095 #ifdef Q_OS_WIN
0096     // draw dotted focus frame if button has focus; otherwise, draw invisible frame using background color
0097     toggleButton.setStyleSheet(
0098         QStringLiteral("QToolButton { border: 1px solid palette(window); }"
0099                        "QToolButton:focus { border: 1px dotted palette(window-text); }"));
0100 #else
0101     // this works with Breeze style because Breeze draws the focus frame when drawing CE_ToolButtonLabel
0102     // while the Windows styles (and Qt's common base style) draw the focus frame before drawing CE_ToolButtonLabel
0103     toggleButton.setStyleSheet(QStringLiteral("QToolButton { border: none; }"));
0104 #endif
0105     toggleButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0106     toggleButton.setArrowType(Qt::ArrowType::RightArrow);
0107     toggleButton.setText(title);
0108     if (!accessibleTitle.isEmpty()) {
0109         toggleButton.setAccessibleName(accessibleTitle);
0110     }
0111     toggleButton.setCheckable(true);
0112     toggleButton.setChecked(false);
0113 
0114     headerLine.setFrameShape(QFrame::HLine);
0115     headerLine.setFrameShadow(QFrame::Sunken);
0116     headerLine.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
0117 
0118     contentArea.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
0119 
0120     // start out collapsed
0121     contentArea.setMaximumHeight(0);
0122     contentArea.setMinimumHeight(0);
0123     contentArea.setVisible(false);
0124 
0125     // let the entire widget grow and shrink with its content
0126     toggleAnimation.addAnimation(new QPropertyAnimation(this, "minimumHeight"));
0127     toggleAnimation.addAnimation(new QPropertyAnimation(this, "maximumHeight"));
0128     toggleAnimation.addAnimation(new QPropertyAnimation(&contentArea, "maximumHeight"));
0129 
0130     mainLayout.setVerticalSpacing(0);
0131     mainLayout.setContentsMargins(0, 0, 0, 0);
0132     int row = 0;
0133     mainLayout.addWidget(&toggleButton, row, 0, 1, 1, Qt::AlignLeft);
0134     mainLayout.addWidget(&headerLine, row++, 2, 1, 1);
0135     mainLayout.addWidget(&contentArea, row, 0, 1, 3);
0136     setLayout(&mainLayout);
0137     connect(&toggleButton, &QToolButton::toggled, this, [this](const bool checked) {
0138         if (checked) {
0139             // make the content visible when expanding starts
0140             contentArea.setVisible(true);
0141         }
0142         // use instant animation if widget isn't visible (e.g. before widget is shown)
0143         const int duration = isVisible() ? animationDuration : 0;
0144         // update the size of the content area
0145         const auto collapsedHeight = sizeHint().height() - contentArea.maximumHeight();
0146         const auto contentHeight = contentArea.layout()->sizeHint().height();
0147         for (int i = 0; i < toggleAnimation.animationCount() - 1; ++i) {
0148             auto expanderAnimation = static_cast<QPropertyAnimation *>(toggleAnimation.animationAt(i));
0149             expanderAnimation->setDuration(duration);
0150             expanderAnimation->setStartValue(collapsedHeight);
0151             expanderAnimation->setEndValue(collapsedHeight + contentHeight);
0152         }
0153         auto contentAnimation = static_cast<QPropertyAnimation *>(toggleAnimation.animationAt(toggleAnimation.animationCount() - 1));
0154         contentAnimation->setDuration(duration);
0155         contentAnimation->setStartValue(0);
0156         contentAnimation->setEndValue(contentHeight);
0157         toggleButton.setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow);
0158         toggleAnimation.setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward);
0159         toggleAnimation.start();
0160     });
0161     connect(&toggleAnimation, &QAbstractAnimation::finished, this, [this]() {
0162         // hide the content area when it is fully collapsed
0163         if (!toggleButton.isChecked()) {
0164             contentArea.setVisible(false);
0165         }
0166     });
0167 }
0168 
0169 void AnimatedExpander::setContentLayout(QLayout *contentLayout)
0170 {
0171     delete contentArea.layout();
0172     contentArea.setLayout(contentLayout);
0173 }
0174 
0175 bool AnimatedExpander::isExpanded() const
0176 {
0177     return toggleButton.isChecked();
0178 }
0179 
0180 void AnimatedExpander::setExpanded(bool expanded)
0181 {
0182     toggleButton.setChecked(expanded);
0183 }
0184 
0185 class SecKeyFilter : public DefaultKeyFilter
0186 {
0187 public:
0188     SecKeyFilter()
0189         : DefaultKeyFilter()
0190     {
0191         setRevoked(DefaultKeyFilter::NotSet);
0192         setExpired(DefaultKeyFilter::NotSet);
0193         setHasSecret(DefaultKeyFilter::Set);
0194         setCanCertify(DefaultKeyFilter::Set);
0195         setIsOpenPGP(DefaultKeyFilter::Set);
0196     }
0197 
0198     bool matches(const GpgME::Key &key, Kleo::KeyFilter::MatchContexts contexts) const override
0199     {
0200         if (!(availableMatchContexts() & contexts)) {
0201             return false;
0202         }
0203         if (_detail::ByFingerprint<std::equal_to>()(key, mExcludedKey)) {
0204             return false;
0205         }
0206         return DefaultKeyFilter::matches(key, contexts);
0207     }
0208 
0209     void setExcludedKey(const GpgME::Key &key)
0210     {
0211         mExcludedKey = key;
0212     }
0213 
0214 private:
0215     GpgME::Key mExcludedKey;
0216 };
0217 
0218 auto checkBoxSize(const QCheckBox *checkBox)
0219 {
0220     QStyleOptionButton opt;
0221     return checkBox->style()->sizeFromContents(QStyle::CT_CheckBox, &opt, QSize(), checkBox);
0222 }
0223 
0224 class TreeWidgetInternal : public TreeWidget
0225 {
0226     Q_OBJECT
0227 public:
0228     using TreeWidget::TreeWidget;
0229 
0230 protected:
0231     void focusInEvent(QFocusEvent *event) override
0232     {
0233         TreeWidget::focusInEvent(event);
0234         // queue the invokation, so that it happens after the widget itself got focus
0235         QMetaObject::invokeMethod(this, &TreeWidgetInternal::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection);
0236     }
0237 
0238     bool edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) override
0239     {
0240         if (event && event->type() == QEvent::KeyPress) {
0241             const auto *const keyEvent = static_cast<QKeyEvent *>(event);
0242             if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Select) {
0243                 // toggle checked state regardless of the index's column
0244                 return TreeWidget::edit(index.siblingAtColumn(0), trigger, event);
0245             }
0246         }
0247         return TreeWidget::edit(index, trigger, event);
0248     }
0249 
0250 private:
0251     void forceAccessibleFocusEventForCurrentItem()
0252     {
0253         // force Qt to send a focus event for the current item to accessibility
0254         // tools; otherwise, the user has no idea which item is selected when the
0255         // list gets keyboard input focus
0256         const auto current = currentIndex();
0257         setCurrentIndex({});
0258         setCurrentIndex(current);
0259     }
0260 };
0261 
0262 struct UserIDCheckState {
0263     GpgME::UserID userId;
0264     Qt::CheckState checkState;
0265 };
0266 }
0267 
0268 class CertifyWidget::Private
0269 {
0270 public:
0271     enum Role { UserIdRole = Qt::UserRole };
0272     enum Mode {
0273         SingleCertification,
0274         BulkCertification,
0275     };
0276     enum TagsState {
0277         TagsMustBeChecked,
0278         TagsLoading,
0279         TagsLoaded,
0280     };
0281 
0282     Private(CertifyWidget *qq)
0283         : q{qq}
0284     {
0285         auto mainLay = new QVBoxLayout{q};
0286 
0287         {
0288             mInfoLabel = new QLabel{i18n("Verify the fingerprint, mark the user IDs you want to certify, "
0289                                          "and select the key you want to certify the user IDs with.<br>"
0290                                          "<i>Note: Only the fingerprint clearly identifies the key and its owner.</i>"),
0291                                     q};
0292             mInfoLabel->setWordWrap(true);
0293             labelHelper.addLabel(mInfoLabel);
0294             mainLay->addWidget(mInfoLabel);
0295         }
0296 
0297         mainLay->addWidget(new KSeparator{Qt::Horizontal, q});
0298 
0299         {
0300             auto grid = new QGridLayout;
0301             grid->setColumnStretch(1, 1);
0302             int row = -1;
0303 
0304             row++;
0305             mFprField = std::make_unique<InfoField>(i18n("Fingerprint:"), q);
0306             grid->addWidget(mFprField->label(), row, 0);
0307             grid->addLayout(mFprField->layout(), row, 1);
0308 
0309             row++;
0310             auto label = new QLabel{i18n("Certify with:"), q};
0311             mSecKeySelect = new KeySelectionCombo{/* secretOnly= */ true, q};
0312             mSecKeySelect->setKeyFilter(std::make_shared<SecKeyFilter>());
0313             label->setBuddy(mSecKeySelect);
0314             grid->addWidget(label, row, 0);
0315             grid->addWidget(mSecKeySelect);
0316 
0317             mainLay->addLayout(grid);
0318         }
0319 
0320         mMissingOwnerTrustInfo = new KMessageWidget{q};
0321         mSetOwnerTrustAction = new QAction{q};
0322         mSetOwnerTrustAction->setText(i18nc("@action:button", "Set Owner Trust"));
0323         mSetOwnerTrustAction->setToolTip(i18nc("@info:tooltip",
0324                                                "Click to set the trust level of the selected certification key to ultimate trust. "
0325                                                "This is what you usually want to do for your own keys."));
0326         connect(mSetOwnerTrustAction, &QAction::triggered, q, [this]() {
0327             setOwnerTrust();
0328         });
0329         mMissingOwnerTrustInfo->addAction(mSetOwnerTrustAction);
0330         mMissingOwnerTrustInfo->setVisible(false);
0331 
0332         mainLay->addWidget(mMissingOwnerTrustInfo);
0333 
0334         mainLay->addWidget(new KSeparator{Qt::Horizontal, q});
0335 
0336         mBadCertificatesInfo = new KMessageWidget{q};
0337         mBadCertificatesInfo->setMessageType(KMessageWidget::Warning);
0338         mBadCertificatesInfo->setIcon(QIcon::fromTheme(QStringLiteral("data-warning"), QIcon::fromTheme(QStringLiteral("dialog-warning"))));
0339         mBadCertificatesInfo->setText(i18nc("@info", "One or more certificates cannot be certified."));
0340         mBadCertificatesInfo->setCloseButtonVisible(false);
0341         mBadCertificatesInfo->setVisible(false);
0342         mainLay->addWidget(mBadCertificatesInfo);
0343 
0344         userIdListView = new TreeWidget{q};
0345         userIdListView->setAccessibleName(i18n("User IDs"));
0346         userIdListView->setEditTriggers(QAbstractItemView::NoEditTriggers);
0347         userIdListView->setSelectionMode(QAbstractItemView::SingleSelection);
0348         userIdListView->setRootIsDecorated(false);
0349         userIdListView->setUniformRowHeights(true);
0350         userIdListView->setAllColumnsShowFocus(false);
0351         userIdListView->setHeaderHidden(true);
0352         userIdListView->setHeaderLabels({i18nc("@title:column", "User ID")});
0353         mainLay->addWidget(userIdListView, 1);
0354 
0355         // Setup the advanced area
0356         mAdvancedOptionsExpander = new AnimatedExpander{i18n("Advanced"), i18n("Show advanced options"), q};
0357         mainLay->addWidget(mAdvancedOptionsExpander);
0358 
0359         auto advLay = new QVBoxLayout;
0360 
0361         mExportCB = new QCheckBox{q};
0362         mExportCB->setText(i18n("Certify for everyone to see (exportable)"));
0363         mExportCB->setToolTip(xi18nc("@info:tooltip",
0364                                      "Check this option, if you want to share your certifications with others. "
0365                                      "If you just want to mark certificates as certified for yourself, then you can uncheck it."));
0366         advLay->addWidget(mExportCB);
0367 
0368         {
0369             auto layout = new QHBoxLayout;
0370 
0371             mPublishCB = new QCheckBox{q};
0372             mPublishCB->setText(i18n("Publish on keyserver afterwards"));
0373             mPublishCB->setToolTip(xi18nc("@info:tooltip",
0374                                           "Check this option, if you want to upload your certifications to a certificate "
0375                                           "directory after successful certification."));
0376             mPublishCB->setEnabled(mExportCB->isChecked());
0377 
0378             layout->addSpacing(checkBoxSize(mExportCB).width());
0379             layout->addWidget(mPublishCB);
0380 
0381             advLay->addLayout(layout);
0382         }
0383 
0384         {
0385             auto tagsLay = new QHBoxLayout;
0386 
0387             auto label = new QLabel{i18n("Tags:"), q};
0388             mTagsLE = new QLineEdit{q};
0389             label->setBuddy(mTagsLE);
0390 
0391             const auto tooltip = i18n("You can use this to add additional info to a certification.") + QStringLiteral("<br/><br/>")
0392                 + i18n("Tags created by anyone with full certification trust "
0393                        "are shown in the keylist and can be searched.");
0394             label->setToolTip(tooltip);
0395             mTagsLE->setToolTip(tooltip);
0396 
0397             tagsLay->addWidget(label);
0398             tagsLay->addWidget(mTagsLE, 1);
0399 
0400             advLay->addLayout(tagsLay);
0401         }
0402 
0403         {
0404             auto layout = new QHBoxLayout;
0405 
0406             mExpirationCheckBox = new QCheckBox{q};
0407             mExpirationCheckBox->setText(i18n("Expiration:"));
0408 
0409             mExpirationDateEdit = new KDateComboBox{q};
0410             Kleo::setUpExpirationDateComboBox(mExpirationDateEdit, {QDate::currentDate().addDays(1), QDate{}});
0411             mExpirationDateEdit->setDate(Kleo::defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration));
0412             mExpirationDateEdit->setEnabled(mExpirationCheckBox->isChecked());
0413 
0414             const auto tooltip = i18n("You can use this to set an expiration date for a certification.") + QStringLiteral("<br/><br/>")
0415                 + i18n("By setting an expiration date, you can limit the validity of "
0416                        "your certification to a certain amount of time. Once the expiration "
0417                        "date has passed, your certification is no longer valid.");
0418             mExpirationCheckBox->setToolTip(tooltip);
0419             mExpirationDateEdit->setToolTip(tooltip);
0420 
0421             layout->addWidget(mExpirationCheckBox);
0422             layout->addWidget(mExpirationDateEdit, 1);
0423 
0424             advLay->addLayout(layout);
0425         }
0426 
0427         {
0428             mTrustSignatureCB = new QCheckBox{q};
0429             mTrustSignatureWidgets.addWidget(mTrustSignatureCB);
0430             mTrustSignatureCB->setText(i18n("Certify as trusted introducer"));
0431             const auto tooltip = i18n("You can use this to certify a trusted introducer for a domain.") + QStringLiteral("<br/><br/>")
0432                 + i18n("All certificates with email addresses belonging to the domain "
0433                        "that have been certified by the trusted introducer are treated "
0434                        "as certified, i.e. a trusted introducer acts as a kind of "
0435                        "intermediate CA for a domain.");
0436             mTrustSignatureCB->setToolTip(tooltip);
0437 
0438             advLay->addWidget(mTrustSignatureCB);
0439         }
0440         {
0441             auto layout = new QHBoxLayout;
0442 
0443             auto label = new QLabel{i18n("Domain:"), q};
0444             mTrustSignatureWidgets.addWidget(label);
0445 
0446             mTrustSignatureDomainLE = new QLineEdit{q};
0447             mTrustSignatureWidgets.addWidget(mTrustSignatureDomainLE);
0448             mTrustSignatureDomainLE->setEnabled(mTrustSignatureCB->isChecked());
0449             label->setBuddy(mTrustSignatureDomainLE);
0450 
0451             layout->addSpacing(checkBoxSize(mTrustSignatureCB).width());
0452             layout->addWidget(label);
0453             layout->addWidget(mTrustSignatureDomainLE);
0454 
0455             advLay->addLayout(layout);
0456         }
0457 
0458         mAdvancedOptionsExpander->setContentLayout(advLay);
0459 
0460         connect(userIdListView, &QTreeWidget::itemChanged, q, [this](auto item, auto) {
0461             onItemChanged(item);
0462         });
0463 
0464         connect(mExportCB, &QCheckBox::toggled, q, [this](bool on) {
0465             mPublishCB->setEnabled(on);
0466         });
0467 
0468         connect(mSecKeySelect, &KeySelectionCombo::currentKeyChanged, q, [this](const GpgME::Key &) {
0469             updateSelectedUserIds();
0470             updateTags();
0471             checkOwnerTrust();
0472             Q_EMIT q->changed();
0473         });
0474 
0475         connect(mExpirationCheckBox, &QCheckBox::toggled, q, [this](bool checked) {
0476             mExpirationDateEdit->setEnabled(checked);
0477             Q_EMIT q->changed();
0478         });
0479         connect(mExpirationDateEdit, &KDateComboBox::dateChanged, q, &CertifyWidget::changed);
0480 
0481         connect(mTrustSignatureCB, &QCheckBox::toggled, q, [this](bool on) {
0482             mTrustSignatureDomainLE->setEnabled(on);
0483             Q_EMIT q->changed();
0484         });
0485         connect(mTrustSignatureDomainLE, &QLineEdit::textChanged, q, &CertifyWidget::changed);
0486 
0487         loadConfig(true);
0488     }
0489 
0490     ~Private() = default;
0491 
0492     void loadConfig(bool loadAll = false)
0493     {
0494         const KConfigGroup conf(KSharedConfig::openConfig(), QStringLiteral("CertifySettings"));
0495 
0496         if (loadAll) {
0497             const Settings settings;
0498             mExpirationCheckBox->setChecked(settings.certificationValidityInDays() > 0);
0499             if (settings.certificationValidityInDays() > 0) {
0500                 const QDate expirationDate = QDate::currentDate().addDays(settings.certificationValidityInDays());
0501                 mExpirationDateEdit->setDate(expirationDate > mExpirationDateEdit->maximumDate() //
0502                                                  ? mExpirationDateEdit->maximumDate() //
0503                                                  : expirationDate);
0504             }
0505 
0506             mSecKeySelect->setDefaultKey(conf.readEntry("LastKey", QString()));
0507         }
0508 
0509         switch (mMode) {
0510         case SingleCertification: {
0511             mExportCB->setChecked(conf.readEntry("ExportCheckState", false));
0512             mPublishCB->setChecked(conf.readEntry("PublishCheckState", false));
0513             mAdvancedOptionsExpander->setExpanded(conf.readEntry("AdvancedOptionsExpanded", false));
0514             break;
0515         }
0516         case BulkCertification: {
0517             mExportCB->setChecked(conf.readEntry("BulkExportCheckState", true));
0518             mPublishCB->setChecked(conf.readEntry("BulkPublishCheckState", false));
0519             mAdvancedOptionsExpander->setExpanded(conf.readEntry("BulkAdvancedOptionsExpanded", true));
0520             break;
0521         }
0522         }
0523     }
0524 
0525     void saveConfig()
0526     {
0527         KConfigGroup conf{KSharedConfig::openConfig(), QLatin1StringView("CertifySettings")};
0528         if (!secKey().isNull()) {
0529             conf.writeEntry("LastKey", secKey().primaryFingerprint());
0530         }
0531         switch (mMode) {
0532         case SingleCertification: {
0533             conf.writeEntry("ExportCheckState", mExportCB->isChecked());
0534             conf.writeEntry("PublishCheckState", mPublishCB->isChecked());
0535             conf.writeEntry("AdvancedOptionsExpanded", mAdvancedOptionsExpander->isExpanded());
0536             break;
0537         }
0538         case BulkCertification: {
0539             conf.writeEntry("BulkExportCheckState", mExportCB->isChecked());
0540             conf.writeEntry("BulkPublishCheckState", mPublishCB->isChecked());
0541             conf.writeEntry("BulkAdvancedOptionsExpanded", mAdvancedOptionsExpander->isExpanded());
0542             break;
0543         }
0544         }
0545         conf.sync();
0546     }
0547 
0548     void setMode(Mode mode)
0549     {
0550         mMode = mode;
0551         switch (mMode) {
0552         case SingleCertification:
0553             break;
0554         case BulkCertification: {
0555             mInfoLabel->setText(i18nc("@info",
0556                                       "Verify the fingerprints, mark the user IDs you want to certify, "
0557                                       "and select the certificate you want to certify the user IDs with.<br>"
0558                                       "<i>Note: Only the fingerprints clearly identify the certificate and its owner.</i>"));
0559             mFprField->setVisible(false);
0560             mTrustSignatureWidgets.setVisible(false);
0561             break;
0562         }
0563         }
0564         loadConfig();
0565     }
0566 
0567     void setUpUserIdList(const std::vector<GpgME::UserID> &uids = {})
0568     {
0569         userIdListView->clear();
0570         if (mMode == SingleCertification) {
0571             userIdListView->setColumnCount(1);
0572             userIdListView->setHeaderHidden(true);
0573             // set header labels for accessibility tools to overwrite the default "1"
0574             userIdListView->setHeaderLabels({i18nc("@title:column", "User ID")});
0575             for (const auto &uid : uids) {
0576                 if (uid.isInvalid() || Kleo::isRevokedOrExpired(uid)) {
0577                     // Skip user IDs that cannot really be certified.
0578                     continue;
0579                 }
0580                 auto item = new QTreeWidgetItem;
0581                 item->setData(0, UserIdRole, QVariant::fromValue(uid));
0582                 item->setData(0, Qt::DisplayRole, Kleo::Formatting::prettyUserID(uid));
0583                 item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
0584                 item->setCheckState(0, Qt::Checked);
0585                 userIdListView->addTopLevelItem(item);
0586             }
0587         } else {
0588             const QStringList headers = {i18nc("@title:column", "User ID"), i18nc("@title:column", "Fingerprint")};
0589             userIdListView->setColumnCount(headers.count());
0590             userIdListView->setHeaderHidden(false);
0591             userIdListView->setHeaderLabels(headers);
0592             for (const auto &key : mKeys) {
0593                 const auto &uid = key.userID(0);
0594                 auto item = new QTreeWidgetItem;
0595                 item->setData(0, UserIdRole, QVariant::fromValue(uid));
0596                 item->setData(0, Qt::DisplayRole, Kleo::Formatting::prettyUserID(uid));
0597                 item->setData(1, Qt::DisplayRole, Kleo::Formatting::prettyID(key.primaryFingerprint()));
0598                 item->setData(1, Qt::AccessibleTextRole, Kleo::Formatting::accessibleHexID(key.primaryFingerprint()));
0599 
0600                 if ((key.protocol() != OpenPGP) || uid.isInvalid() || Kleo::isRevokedOrExpired(uid)) {
0601                     item->setFlags(Qt::NoItemFlags);
0602                     item->setCheckState(0, Qt::Unchecked);
0603                     if (key.protocol() == CMS) {
0604                         item->setData(0, Qt::ToolTipRole, i18nc("@info:tooltip", "S/MIME certificates cannot be certified."));
0605                         item->setData(1, Qt::ToolTipRole, i18nc("@info:tooltip", "S/MIME certificates cannot be certified."));
0606                     } else {
0607                         item->setData(0, Qt::ToolTipRole, i18nc("@info:tooltip", "Expired or revoked certificates cannot be certified."));
0608                         item->setData(1, Qt::ToolTipRole, i18nc("@info:tooltip", "Expired or revoked certificates cannot be certified."));
0609                     }
0610                 } else {
0611                     item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
0612                     item->setCheckState(0, Qt::Checked);
0613                 }
0614                 userIdListView->addTopLevelItem(item);
0615             }
0616             userIdListView->sortItems(0, Qt::AscendingOrder);
0617             userIdListView->resizeColumnToContents(0);
0618             userIdListView->resizeColumnToContents(1);
0619         }
0620     }
0621 
0622     void updateSelectedUserIds()
0623     {
0624         if (mMode == SingleCertification) {
0625             return;
0626         }
0627         if (userIdListView->topLevelItemCount() == 0) {
0628             return;
0629         }
0630 
0631         // restore check state of primary user ID of previous certification key
0632         if (!mCertificationKey.isNull()) {
0633             for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
0634                 const auto uidItem = userIdListView->topLevelItem(i);
0635                 const auto itemUserId = getUserId(uidItem);
0636                 if (userIDBelongsToKey(itemUserId, mCertificationKey)) {
0637                     uidItem->setCheckState(0, mCertificationKeyUserIDCheckState);
0638                     uidItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
0639                     break; // we only show the primary user IDs
0640                 }
0641             }
0642         }
0643 
0644         mCertificationKey = mSecKeySelect->currentKey();
0645 
0646         // save and unset check state of primary user ID of current certification key
0647         if (!mCertificationKey.isNull()) {
0648             for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
0649                 const auto uidItem = userIdListView->topLevelItem(i);
0650                 const auto itemUserId = getUserId(uidItem);
0651                 if (userIDBelongsToKey(itemUserId, mCertificationKey)) {
0652                     mCertificationKeyUserIDCheckState = uidItem->checkState(0);
0653                     if (mCertificationKeyUserIDCheckState) {
0654                         uidItem->setCheckState(0, Qt::Unchecked);
0655                     }
0656                     uidItem->setFlags(Qt::ItemIsSelectable);
0657                     break; // we only show the primary user IDs
0658                 }
0659             }
0660         }
0661     }
0662 
0663     void updateTags()
0664     {
0665         struct ItemAndRemark {
0666             QTreeWidgetItem *item;
0667             QString remark;
0668         };
0669 
0670         if (mTagsState != TagsLoaded) {
0671             return;
0672         }
0673         if (mTagsLE->isModified()) {
0674             return;
0675         }
0676         GpgME::Key remarkKey = mSecKeySelect->currentKey();
0677 
0678         if (!remarkKey.isNull()) {
0679             std::vector<ItemAndRemark> itemsAndRemarks;
0680             // first choose the remark we want to prefill the Tags field with
0681             QString remark;
0682             for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
0683                 const auto item = userIdListView->topLevelItem(i);
0684                 if (item->isDisabled()) {
0685                     continue;
0686                 }
0687                 const auto uid = getUserId(item);
0688                 GpgME::Error err;
0689                 const char *c_remark = uid.remark(remarkKey, err);
0690                 const QString itemRemark = (!err && c_remark) ? QString::fromUtf8(c_remark) : QString{};
0691                 if (!itemRemark.isEmpty() && (itemRemark != remark)) {
0692                     if (!remark.isEmpty()) {
0693                         qCDebug(KLEOPATRA_LOG) << "Different remarks on user IDs. Taking last.";
0694                     }
0695                     remark = itemRemark;
0696                 }
0697                 itemsAndRemarks.push_back({item, itemRemark});
0698             }
0699             // then select the user IDs with the chosen remark; this prevents overwriting existing
0700             // different remarks on the other user IDs (as long as the user doesn't select any of
0701             // the unselected user IDs with a different remark)
0702             if (!remark.isEmpty()) {
0703                 for (const auto &[item, itemRemark] : itemsAndRemarks) {
0704                     item->setCheckState(0, itemRemark == remark ? Qt::Checked : Qt::Unchecked);
0705                 }
0706             }
0707             mTagsLE->setText(remark);
0708         }
0709     }
0710 
0711     void updateTrustSignatureDomain()
0712     {
0713         if (mMode == SingleCertification) {
0714             if (mTrustSignatureDomainLE->text().isEmpty() && certificate().numUserIDs() == 1) {
0715                 // try to guess the domain to use for the trust signature
0716                 const auto address = certificate().userID(0).addrSpec();
0717                 const auto atPos = address.find('@');
0718                 if (atPos != std::string::npos) {
0719                     const auto domain = address.substr(atPos + 1);
0720                     mTrustSignatureDomainLE->setText(QString::fromUtf8(domain.c_str(), domain.size()));
0721                 }
0722             }
0723         }
0724     }
0725 
0726     void loadAllTags()
0727     {
0728         const auto keyWithoutTags = std::find_if(mKeys.cbegin(), mKeys.cend(), [](const auto &key) {
0729             return (key.protocol() == GpgME::OpenPGP) && !(key.keyListMode() & GpgME::SignatureNotations);
0730         });
0731         const auto indexOfKeyWithoutTags = std::distance(mKeys.cbegin(), keyWithoutTags);
0732         if (indexOfKeyWithoutTags < signed(mKeys.size())) {
0733             auto loadTags = [this, indexOfKeyWithoutTags]() {
0734                 Q_ASSERT(indexOfKeyWithoutTags < signed(mKeys.size()));
0735                 // call update() on the reference to the vector element because it swaps key with the updated key
0736                 mKeys[indexOfKeyWithoutTags].update();
0737                 loadAllTags();
0738             };
0739             QMetaObject::invokeMethod(q, loadTags, Qt::QueuedConnection);
0740             return;
0741         }
0742         mTagsState = TagsLoaded;
0743         QMetaObject::invokeMethod(
0744             q,
0745             [this]() {
0746                 setUpWidget();
0747             },
0748             Qt::QueuedConnection);
0749     }
0750 
0751     bool ensureTagsLoaded()
0752     {
0753         Q_ASSERT(mTagsState != TagsLoading);
0754         if (mTagsState == TagsLoaded) {
0755             return true;
0756         }
0757 
0758         const auto allTagsAreLoaded = Kleo::all_of(mKeys, [](const auto &key) {
0759             return (key.protocol() != GpgME::OpenPGP) || (key.keyListMode() & GpgME::SignatureNotations);
0760         });
0761         if (allTagsAreLoaded) {
0762             mTagsState = TagsLoaded;
0763         } else {
0764             mTagsState = TagsLoading;
0765             QMetaObject::invokeMethod(
0766                 q,
0767                 [this]() {
0768                     loadAllTags();
0769                 },
0770                 Qt::QueuedConnection);
0771         }
0772         return mTagsState == TagsLoaded;
0773     }
0774 
0775     void setUpWidget()
0776     {
0777         if (!ensureTagsLoaded()) {
0778             return;
0779         }
0780         if (mMode == SingleCertification) {
0781             const auto key = certificate();
0782             mFprField->setValue(QStringLiteral("<b>") + Formatting::prettyID(key.primaryFingerprint()) + QStringLiteral("</b>"),
0783                                 Formatting::accessibleHexID(key.primaryFingerprint()));
0784             setUpUserIdList(mUserIds.empty() ? key.userIDs() : mUserIds);
0785 
0786             auto keyFilter = std::make_shared<SecKeyFilter>();
0787             keyFilter->setExcludedKey(key);
0788             mSecKeySelect->setKeyFilter(keyFilter);
0789 
0790             updateTrustSignatureDomain();
0791         } else {
0792             // check for certificates that cannot be certified
0793             const auto haveBadCertificates = Kleo::any_of(mKeys, [](const auto &key) {
0794                 const auto &uid = key.userID(0);
0795                 return (key.protocol() != OpenPGP) || uid.isInvalid() || Kleo::isRevokedOrExpired(uid);
0796             });
0797             if (haveBadCertificates) {
0798                 mBadCertificatesInfo->animatedShow();
0799             }
0800 
0801             setUpUserIdList();
0802         }
0803         updateTags();
0804         updateSelectedUserIds();
0805         Q_EMIT q->changed();
0806     }
0807 
0808     GpgME::Key certificate() const
0809     {
0810         Q_ASSERT(mMode == SingleCertification);
0811         return !mKeys.empty() ? mKeys.front() : Key{};
0812     }
0813 
0814     void setCertificates(const std::vector<GpgME::Key> &keys, const std::vector<GpgME::UserID> &uids)
0815     {
0816         mKeys = keys;
0817         mUserIds = uids;
0818         mTagsState = TagsMustBeChecked;
0819         setUpWidget();
0820     }
0821 
0822     std::vector<GpgME::Key> certificates() const
0823     {
0824         Q_ASSERT(mMode != SingleCertification);
0825         return mKeys;
0826     }
0827 
0828     GpgME::Key secKey() const
0829     {
0830         return mSecKeySelect->currentKey();
0831     }
0832 
0833     GpgME::UserID getUserId(const QTreeWidgetItem *item) const
0834     {
0835         return item ? item->data(0, UserIdRole).value<UserID>() : UserID{};
0836     }
0837 
0838     void selectUserIDs(const std::vector<GpgME::UserID> &uids)
0839     {
0840         for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
0841             const auto uidItem = userIdListView->topLevelItem(i);
0842             const auto itemUserId = getUserId(uidItem);
0843             const bool userIdIsInList = Kleo::any_of(uids, [itemUserId](const auto &uid) {
0844                 return Kleo::userIDsAreEqual(itemUserId, uid);
0845             });
0846             uidItem->setCheckState(0, userIdIsInList ? Qt::Checked : Qt::Unchecked);
0847         }
0848     }
0849 
0850     std::vector<GpgME::UserID> selectedUserIDs() const
0851     {
0852         std::vector<GpgME::UserID> userIds;
0853         userIds.reserve(userIdListView->topLevelItemCount());
0854         for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
0855             const auto *const uidItem = userIdListView->topLevelItem(i);
0856             if (uidItem->checkState(0) == Qt::Checked) {
0857                 userIds.push_back(getUserId(uidItem));
0858             }
0859         }
0860         qCDebug(KLEOPATRA_LOG) << "Checked user IDs:" << userIds;
0861         return userIds;
0862     }
0863 
0864     bool exportableSelected() const
0865     {
0866         return mExportCB->isChecked();
0867     }
0868 
0869     bool publishSelected() const
0870     {
0871         return mPublishCB->isChecked();
0872     }
0873 
0874     QString tags() const
0875     {
0876         return mTagsLE->text().trimmed();
0877     }
0878 
0879     bool isValid() const
0880     {
0881         static const QRegularExpression domainNameRegExp{QStringLiteral(R"(^\s*((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}\s*$)"),
0882                                                          QRegularExpression::CaseInsensitiveOption};
0883 
0884         if (mTagsState != TagsLoaded) {
0885             return false;
0886         }
0887         // do not accept null keys
0888         if (mKeys.empty() || mSecKeySelect->currentKey().isNull()) {
0889             return false;
0890         }
0891         // do not accept empty list of user IDs
0892         const auto userIds = selectedUserIDs();
0893         if (userIds.empty()) {
0894             return false;
0895         }
0896         // do not accept if any of the selected user IDs belongs to the certification key
0897         const auto certificationKey = mSecKeySelect->currentKey();
0898         const auto userIdToCertifyBelongsToCertificationKey = std::any_of(userIds.cbegin(), userIds.cend(), [certificationKey](const auto &userId) {
0899             return Kleo::userIDBelongsToKey(userId, certificationKey);
0900         });
0901         if (userIdToCertifyBelongsToCertificationKey) {
0902             return false;
0903         }
0904         if (mExpirationCheckBox->isChecked() && !mExpirationDateEdit->isValid()) {
0905             return false;
0906         }
0907         if (mTrustSignatureCB->isChecked() && !domainNameRegExp.match(mTrustSignatureDomainLE->text()).hasMatch()) {
0908             return false;
0909         }
0910         return true;
0911     }
0912 
0913     void checkOwnerTrust()
0914     {
0915         const auto secretKey = secKey();
0916         if (secretKey.ownerTrust() != GpgME::Key::Ultimate) {
0917             mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Information);
0918             mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("question")));
0919             mMissingOwnerTrustInfo->setText(i18n("Is this your own key?"));
0920             mSetOwnerTrustAction->setEnabled(true);
0921             mMissingOwnerTrustInfo->animatedShow();
0922         } else {
0923             mMissingOwnerTrustInfo->animatedHide();
0924         }
0925     }
0926 
0927     void setOwnerTrust()
0928     {
0929         mSetOwnerTrustAction->setEnabled(false);
0930         QGpgME::ChangeOwnerTrustJob *const j = QGpgME::openpgp()->changeOwnerTrustJob();
0931         connect(j, &QGpgME::ChangeOwnerTrustJob::result, q, [this](const GpgME::Error &err) {
0932             if (err) {
0933                 KMessageBox::error(q,
0934                                    i18n("<p>Changing the certification trust of the key <b>%1</b> failed:</p><p>%2</p>",
0935                                         Formatting::formatForComboBox(secKey()),
0936                                         Formatting::errorAsString(err)),
0937                                    i18nc("@title:window", "Certification Trust Change Failed"));
0938             }
0939             if (err || err.isCanceled()) {
0940                 mSetOwnerTrustAction->setEnabled(true);
0941             } else {
0942                 mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Positive);
0943                 mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("checkmark")));
0944                 mMissingOwnerTrustInfo->setText(i18n("Owner trust set successfully."));
0945             }
0946         });
0947         j->start(secKey(), GpgME::Key::Ultimate);
0948     }
0949 
0950     void onItemChanged(QTreeWidgetItem *item)
0951     {
0952         Q_EMIT q->changed();
0953 
0954 #ifndef QT_NO_ACCESSIBILITY
0955         if (item) {
0956             // assume that the checked state changed
0957             QAccessible::State st;
0958             st.checked = true;
0959             QAccessibleStateChangeEvent e(userIdListView, st);
0960             e.setChild(userIdListView->indexOfTopLevelItem(item));
0961             QAccessible::updateAccessibility(&e);
0962         }
0963 #endif
0964     }
0965 
0966 public:
0967     CertifyWidget *const q;
0968     QLabel *mInfoLabel = nullptr;
0969     std::unique_ptr<InfoField> mFprField;
0970     KeySelectionCombo *mSecKeySelect = nullptr;
0971     KMessageWidget *mMissingOwnerTrustInfo = nullptr;
0972     KMessageWidget *mBadCertificatesInfo = nullptr;
0973     TreeWidget *userIdListView = nullptr;
0974     AnimatedExpander *mAdvancedOptionsExpander = nullptr;
0975     QCheckBox *mExportCB = nullptr;
0976     QCheckBox *mPublishCB = nullptr;
0977     QLineEdit *mTagsLE = nullptr;
0978     BulkStateChanger mTrustSignatureWidgets;
0979     QCheckBox *mTrustSignatureCB = nullptr;
0980     QLineEdit *mTrustSignatureDomainLE = nullptr;
0981     QCheckBox *mExpirationCheckBox = nullptr;
0982     KDateComboBox *mExpirationDateEdit = nullptr;
0983     QAction *mSetOwnerTrustAction = nullptr;
0984 
0985     LabelHelper labelHelper;
0986 
0987     Mode mMode = SingleCertification;
0988     std::vector<GpgME::Key> mKeys;
0989     std::vector<GpgME::UserID> mUserIds;
0990     TagsState mTagsState = TagsMustBeChecked;
0991 
0992     GpgME::Key mCertificationKey;
0993     Qt::CheckState mCertificationKeyUserIDCheckState;
0994 };
0995 
0996 CertifyWidget::CertifyWidget(QWidget *parent)
0997     : QWidget{parent}
0998     , d{std::make_unique<Private>(this)}
0999 {
1000 }
1001 
1002 Kleo::CertifyWidget::~CertifyWidget() = default;
1003 
1004 void CertifyWidget::setCertificate(const GpgME::Key &key, const std::vector<GpgME::UserID> &uids)
1005 {
1006     Q_ASSERT(!key.isNull());
1007     d->setMode(Private::SingleCertification);
1008     d->setCertificates({key}, uids);
1009 }
1010 
1011 GpgME::Key CertifyWidget::certificate() const
1012 {
1013     return d->certificate();
1014 }
1015 
1016 void CertifyWidget::setCertificates(const std::vector<GpgME::Key> &keys)
1017 {
1018     d->setMode(Private::BulkCertification);
1019     d->setCertificates(keys, {});
1020 }
1021 
1022 std::vector<GpgME::Key> CertifyWidget::certificates() const
1023 {
1024     return d->certificates();
1025 }
1026 
1027 void CertifyWidget::selectUserIDs(const std::vector<GpgME::UserID> &uids)
1028 {
1029     d->selectUserIDs(uids);
1030 }
1031 
1032 std::vector<GpgME::UserID> CertifyWidget::selectedUserIDs() const
1033 {
1034     return d->selectedUserIDs();
1035 }
1036 
1037 GpgME::Key CertifyWidget::secKey() const
1038 {
1039     return d->secKey();
1040 }
1041 
1042 bool CertifyWidget::exportableSelected() const
1043 {
1044     return d->exportableSelected();
1045 }
1046 
1047 QString CertifyWidget::tags() const
1048 {
1049     return d->tags();
1050 }
1051 
1052 bool CertifyWidget::publishSelected() const
1053 {
1054     return d->publishSelected();
1055 }
1056 
1057 bool CertifyWidget::trustSignatureSelected() const
1058 {
1059     return d->mTrustSignatureCB->isChecked();
1060 }
1061 
1062 QString CertifyWidget::trustSignatureDomain() const
1063 {
1064     return d->mTrustSignatureDomainLE->text().trimmed();
1065 }
1066 
1067 QDate CertifyWidget::expirationDate() const
1068 {
1069     return d->mExpirationCheckBox->isChecked() ? d->mExpirationDateEdit->date() : QDate{};
1070 }
1071 
1072 bool CertifyWidget::isValid() const
1073 {
1074     return d->isValid();
1075 }
1076 
1077 void CertifyWidget::saveState() const
1078 {
1079     d->saveConfig();
1080 }
1081 
1082 #include "certifywidget.moc"
1083 
1084 #include "moc_certifywidget.cpp"