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"