diff --git a/src/commands/changeexpirycommand.cpp b/src/commands/changeexpirycommand.cpp index 0fa0c742b..e19f3803b 100644 --- a/src/commands/changeexpirycommand.cpp +++ b/src/commands/changeexpirycommand.cpp @@ -1,287 +1,285 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/changeexpirycommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "changeexpirycommand.h" #include "command_p.h" #include "dialogs/expirydialog.h" #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace GpgME; using namespace QGpgME; namespace { #ifdef QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY bool allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(const Key &key) { Q_ASSERT(!key.isNull() && key.numSubkeys() > 0); const auto subkeys = key.subkeys(); const auto primaryKey = subkeys[0]; if (primaryKey.neverExpires()) { return std::all_of(std::begin(subkeys), std::end(subkeys), [] (const auto &subkey) { // revoked subkeys are ignored by gpg --quick-set-expire when updating the expiration of all subkeys return subkey.isRevoked() || subkey.neverExpires(); }); } const auto primaryExpiration = primaryKey.expirationTime(); return std::all_of(std::begin(subkeys), std::end(subkeys), [primaryExpiration] (const auto &subkey) { // revoked subkeys are ignored by gpg --quick-set-expire when updating the expiration of all subkeys; // check if expiration of subkey is (more or less) the same as the expiration of the primary key return subkey.isRevoked() || (primaryExpiration - 10 <= subkey.expirationTime() && subkey.expirationTime() <= primaryExpiration + 10); }); } #endif } class ChangeExpiryCommand::Private : public Command::Private { friend class ::Kleo::Commands::ChangeExpiryCommand; ChangeExpiryCommand *q_func() const { return static_cast(q); } public: explicit Private(ChangeExpiryCommand *qq, KeyListController *c); ~Private() override; private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void ensureDialogCreated(ExpiryDialog::Mode mode); void createJob(); void showErrorDialog(const Error &error); void showSuccessDialog(); private: GpgME::Key key; GpgME::Subkey subkey; QPointer dialog; QPointer job; }; ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func() { return static_cast(d.get()); } const ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ChangeExpiryCommand::Private::Private(ChangeExpiryCommand *qq, KeyListController *c) : Command::Private{qq, c} { } ChangeExpiryCommand::Private::~Private() = default; void ChangeExpiryCommand::Private::slotDialogAccepted() { Q_ASSERT(dialog); static const QTime END_OF_DAY{23, 59, 59}; const QDateTime expiry{dialog->dateOfExpiry(), END_OF_DAY}; qCDebug(KLEOPATRA_LOG) << "expiry" << expiry; createJob(); Q_ASSERT(job); #ifdef QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY if (subkey.isNull() && dialog->updateExpirationOfAllSubkeys()) { job->setOptions(ChangeExpiryJob::UpdateAllSubkeys); } #endif std::vector subkeys; if (!subkey.isNull() && subkey.keyID() != key.keyID()) { // ignore the primary subkey subkeys.push_back(subkey); } if (const Error err = job->start(key, expiry, subkeys)) { showErrorDialog(err); finished(); } } void ChangeExpiryCommand::Private::slotDialogRejected() { Q_EMIT q->canceled(); finished(); } void ChangeExpiryCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) ; else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void ChangeExpiryCommand::Private::ensureDialogCreated(ExpiryDialog::Mode mode) { if (dialog) { return; } dialog = new ExpiryDialog{mode}; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void ChangeExpiryCommand::Private::createJob() { Q_ASSERT(!job); const auto backend = (key.protocol() == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); if (!backend) { return; } ChangeExpiryJob *const j = backend->changeExpiryJob(); if (!j) { return; } connect(j, &Job::progress, q, &Command::progress); connect(j, &ChangeExpiryJob::result, q, [this] (const auto &err) { slotResult(err); }); job = j; } void ChangeExpiryCommand::Private::showErrorDialog(const Error &err) { error(i18n("

An error occurred while trying to change " - "the expiry date for %1:

%2

", + "the end of the validity period for %1:

%2

", Formatting::formatForComboBox(key), - QString::fromLocal8Bit(err.asString())), - i18n("Expiry Date Change Error")); + QString::fromLocal8Bit(err.asString()))); } void ChangeExpiryCommand::Private::showSuccessDialog() { - information(i18n("Expiry date changed successfully."), - i18n("Expiry Date Change Succeeded")); + success(i18n("End of validity period changed successfully.")); } ChangeExpiryCommand::ChangeExpiryCommand(KeyListController *c) : Command{new Private{this, c}} { } ChangeExpiryCommand::ChangeExpiryCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{this, c}} { } ChangeExpiryCommand::ChangeExpiryCommand(const GpgME::Key &key) : Command{key, new Private{this, nullptr}} { } ChangeExpiryCommand::~ChangeExpiryCommand() = default; void ChangeExpiryCommand::setSubkey(const GpgME::Subkey &subkey) { d->subkey = subkey; } void ChangeExpiryCommand::doStart() { const std::vector keys = d->keys(); if (keys.size() != 1 || keys.front().protocol() != GpgME::OpenPGP || !keys.front().hasSecret() || keys.front().subkey(0).isNull()) { d->finished(); return; } d->key = keys.front(); if (!d->subkey.isNull() && d->subkey.parent().primaryFingerprint() != d->key.primaryFingerprint()) { qDebug() << "Invalid subkey" << d->subkey.fingerprint() << ": Not a subkey of key" << d->key.primaryFingerprint(); d->finished(); return; } ExpiryDialog::Mode mode; if (!d->subkey.isNull()) { mode = ExpiryDialog::Mode::UpdateIndividualSubkey; } else if (d->key.numSubkeys() == 1) { mode = ExpiryDialog::Mode::UpdateCertificateWithoutSubkeys; } else { mode = ExpiryDialog::Mode::UpdateCertificateWithSubkeys; } d->ensureDialogCreated(mode); Q_ASSERT(d->dialog); const Subkey subkey = !d->subkey.isNull() ? d->subkey : d->key.subkey(0); d->dialog->setDateOfExpiry(subkey.neverExpires() ? QDate() : QDateTime::fromSecsSinceEpoch(subkey.expirationTime()).date()); #ifdef QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY if (mode == ExpiryDialog::Mode::UpdateCertificateWithSubkeys) { d->dialog->setUpdateExpirationOfAllSubkeys(allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(d->key)); } #endif d->dialog->show(); } void ChangeExpiryCommand::doCancel() { if (d->job) { d->job->slotCancel(); } } #undef d #undef q #include "moc_changeexpirycommand.cpp" diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp index 9270cc0da..97bd4a0e3 100644 --- a/src/dialogs/certificatedetailswidget.cpp +++ b/src/dialogs/certificatedetailswidget.cpp @@ -1,1209 +1,1209 @@ /* dialogs/certificatedetailswidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-FileCopyrightText: 2022 Felix Tiede SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certificatedetailswidget.h" #include "kleopatra_debug.h" #include "exportdialog.h" #include "trustchainwidget.h" #include "subkeyswidget.h" #include "weboftrustdialog.h" #include "commands/changepassphrasecommand.h" #include "commands/changeexpirycommand.h" #include "commands/certifycertificatecommand.h" #ifdef MAILAKONADI_ENABLED #include "commands/exportopenpgpcerttoprovidercommand.h" #endif // MAILAKONADI_ENABLED #include "commands/refreshcertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "commands/revokeuseridcommand.h" #include "commands/adduseridcommand.h" #include "commands/genrevokecommand.h" #include "commands/detailscommand.h" #include "commands/dumpcertificatecommand.h" #include "utils/keys.h" #include "utils/tags.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if __has_include() #include #define USE_RANGES #endif #include Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; static const char *accessibleNameProperty = "_kleo_accessibleName"; class InfoField { public: InfoField(const QString &label, QWidget *parent); void setValue(const QString &value, const QString &accessibleValue = {}); QString value() const; void setIcon(const QIcon &icon); void setAction(const QAction *action); void setToolTip(const QString &toolTip); void setVisible(bool visible); QLabel *label() const { return mLabel; } QLayout *layout() const { return mLayout; } private: void onActionChanged(); QLabel *mLabel = nullptr; QHBoxLayout *mLayout = nullptr; QLabel *mIcon = nullptr; QLabel *mValue = nullptr; QPushButton *mButton = nullptr; const QAction *mAction = nullptr; }; InfoField::InfoField(const QString &label, QWidget *parent) : mLabel{new QLabel{label, parent}} , mLayout{new QHBoxLayout} , mIcon{new QLabel{parent}} , mValue{new QLabel{parent}} , mButton{new QPushButton{parent}} { mLabel->setBuddy(mValue); mLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); mIcon->setVisible(false); mLayout->addWidget(mIcon); mValue->setTextInteractionFlags(Qt::TextSelectableByMouse); mValue->setFocusPolicy(Qt::TabFocus); mLayout->addWidget(mValue); mButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); mButton->setVisible(false); mLayout->addWidget(mButton); mLayout->addStretch(); } void InfoField::setValue(const QString &value, const QString &accessibleValue) { mValue->setText(value); mValue->setAccessibleName(accessibleValue); } QString InfoField::value() const { return mValue->text(); } void InfoField::setIcon(const QIcon &icon) { if (!icon.isNull()) { const int iconSize = mIcon->style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, mIcon); mIcon->setPixmap(icon.pixmap(iconSize)); mIcon->setVisible(true); } else { mIcon->setVisible(false); mIcon->clear(); } } void InfoField::setAction(const QAction *action) { if (action == mAction) { return; } if (mAction) { QObject::disconnect(mButton, {}, mAction, {}); QObject::disconnect(mAction, {}, mButton, {}); } mAction = action; if (mAction) { QObject::connect(mButton, &QPushButton::clicked, action, &QAction::trigger); QObject::connect(mAction, &QAction::changed, mButton, [this]() { onActionChanged(); }); onActionChanged(); mButton->setAccessibleName(mAction->property(accessibleNameProperty).toString()); mButton->setVisible(true); } else { mButton->setVisible(false); mButton->setText({}); mButton->setIcon({}); } } void InfoField::setToolTip(const QString &toolTip) { mValue->setToolTip(toolTip); } void InfoField::setVisible(bool visible) { mLabel->setVisible(visible); mIcon->setVisible(visible && !mIcon->pixmap(Qt::ReturnByValue).isNull()); mValue->setVisible(visible); mButton->setVisible(visible && mAction); } void InfoField::onActionChanged() { if (!mAction) { return; } if (mAction->text() != mButton->text()) { mButton->setText(mAction->text()); } mButton->setIcon(mAction->icon()); if (mAction->toolTip() != mButton->toolTip()) { mButton->setToolTip(mAction->toolTip()); } if (mAction->isEnabled() != mButton->isEnabled()) { mButton->setEnabled(mAction->isEnabled()); } } namespace { std::vector selectedUserIDs(const QTreeWidget *treeWidget) { if (!treeWidget) { return {}; } std::vector userIDs; const auto selected = treeWidget->selectedItems(); std::transform(selected.begin(), selected.end(), std::back_inserter(userIDs), [](const QTreeWidgetItem *item) { return item->data(0, Qt::UserRole).value(); }); return userIDs; } } class CertificateDetailsWidget::Private { public: Private(CertificateDetailsWidget *qq); void setupCommonProperties(); void updateUserIDActions(); void setUpUserIDTable(); void setUpSMIMEAdressList(); void setupPGPProperties(); void setupSMIMEProperties(); void revokeUserID(const GpgME::UserID &uid); void revokeSelectedUserID(); void genRevokeCert(); void refreshCertificate(); void certifyUserIDs(); void revokeCertifications(); void webOfTrustClicked(); void exportClicked(); void addUserID(); void changePassphrase(); void changeExpiration(); void keysMayHaveChanged(); void showTrustChainDialog(); void showMoreDetails(); void userIDTableContextMenuRequested(const QPoint &p); QString tofuTooltipString(const GpgME::UserID &uid) const; QIcon trustLevelIcon(const GpgME::UserID &uid) const; QString trustLevelText(const GpgME::UserID &uid) const; void showIssuerCertificate(); void updateKey(); void setUpdatedKey(const GpgME::Key &key); void keyListDone(const GpgME::KeyListResult &, const std::vector &, const QString &, const GpgME::Error &); void copyFingerprintToClipboard(); private: CertificateDetailsWidget *const q; public: GpgME::Key key; bool updateInProgress = false; private: InfoField *attributeField(const QString &attributeName) { const auto keyValuePairIt = ui.smimeAttributeFields.find(attributeName); if (keyValuePairIt != ui.smimeAttributeFields.end()) { return (*keyValuePairIt).second.get(); } return nullptr; } private: struct UI { QWidget *userIDs = nullptr; QLabel *userIDTableLabel = nullptr; NavigatableTreeWidget *userIDTable = nullptr; QPushButton *addUserIDBtn = nullptr; QPushButton *certifyBtn = nullptr; QPushButton *revokeCertificationsBtn = nullptr; QPushButton *revokeUserIDBtn = nullptr; QPushButton *webOfTrustBtn = nullptr; std::map> smimeAttributeFields; std::unique_ptr smimeTrustLevelField; std::unique_ptr validFromField; std::unique_ptr expiresField; QAction *changeExpirationAction = nullptr; std::unique_ptr fingerprintField; QAction *copyFingerprintAction = nullptr; std::unique_ptr smimeIssuerField; QAction *showIssuerCertificateAction = nullptr; std::unique_ptr complianceField; std::unique_ptr trustedIntroducerField; QLabel *smimeRelatedAddresses = nullptr; QListWidget *smimeAddressList = nullptr; QPushButton *moreDetailsBtn = nullptr; QPushButton *trustChainDetailsBtn = nullptr; QPushButton *refreshBtn = nullptr; QPushButton *changePassphraseBtn = nullptr; QPushButton *exportBtn = nullptr; QPushButton *genRevokeBtn = nullptr; void setupUi(QWidget *parent) { auto mainLayout = new QVBoxLayout{parent}; userIDs = new QWidget{parent}; { auto userIDsLayout = new QVBoxLayout{userIDs}; userIDsLayout->setContentsMargins({}); userIDTableLabel = new QLabel(i18n("User IDs:"), parent); userIDsLayout->addWidget(userIDTableLabel); userIDTable = new NavigatableTreeWidget{parent}; userIDTableLabel->setBuddy(userIDTable); userIDTable->setAccessibleName(i18n("User IDs")); QTreeWidgetItem *__qtreewidgetitem = new QTreeWidgetItem(); __qtreewidgetitem->setText(0, QString::fromUtf8("1")); userIDTable->setHeaderItem(__qtreewidgetitem); userIDTable->setEditTriggers(QAbstractItemView::NoEditTriggers); userIDTable->setSelectionMode(QAbstractItemView::ExtendedSelection); userIDTable->setRootIsDecorated(false); userIDTable->setUniformRowHeights(true); userIDTable->setAllColumnsShowFocus(false); userIDsLayout->addWidget(userIDTable); { auto buttonRow = new QHBoxLayout; addUserIDBtn = new QPushButton(i18nc("@action:button", "Add User ID"), parent); buttonRow->addWidget(addUserIDBtn); certifyBtn = new QPushButton(i18nc("@action:button", "Certify User IDs"), parent); buttonRow->addWidget(certifyBtn); webOfTrustBtn = new QPushButton(i18nc("@action:button", "Show Certifications"), parent); buttonRow->addWidget(webOfTrustBtn); revokeCertificationsBtn = new QPushButton(i18nc("@action:button", "Revoke Certifications"), parent); buttonRow->addWidget(revokeCertificationsBtn); revokeUserIDBtn = new QPushButton(i18nc("@action:button", "Revoke User ID"), parent); buttonRow->addWidget(revokeUserIDBtn); buttonRow->addStretch(1); userIDsLayout->addLayout(buttonRow); } userIDsLayout->addWidget(new KSeparator{Qt::Horizontal, parent}); } mainLayout->addWidget(userIDs); { auto gridLayout = new QGridLayout; gridLayout->setColumnStretch(1, 1); int row = -1; for (const auto &attribute : DN::attributeOrder()) { const auto attributeLabel = DN::attributeNameToLabel(attribute); if (attributeLabel.isEmpty()) { continue; } const auto labelWithColon = i18nc("interpunctation for labels", "%1:", attributeLabel); const auto & [it, inserted] = smimeAttributeFields.try_emplace(attribute, std::make_unique(labelWithColon, parent)); if (inserted) { row++; const auto &field = it->second; gridLayout->addWidget(field->label(), row, 0); gridLayout->addLayout(field->layout(), row, 1); } } row++; smimeTrustLevelField = std::make_unique(i18n("Trust level:"), parent); gridLayout->addWidget(smimeTrustLevelField->label(), row, 0); gridLayout->addLayout(smimeTrustLevelField->layout(), row, 1); row++; validFromField = std::make_unique(i18n("Valid from:"), parent); gridLayout->addWidget(validFromField->label(), row, 0); gridLayout->addLayout(validFromField->layout(), row, 1); row++; - expiresField = std::make_unique(i18n("Expires:"), parent); + expiresField = std::make_unique(i18n("Valid until:"), parent); changeExpirationAction = new QAction{parent}; changeExpirationAction->setIcon(QIcon::fromTheme(QStringLiteral("editor"))); - changeExpirationAction->setToolTip(i18nc("@info:tooltip", "Change the expiration date")); - changeExpirationAction->setProperty(accessibleNameProperty, i18nc("@action:button", "Change expiration")); + changeExpirationAction->setToolTip(i18nc("@info:tooltip", "Change the end of the validity period")); + changeExpirationAction->setProperty(accessibleNameProperty, i18nc("@action:button", "Change Validity")); expiresField->setAction(changeExpirationAction); gridLayout->addWidget(expiresField->label(), row, 0); gridLayout->addLayout(expiresField->layout(), row, 1); row++; fingerprintField = std::make_unique(i18n("Fingerprint:"), parent); if (QGuiApplication::clipboard()) { copyFingerprintAction = new QAction{parent}; copyFingerprintAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); copyFingerprintAction->setToolTip(i18nc("@info:tooltip", "Copy the fingerprint to the clipboard")); copyFingerprintAction->setProperty(accessibleNameProperty, i18nc("@action:button", "Copy fingerprint")); fingerprintField->setAction(copyFingerprintAction); } gridLayout->addWidget(fingerprintField->label(), row, 0); gridLayout->addLayout(fingerprintField->layout(), row, 1); row++; smimeIssuerField = std::make_unique(i18n("Issuer:"), parent); showIssuerCertificateAction = new QAction{parent}; showIssuerCertificateAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); showIssuerCertificateAction->setToolTip(i18nc("@info:tooltip", "Show the issuer certificate")); showIssuerCertificateAction->setProperty(accessibleNameProperty, i18nc("@action:button", "Show certificate")); smimeIssuerField->setAction(showIssuerCertificateAction); gridLayout->addWidget(smimeIssuerField->label(), row, 0); gridLayout->addLayout(smimeIssuerField->layout(), row, 1); row++; complianceField = std::make_unique(i18n("Compliance:"), parent); gridLayout->addWidget(complianceField->label(), row, 0); gridLayout->addLayout(complianceField->layout(), row, 1); row++; trustedIntroducerField = std::make_unique(i18n("Trusted introducer for:"), parent); gridLayout->addWidget(trustedIntroducerField->label(), row, 0); trustedIntroducerField->setToolTip(i18n("See certifications for details.")); gridLayout->addLayout(trustedIntroducerField->layout(), row, 1); mainLayout->addLayout(gridLayout); } smimeRelatedAddresses = new QLabel(i18n("Related addresses:"), parent); mainLayout->addWidget(smimeRelatedAddresses); smimeAddressList = new QListWidget{parent}; smimeRelatedAddresses->setBuddy(smimeAddressList); smimeAddressList->setAccessibleName(i18n("Related addresses")); smimeAddressList->setEditTriggers(QAbstractItemView::NoEditTriggers); smimeAddressList->setSelectionMode(QAbstractItemView::SingleSelection); mainLayout->addWidget(smimeAddressList); mainLayout->addStretch(); { auto buttonRow = new QHBoxLayout; moreDetailsBtn = new QPushButton(i18nc("@action:button", "More Details..."), parent); buttonRow->addWidget(moreDetailsBtn); trustChainDetailsBtn = new QPushButton(i18nc("@action:button", "Trust Chain Details"), parent); buttonRow->addWidget(trustChainDetailsBtn); refreshBtn = new QPushButton{i18nc("@action:button", "Update"), parent}; #ifndef QGPGME_SUPPORTS_KEY_REFRESH refreshBtn->setVisible(false); #endif buttonRow->addWidget(refreshBtn); exportBtn = new QPushButton(i18nc("@action:button", "Export"), parent); buttonRow->addWidget(exportBtn); changePassphraseBtn = new QPushButton(i18nc("@action:button", "Change Passphrase"), parent); buttonRow->addWidget(changePassphraseBtn); genRevokeBtn = new QPushButton(i18nc("@action:button", "Generate Revocation Certificate"), parent); genRevokeBtn->setToolTip(u"" % i18n("A revocation certificate is a file that serves as a \"kill switch\" to publicly " "declare that a key shall not anymore be used. It is not possible " "to retract such a revocation certificate once it has been published.") % u""); buttonRow->addWidget(genRevokeBtn); buttonRow->addStretch(1); mainLayout->addLayout(buttonRow); } } } ui; }; CertificateDetailsWidget::Private::Private(CertificateDetailsWidget *qq) : q{qq} { ui.setupUi(q); ui.userIDTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui.userIDTable, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) { userIDTableContextMenuRequested(p); }); connect(ui.userIDTable, &QTreeWidget::itemSelectionChanged, q, [this]() { updateUserIDActions(); }); connect(ui.addUserIDBtn, &QPushButton::clicked, q, [this]() { addUserID(); }); connect(ui.revokeUserIDBtn, &QPushButton::clicked, q, [this]() { revokeSelectedUserID(); }); connect(ui.changePassphraseBtn, &QPushButton::clicked, q, [this]() { changePassphrase(); }); connect(ui.genRevokeBtn, &QPushButton::clicked, q, [this]() { genRevokeCert(); }); connect(ui.changeExpirationAction, &QAction::triggered, q, [this]() { changeExpiration(); }); connect(ui.showIssuerCertificateAction, &QAction::triggered, q, [this]() { showIssuerCertificate(); }); connect(ui.trustChainDetailsBtn, &QPushButton::pressed, q, [this]() { showTrustChainDialog(); }); connect(ui.moreDetailsBtn, &QPushButton::pressed, q, [this]() { showMoreDetails(); }); connect(ui.refreshBtn, &QPushButton::clicked, q, [this]() { refreshCertificate(); }); connect(ui.certifyBtn, &QPushButton::clicked, q, [this]() { certifyUserIDs(); }); connect(ui.revokeCertificationsBtn, &QPushButton::clicked, q, [this]() { revokeCertifications(); }); connect(ui.webOfTrustBtn, &QPushButton::clicked, q, [this]() { webOfTrustClicked(); }); connect(ui.exportBtn, &QPushButton::clicked, q, [this]() { exportClicked(); }); if (ui.copyFingerprintAction) { connect(ui.copyFingerprintAction, &QAction::triggered, q, [this]() { copyFingerprintToClipboard(); }); } connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { keysMayHaveChanged(); }); } void CertificateDetailsWidget::Private::setupCommonProperties() { const bool isOpenPGP = key.protocol() == GpgME::OpenPGP; const bool isSMIME = key.protocol() == GpgME::CMS; const bool isOwnKey = key.hasSecret(); // update visibility of UI elements ui.userIDs->setVisible(isOpenPGP); ui.addUserIDBtn->setVisible(isOwnKey); // ui.certifyBtn->setVisible(true); // always visible (for OpenPGP keys) // ui.webOfTrustBtn->setVisible(true); // always visible (for OpenPGP keys) ui.revokeCertificationsBtn->setVisible(Kleo::Commands::RevokeCertificationCommand::isSupported()); ui.revokeUserIDBtn->setVisible(isOwnKey); for (const auto &[_, field] : ui.smimeAttributeFields) { field->setVisible(isSMIME); } ui.smimeTrustLevelField->setVisible(isSMIME); // ui.validFromField->setVisible(true); // always visible // ui.expiresField->setVisible(true); // always visible if (isOpenPGP && isOwnKey) { ui.expiresField->setAction(ui.changeExpirationAction); } else { ui.expiresField->setAction(nullptr); } // ui.fingerprintField->setVisible(true); // always visible ui.smimeIssuerField->setVisible(isSMIME); ui.complianceField->setVisible(DeVSCompliance::isCompliant()); ui.trustedIntroducerField->setVisible(isOpenPGP); // may be hidden again by setupPGPProperties() ui.smimeRelatedAddresses->setVisible(isSMIME); ui.smimeAddressList->setVisible(isSMIME); // ui.moreDetailsBtn->setVisible(true); // always visible ui.trustChainDetailsBtn->setVisible(isSMIME); // ui.refreshBtn->setVisible(true); // always visible ui.changePassphraseBtn->setVisible(isSecretKeyStoredInKeyRing(key)); // ui.exportBtn->setVisible(true); // always visible ui.genRevokeBtn->setVisible(isOpenPGP && isOwnKey); // update availability of buttons const auto userCanSignUserIDs = userHasCertificationKey(); ui.addUserIDBtn->setEnabled(canBeUsedForSecretKeyOperations(key)); ui.certifyBtn->setEnabled(userCanSignUserIDs); ui.revokeCertificationsBtn->setEnabled(userCanSignUserIDs); ui.revokeUserIDBtn->setEnabled(false); // requires a selected user ID ui.changeExpirationAction->setEnabled(canBeUsedForSecretKeyOperations(key)); ui.changePassphraseBtn->setEnabled(isSecretKeyStoredInKeyRing(key)); ui.genRevokeBtn->setEnabled(canBeUsedForSecretKeyOperations(key)); // update values of protocol-independent UI elements ui.validFromField->setValue(Formatting::creationDateString(key), Formatting::accessibleCreationDate(key)); - ui.expiresField->setValue(Formatting::expirationDateString(key, i18nc("Expires", "never")), - Formatting::accessibleExpirationDate(key, i18nc("Expires", "never"))); + ui.expiresField->setValue(Formatting::expirationDateString(key, i18nc("Valid until:", "unlimited")), + Formatting::accessibleExpirationDate(key)); ui.fingerprintField->setValue(Formatting::prettyID(key.primaryFingerprint()), Formatting::accessibleHexID(key.primaryFingerprint())); if (DeVSCompliance::isCompliant()) { ui.complianceField->setValue(Kleo::Formatting::complianceStringForKey(key)); } } void CertificateDetailsWidget::Private::updateUserIDActions() { const auto userIDs = selectedUserIDs(ui.userIDTable); ui.revokeUserIDBtn->setEnabled((userIDs.size() == 1) && canCreateCertifications(key) && canRevokeUserID(userIDs.front())); } void CertificateDetailsWidget::Private::setUpUserIDTable() { ui.userIDTable->clear(); QStringList headers = { i18n("Email"), i18n("Name"), i18n("Trust Level"), i18n("Tags") }; ui.userIDTable->setColumnCount(headers.count()); ui.userIDTable->setColumnWidth(0, 200); ui.userIDTable->setColumnWidth(1, 200); ui.userIDTable->setHeaderLabels(headers); const auto uids = key.userIDs(); for (unsigned int i = 0; i < uids.size(); ++i) { const auto &uid = uids[i]; auto item = new QTreeWidgetItem; const QString toolTip = tofuTooltipString(uid); item->setData(0, Qt::UserRole, QVariant::fromValue(uid)); auto pMail = Kleo::Formatting::prettyEMail(uid); auto pName = Kleo::Formatting::prettyName(uid); item->setData(0, Qt::DisplayRole, pMail); item->setData(0, Qt::ToolTipRole, toolTip); item->setData(0, Qt::AccessibleTextRole, pMail.isEmpty() ? i18nc("text for screen readers for an empty email address", "no email") : pMail); item->setData(1, Qt::DisplayRole, pName); item->setData(1, Qt::ToolTipRole, toolTip); item->setData(2, Qt::DecorationRole, trustLevelIcon(uid)); item->setData(2, Qt::DisplayRole, trustLevelText(uid)); item->setData(2, Qt::ToolTipRole, toolTip); GpgME::Error err; QStringList tagList; for (const auto &tag: uid.remarks(Tags::tagKeys(), err)) { if (err) { qCWarning(KLEOPATRA_LOG) << "Getting remarks for user ID" << uid.id() << "failed:" << err; } tagList << QString::fromStdString(tag); } qCDebug(KLEOPATRA_LOG) << "tagList:" << tagList; const auto tags = tagList.join(QStringLiteral("; ")); item->setData(3, Qt::DisplayRole, tags); item->setData(3, Qt::ToolTipRole, toolTip); ui.userIDTable->addTopLevelItem(item); } if (!Tags::tagsEnabled()) { ui.userIDTable->hideColumn(3); } } void CertificateDetailsWidget::Private::setUpSMIMEAdressList() { ui.smimeAddressList->clear(); const auto *const emailField = attributeField(QStringLiteral("EMAIL")); // add email address from primary user ID if not listed already as attribute field if (!emailField) { const auto ownerId = key.userID(0); const Kleo::DN dn(ownerId.id()); const QString dnEmail = dn[QStringLiteral("EMAIL")]; if (!dnEmail.isEmpty()) { ui.smimeAddressList->addItem(dnEmail); } } if (key.numUserIDs() > 1) { // iterate over the secondary user IDs #ifdef USE_RANGES for (const auto uids = key.userIDs(); const auto &uid : std::ranges::subrange(std::next(uids.begin()), uids.end())) { #else const auto uids = key.userIDs(); for (auto it = std::next(uids.begin()); it != uids.end(); ++it) { const auto &uid = *it; #endif const auto name = Kleo::Formatting::prettyName(uid); const auto email = Kleo::Formatting::prettyEMail(uid); QString itemText; if (name.isEmpty() && !email.isEmpty()) { // skip email addresses already listed in email attribute field if (emailField && email == emailField->value()) { continue; } itemText = email; } else { // S/MIME certificates sometimes contain urls where both // name and mail is empty. In that case we print whatever // the uid is as name. // // Can be ugly like (3:uri24:http://ca.intevation.org), but // this is better then showing an empty entry. itemText = QString::fromUtf8(uid.id()); } // avoid duplicate entries in the list if (ui.smimeAddressList->findItems(itemText, Qt::MatchExactly).empty()) { ui.smimeAddressList->addItem(itemText); } } } if (ui.smimeAddressList->count() == 0) { ui.smimeRelatedAddresses->setVisible(false); ui.smimeAddressList->setVisible(false); } } void CertificateDetailsWidget::Private::revokeUserID(const GpgME::UserID &userId) { const QString message = xi18nc( "@info", "Do you really want to revoke the user ID%1 ?", QString::fromUtf8(userId.id())); auto confirmButton = KStandardGuiItem::yes(); confirmButton.setText(i18nc("@action:button", "Revoke User ID")); confirmButton.setToolTip({}); const auto choice = KMessageBox::questionYesNo( q->window(), message, i18nc("@title:window", "Confirm Revocation"), confirmButton, KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::WindowModal); if (choice != KMessageBox::Yes) { return; } auto cmd = new Commands::RevokeUserIDCommand(userId); cmd->setParentWidget(q); connect(cmd, &Command::finished, q, [this]() { ui.userIDTable->setEnabled(true); // the Revoke User ID button will be updated by the key update updateKey(); }); ui.userIDTable->setEnabled(false); ui.revokeUserIDBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::revokeSelectedUserID() { const auto userIDs = selectedUserIDs(ui.userIDTable); if (userIDs.size() != 1) { return; } revokeUserID(userIDs.front()); } void CertificateDetailsWidget::Private::changeExpiration() { auto cmd = new Kleo::Commands::ChangeExpiryCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangeExpiryCommand::finished, q, [this]() { ui.changeExpirationAction->setEnabled(true); }); ui.changeExpirationAction->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::changePassphrase() { auto cmd = new Kleo::Commands::ChangePassphraseCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangePassphraseCommand::finished, q, [this]() { ui.changePassphraseBtn->setEnabled(true); }); ui.changePassphraseBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::genRevokeCert() { auto cmd = new Kleo::Commands::GenRevokeCommand(key); QObject::connect(cmd, &Kleo::Commands::GenRevokeCommand::finished, q, [this]() { ui.genRevokeBtn->setEnabled(true); }); ui.genRevokeBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::refreshCertificate() { auto cmd = new Kleo::RefreshCertificateCommand{key}; QObject::connect(cmd, &Kleo::RefreshCertificateCommand::finished, q, [this]() { ui.refreshBtn->setEnabled(true); }); ui.refreshBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::certifyUserIDs() { const auto userIDs = selectedUserIDs(ui.userIDTable); auto cmd = userIDs.empty() ? new Kleo::Commands::CertifyCertificateCommand{key} // : new Kleo::Commands::CertifyCertificateCommand{userIDs}; QObject::connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this]() { updateKey(); ui.certifyBtn->setEnabled(true); }); ui.certifyBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::revokeCertifications() { const auto userIDs = selectedUserIDs(ui.userIDTable); auto cmd = userIDs.empty() ? new Kleo::Commands::RevokeCertificationCommand{key} // : new Kleo::Commands::RevokeCertificationCommand{userIDs}; QObject::connect(cmd, &Kleo::Command::finished, q, [this]() { updateKey(); ui.revokeCertificationsBtn->setEnabled(true); }); ui.revokeCertificationsBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::webOfTrustClicked() { QScopedPointer dlg(new WebOfTrustDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::exportClicked() { QScopedPointer dlg(new ExportDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::addUserID() { auto cmd = new Kleo::Commands::AddUserIDCommand(key); QObject::connect(cmd, &Kleo::Commands::AddUserIDCommand::finished, q, [this]() { ui.addUserIDBtn->setEnabled(true); updateKey(); }); ui.addUserIDBtn->setEnabled(false); cmd->start(); } namespace { void ensureThatKeyDetailsAreLoaded(GpgME::Key &key) { if (key.userID(0).numSignatures() == 0) { key.update(); } } } void CertificateDetailsWidget::Private::keysMayHaveChanged() { auto newKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint()); if (!newKey.isNull()) { ensureThatKeyDetailsAreLoaded(newKey); setUpdatedKey(newKey); } } void CertificateDetailsWidget::Private::showTrustChainDialog() { QScopedPointer dlg(new TrustChainDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::userIDTableContextMenuRequested(const QPoint &p) { const auto userIDs = selectedUserIDs(ui.userIDTable); const auto singleUserID = (userIDs.size() == 1) ? userIDs.front() : GpgME::UserID{}; const bool canSignUserIDs = userHasCertificationKey(); auto menu = new QMenu(q); { const auto actionText = userIDs.empty() ? i18nc("@action:inmenu", "Certify User IDs...") : i18ncp("@action:inmenu", "Certify User ID...", "Certify User IDs...", userIDs.size()); auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-sign")), actionText, q, [this]() { certifyUserIDs(); }); action->setEnabled(canSignUserIDs); } if (Kleo::Commands::RevokeCertificationCommand::isSupported()) { const auto actionText = userIDs.empty() ? i18nc("@action:inmenu", "Revoke Certifications...") : i18ncp("@action:inmenu", "Revoke Certification...", "Revoke Certifications...", userIDs.size()); auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), actionText, q, [this]() { revokeCertifications(); }); action->setEnabled(canSignUserIDs); } #ifdef MAILAKONADI_ENABLED if (key.hasSecret()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18nc("@action:inmenu", "Publish at Mail Provider ..."), q, [this, singleUserID]() { auto cmd = new Kleo::Commands::ExportOpenPGPCertToProviderCommand(singleUserID); ui.userIDTable->setEnabled(false); connect(cmd, &Kleo::Commands::ExportOpenPGPCertToProviderCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); }); cmd->start(); }); action->setEnabled(!singleUserID.isNull()); } #endif // MAILAKONADI_ENABLED { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), i18nc("@action:inmenu", "Revoke User ID"), q, [this, singleUserID]() { revokeUserID(singleUserID); }); action->setEnabled(!singleUserID.isNull() && canCreateCertifications(key) && canRevokeUserID(singleUserID)); } connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); menu->popup(ui.userIDTable->viewport()->mapToGlobal(p)); } void CertificateDetailsWidget::Private::showMoreDetails() { ui.moreDetailsBtn->setEnabled(false); if (key.protocol() == GpgME::CMS) { auto cmd = new Kleo::Commands::DumpCertificateCommand(key); connect(cmd, &Kleo::Commands::DumpCertificateCommand::finished, q, [this]() { ui.moreDetailsBtn->setEnabled(true); }); cmd->setUseDialog(true); cmd->start(); } else { QScopedPointer dlg(new SubKeysDialog(q)); dlg->setKey(key); dlg->exec(); ui.moreDetailsBtn->setEnabled(true); } } QString CertificateDetailsWidget::Private::tofuTooltipString(const GpgME::UserID &uid) const { const auto tofu = uid.tofuInfo(); if (tofu.isNull()) { return QString(); } QString html = QStringLiteral(""); const auto appendRow = [&html](const QString &lbl, const QString &val) { html += QStringLiteral("" "" "" "") .arg(lbl, val); }; const auto appendHeader = [this, &html](const QString &hdr) { html += QStringLiteral("") .arg(q->palette().highlight().color().name(), q->palette().highlightedText().color().name(), hdr); }; const auto dateTime = [](long ts) { QLocale l; return ts == 0 ? i18n("never") : l.toString(QDateTime::fromSecsSinceEpoch(ts), QLocale::ShortFormat); }; appendHeader(i18n("Signing")); appendRow(i18n("First message"), dateTime(tofu.signFirst())); appendRow(i18n("Last message"), dateTime(tofu.signLast())); appendRow(i18n("Message count"), QString::number(tofu.signCount())); appendHeader(i18n("Encryption")); appendRow(i18n("First message"), dateTime(tofu.encrFirst())); appendRow(i18n("Last message"), dateTime(tofu.encrLast())); appendRow(i18n("Message count"), QString::number(tofu.encrCount())); html += QStringLiteral("
%1:%2
%3
"); // Make sure the tooltip string is different for each UserID, even if the // data are the same, otherwise the tooltip is not updated and moved when // user moves mouse from one row to another. html += QStringLiteral("").arg(QString::fromUtf8(uid.id())); return html; } QIcon CertificateDetailsWidget::Private::trustLevelIcon(const GpgME::UserID &uid) const { if (updateInProgress) { return QIcon::fromTheme(QStringLiteral("emblem-question")); } switch (uid.validity()) { case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: return QIcon::fromTheme(QStringLiteral("emblem-question")); case GpgME::UserID::Never: return QIcon::fromTheme(QStringLiteral("emblem-error")); case GpgME::UserID::Marginal: return QIcon::fromTheme(QStringLiteral("emblem-warning")); case GpgME::UserID::Full: case GpgME::UserID::Ultimate: return QIcon::fromTheme(QStringLiteral("emblem-success")); } return {}; } QString CertificateDetailsWidget::Private::trustLevelText(const GpgME::UserID &uid) const { return updateInProgress ? i18n("Updating...") : Formatting::validityShort(uid); } namespace { auto isGood(const GpgME::UserID::Signature &signature) { return signature.status() == GpgME::UserID::Signature::NoError && !signature.isInvalid() && 0x10 <= signature.certClass() && signature.certClass() <= 0x13; } auto accumulateTrustDomains(const std::vector &signatures) { return std::accumulate( std::begin(signatures), std::end(signatures), std::set(), [] (auto domains, const auto &signature) { if (isGood(signature) && signature.isTrustSignature()) { domains.insert(Formatting::trustSignatureDomain(signature)); } return domains; } ); } auto accumulateTrustDomains(const std::vector &userIds) { return std::accumulate( std::begin(userIds), std::end(userIds), std::set(), [] (auto domains, const auto &userID) { const auto newDomains = accumulateTrustDomains(userID.signatures()); std::copy(std::begin(newDomains), std::end(newDomains), std::inserter(domains, std::end(domains))); return domains; } ); } } void CertificateDetailsWidget::Private::setupPGPProperties() { setUpUserIDTable(); const auto trustDomains = accumulateTrustDomains(key.userIDs()); ui.trustedIntroducerField->setVisible(!trustDomains.empty()); ui.trustedIntroducerField->setValue(QStringList(std::begin(trustDomains), std::end(trustDomains)).join(u", ")); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the key from external sources.")); } static QString formatDNToolTip(const Kleo::DN &dn) { QString html = QStringLiteral(""); const auto appendRow = [&html, dn](const QString &lbl, const QString &attr) { const QString val = dn[attr]; if (!val.isEmpty()) { html += QStringLiteral( "" "" "").arg(lbl, val); } }; appendRow(i18n("Common Name"), QStringLiteral("CN")); appendRow(i18n("Organization"), QStringLiteral("O")); appendRow(i18n("Street"), QStringLiteral("STREET")); appendRow(i18n("City"), QStringLiteral("L")); appendRow(i18n("State"), QStringLiteral("ST")); appendRow(i18n("Country"), QStringLiteral("C")); html += QStringLiteral("
%1:%2
"); return html; } void CertificateDetailsWidget::Private::setupSMIMEProperties() { const auto ownerId = key.userID(0); const Kleo::DN dn(ownerId.id()); for (const auto &[attributeName, field] : ui.smimeAttributeFields) { const QString attributeValue = dn[attributeName]; field->setValue(attributeValue); field->setVisible(!attributeValue.isEmpty()); } ui.smimeTrustLevelField->setIcon(trustLevelIcon(ownerId)); ui.smimeTrustLevelField->setValue(trustLevelText(ownerId)); const Kleo::DN issuerDN(key.issuerName()); const QString issuerCN = issuerDN[QStringLiteral("CN")]; const QString issuer = issuerCN.isEmpty() ? QString::fromUtf8(key.issuerName()) : issuerCN; ui.smimeIssuerField->setValue(issuer); ui.smimeIssuerField->setToolTip(formatDNToolTip(issuerDN)); ui.showIssuerCertificateAction->setEnabled(!key.isRoot()); setUpSMIMEAdressList(); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the CRLs and do a full validation check of the certificate.")); } void CertificateDetailsWidget::Private::showIssuerCertificate() { // there is either one or no parent key const auto parentKeys = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); if (parentKeys.empty()) { KMessageBox::error(q, i18n("The issuer certificate could not be found locally.")); return; } auto cmd = new Kleo::Commands::DetailsCommand(parentKeys.front(), nullptr); cmd->setParentWidget(q); cmd->start(); } void CertificateDetailsWidget::Private::copyFingerprintToClipboard() { if (auto clipboard = QGuiApplication::clipboard()) { clipboard->setText(QString::fromLatin1(key.primaryFingerprint())); } } CertificateDetailsWidget::CertificateDetailsWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } CertificateDetailsWidget::~CertificateDetailsWidget() = default; void CertificateDetailsWidget::Private::keyListDone(const GpgME::KeyListResult &, const std::vector &keys, const QString &, const GpgME::Error &) { updateInProgress = false; if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Invalid keylist result in update."; return; } // As we listen for keysmayhavechanged we get the update // after updating the keycache. KeyCache::mutableInstance()->insert(keys); } void CertificateDetailsWidget::Private::updateKey() { key.update(); setUpdatedKey(key); } void CertificateDetailsWidget::Private::setUpdatedKey(const GpgME::Key &k) { key = k; setupCommonProperties(); if (key.protocol() == GpgME::OpenPGP) { setupPGPProperties(); } else { setupSMIMEProperties(); } } void CertificateDetailsWidget::setKey(const GpgME::Key &key) { if (key.protocol() == GpgME::CMS) { // For everything but S/MIME this should be quick // and we don't need to show another status. d->updateInProgress = true; } d->setUpdatedKey(key); // Run a keylistjob with full details (TOFU / Validate) QGpgME::KeyListJob *job = key.protocol() == GpgME::OpenPGP ? QGpgME::openpgp()->keyListJob(false, true, true) : QGpgME::smime()->keyListJob(false, true, true); auto ctx = QGpgME::Job::context(job); ctx->addKeyListMode(GpgME::WithTofu); ctx->addKeyListMode(GpgME::SignatureNotations); if (key.hasSecret()) { ctx->addKeyListMode(GpgME::WithSecret); } // Windows QGpgME new style connect problem makes this necessary. connect(job, SIGNAL(result(GpgME::KeyListResult,std::vector,QString,GpgME::Error)), this, SLOT(keyListDone(GpgME::KeyListResult,std::vector,QString,GpgME::Error))); job->start(QStringList() << QLatin1String(key.primaryFingerprint())); } GpgME::Key CertificateDetailsWidget::key() const { return d->key; } #include "moc_certificatedetailswidget.cpp" diff --git a/src/dialogs/expirydialog.cpp b/src/dialogs/expirydialog.cpp index c98f27496..d0b1d2a3c 100644 --- a/src/dialogs/expirydialog.cpp +++ b/src/dialogs/expirydialog.cpp @@ -1,354 +1,354 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/expirydialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "expirydialog.h" #include "utils/gui-helper.h" #include "utils/qt-cxx20-compat.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Dialogs; namespace { enum Period { Days, Weeks, Months, Years, NumPeriods }; static QDate date_by_amount_and_unit(int inAmount, int inUnit) { const QDate current = QDate::currentDate(); switch (inUnit) { case Days: return current.addDays(inAmount); case Weeks: return current.addDays(7 * inAmount); case Months: return current.addMonths(inAmount); case Years: return current.addYears(inAmount); default: Q_ASSERT(!"Should not reach here"); } return QDate(); } static QString accessibleValidityDuration(int amount, Period unit) { switch (unit) { case Days: - return i18ncp("(Expires) in x day(s)", "in %1 day", "in %1 days", amount); + return i18np("Valid for %1 day", "Valid for %1 days", amount); case Weeks: - return i18ncp("(Expires) in x week(s)", "in %1 week", "in %1 weeks", amount); + return i18np("Valid for %1 week", "Valid for %1 weeks", amount); case Months: - return i18ncp("(Expires) in x month(s)", "in %1 month", "in %1 months", amount); + return i18np("Valid for %1 month", "Valid for %1 months", amount); case Years: - return i18ncp("(Expires) in x year(s)", "in %1 year", "in %1 years", amount); + return i18np("Valid for %1 year", "Valid for %1 years", amount); default: Q_ASSERT(!"invalid unit"); } return {}; } // these calculations should be precise enough for the forseeable future... static const double DAYS_IN_GREGORIAN_YEAR = 365.2425; static int monthsBetween(const QDate &d1, const QDate &d2) { const int days = d1.daysTo(d2); return qRound(days / DAYS_IN_GREGORIAN_YEAR * 12); } static int yearsBetween(const QDate &d1, const QDate &d2) { const int days = d1.daysTo(d2); return qRound(days / DAYS_IN_GREGORIAN_YEAR); } } class ExpiryDialog::Private { friend class ::Kleo::Dialogs::ExpiryDialog; ExpiryDialog *const q; public: explicit Private(Mode mode, ExpiryDialog *qq) : q{qq} , mode{mode} , inUnit{Days} , ui{mode, q} { #if QT_DEPRECATED_SINCE(5, 14) connect(ui.inSB, qOverload(&QSpinBox::valueChanged), q, [this] () { slotInAmountChanged(); }); #else connect(ui.inSB, &QSpinBox::valueChanged, q, [this] () { slotInAmountChanged(); }); #endif connect(ui.inCB, qOverload(&QComboBox::currentIndexChanged), q, [this] () { slotInUnitChanged(); }); connect(ui.onCB, &KDateComboBox::dateChanged, q, [this] () { slotOnDateChanged(); }); Q_ASSERT(ui.inCB->currentIndex() == inUnit); } private: void slotInAmountChanged(); void slotInUnitChanged(); void slotOnDateChanged(); private: QDate inDate() const; int inAmountByDate(const QDate &date) const; void setInitialFocus(); private: ExpiryDialog::Mode mode; int inUnit; bool initialFocusWasSet = false; struct UI { QRadioButton *neverRB; QRadioButton *inRB; QSpinBox *inSB; QComboBox *inCB; QRadioButton *onRB; KDateComboBox *onCB; QCheckBox *updateSubkeysCheckBox; explicit UI(Mode mode, Dialogs::ExpiryDialog *qq) { auto mainLayout = new QVBoxLayout{qq}; auto mainWidget = new QWidget{qq}; auto vboxLayout = new QVBoxLayout{mainWidget}; vboxLayout->setContentsMargins(0, 0, 0, 0); { auto label = new QLabel{qq}; label->setText(mode == Mode::UpdateIndividualSubkey ? - i18n("Please select when to expire this subkey:") : - i18n("Please select when to expire this certificate:")); + i18n("Please select until when the subkey should be valid:") : + i18n("Please select until when the certificate should be valid:")); vboxLayout->addWidget(label); } - neverRB = new QRadioButton(i18n("Ne&ver"), mainWidget); + neverRB = new QRadioButton(i18n("Unlimited validity"), mainWidget); neverRB->setChecked(false); vboxLayout->addWidget(neverRB); { auto hboxLayout = new QHBoxLayout; - inRB = new QRadioButton{i18n("In"), mainWidget}; + inRB = new QRadioButton{i18n("Valid for:"), mainWidget}; inRB->setChecked(false); hboxLayout->addWidget(inRB); inSB = new QSpinBox{mainWidget}; inSB->setEnabled(false); inSB->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); inSB->setMinimum(1); hboxLayout->addWidget(inSB); inCB = new QComboBox{mainWidget}; inCB->addItem(i18n("Days")); inCB->addItem(i18n("Weeks")); inCB->addItem(i18n("Months")); inCB->addItem(i18n("Years")); Q_ASSERT(inCB->count() == NumPeriods); inCB->setEnabled(false); hboxLayout->addWidget(inCB); hboxLayout->addStretch(1); vboxLayout->addLayout(hboxLayout); } { auto hboxLayout = new QHBoxLayout; - onRB = new QRadioButton{i18n("On this da&y:"), mainWidget}; + onRB = new QRadioButton{i18n("Valid until:"), mainWidget}; onRB->setChecked(true); hboxLayout->addWidget(onRB); onCB = new KDateComboBox{mainWidget}; onCB->setMinimumDate(QDate::currentDate().addDays(1)); hboxLayout->addWidget(onCB); hboxLayout->addStretch(1); vboxLayout->addLayout(hboxLayout); } { - updateSubkeysCheckBox = new QCheckBox{i18n("Also update the expiry of the subkeys"), qq}; + updateSubkeysCheckBox = new QCheckBox{i18n("Also update the validity period of the subkeys"), qq}; #ifdef QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY updateSubkeysCheckBox->setVisible(mode == Mode::UpdateCertificateWithSubkeys); #else updateSubkeysCheckBox->setVisible(false); #endif vboxLayout->addWidget(updateSubkeysCheckBox); } vboxLayout->addStretch(1); mainLayout->addWidget(mainWidget); auto buttonBox = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel, qq}; auto okButton = buttonBox->button(QDialogButtonBox::Ok); KGuiItem::assign(okButton, KStandardGuiItem::ok()); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); qq->connect(buttonBox, &QDialogButtonBox::accepted, qq, &QDialog::accept); qq->connect(buttonBox, &QDialogButtonBox::rejected, qq, &QDialog::reject); mainLayout->addWidget(buttonBox); connect(onRB, &QRadioButton::toggled, onCB, &QWidget::setEnabled); connect(inRB, &QRadioButton::toggled, inCB, &QWidget::setEnabled); connect(inRB, &QRadioButton::toggled, inSB, &QWidget::setEnabled); } } ui; }; void ExpiryDialog::Private::slotInUnitChanged() { const int oldInAmount = ui.inSB->value(); const QDate targetDate = date_by_amount_and_unit(oldInAmount, inUnit); inUnit = ui.inCB->currentIndex(); if (targetDate.isValid()) { ui.inSB->setValue(inAmountByDate(targetDate)); } else { slotInAmountChanged(); } } void ExpiryDialog::Private::slotInAmountChanged() { if (ui.inRB->isChecked()) { ui.onCB->setDate(inDate()); } ui.inRB->setAccessibleName(accessibleValidityDuration(ui.inSB->value(), static_cast(ui.inCB->currentIndex()))); } void ExpiryDialog::Private::slotOnDateChanged() { if (ui.onRB->isChecked()) { ui.inSB->setValue(inAmountByDate(ui.onCB->date())); } - ui.onRB->setAccessibleName(i18nc("(Expires) on DATE", "on %1", Formatting::accessibleDate(ui.onCB->date()))); + ui.onRB->setAccessibleName(i18nc("Valid until DATE", "Valid until %1", Formatting::accessibleDate(ui.onCB->date()))); } QDate ExpiryDialog::Private::inDate() const { return date_by_amount_and_unit(ui.inSB->value(), ui.inCB->currentIndex()); } int ExpiryDialog::Private::inAmountByDate(const QDate &selected) const { const QDate current = QDate::currentDate(); switch (ui.inCB->currentIndex()) { case Days: return current.daysTo(selected); case Weeks: return qRound(current.daysTo(selected) / 7.0); case Months: return monthsBetween(current, selected); case Years: return yearsBetween(current, selected); }; Q_ASSERT(!"Should not reach here"); return -1; } void ExpiryDialog::Private::setInitialFocus() { if (initialFocusWasSet) { return; } // give focus to the checked radio button (void) focusFirstCheckedButton({ui.neverRB, ui.inRB, ui.onRB}); initialFocusWasSet = true; } ExpiryDialog::ExpiryDialog(Mode mode, QWidget *p) : QDialog{p} , d{new Private{mode, this}} { - setWindowTitle(i18nc("@title:window", "Change Expiry")); + setWindowTitle(i18nc("@title:window", "Change Validity Period")); } ExpiryDialog::~ExpiryDialog() = default; void ExpiryDialog::setDateOfExpiry(const QDate &date) { const QDate current = QDate::currentDate(); if (date.isValid()) { d->ui.onRB->setChecked(true); d->ui.onCB->setDate(qMax(date, current)); } else { d->ui.neverRB->setChecked(true); d->ui.onCB->setDate(current); d->ui.inSB->setValue(0); } } QDate ExpiryDialog::dateOfExpiry() const { return d->ui.inRB->isChecked() ? d->inDate() : d->ui.onRB->isChecked() ? d->ui.onCB->date() : QDate{}; } void ExpiryDialog::setUpdateExpirationOfAllSubkeys(bool update) { d->ui.updateSubkeysCheckBox->setChecked(update); } bool ExpiryDialog::updateExpirationOfAllSubkeys() const { return d->ui.updateSubkeysCheckBox->isChecked(); } void ExpiryDialog::showEvent(QShowEvent *event) { d->setInitialFocus(); QDialog::showEvent(event); } #include "moc_expirydialog.cpp" diff --git a/src/dialogs/subkeyswidget.cpp b/src/dialogs/subkeyswidget.cpp index 39b7d857b..dd4d9f116 100644 --- a/src/dialogs/subkeyswidget.cpp +++ b/src/dialogs/subkeyswidget.cpp @@ -1,322 +1,322 @@ /* dialogs/subkeyswidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "subkeyswidget.h" #include "commands/changeexpirycommand.h" #ifdef QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT #include "commands/exportsecretsubkeycommand.h" #endif #include "commands/keytocardcommand.h" #include "commands/importpaperkeycommand.h" #include "exportdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(GpgME::Subkey) using namespace Kleo; using namespace Kleo::Commands; class SubKeysWidget::Private { SubKeysWidget *const q; public: Private(SubKeysWidget *qq) : q{qq} , ui{qq} { ui.subkeysTree->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui.subkeysTree, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) { tableContextMenuRequested(p); }); } private: void tableContextMenuRequested(const QPoint &p); public: GpgME::Key key; public: struct UI { QVBoxLayout *mainLayout; NavigatableTreeWidget *subkeysTree; UI(QWidget *widget) { mainLayout = new QVBoxLayout{widget}; mainLayout->setContentsMargins(0, 0, 0, 0); auto subkeysTreeLabel = new QLabel{i18nc("@label", "Subkeys:"), widget}; mainLayout->addWidget(subkeysTreeLabel); subkeysTree = new NavigatableTreeWidget{widget}; subkeysTreeLabel->setBuddy(subkeysTree); subkeysTree->setAccessibleName(i18nc("@label", "Subkeys")); subkeysTree->setRootIsDecorated(false); subkeysTree->setHeaderLabels({ i18nc("@title:column", "ID"), i18nc("@title:column", "Type"), i18nc("@title:column", "Valid From"), i18nc("@title:column", "Valid Until"), i18nc("@title:column", "Status"), i18nc("@title:column", "Strength"), i18nc("@title:column", "Usage"), i18nc("@title:column", "Primary"), i18nc("@title:column", "Storage"), }); mainLayout->addWidget(subkeysTree); } } ui; }; void SubKeysWidget::Private::tableContextMenuRequested(const QPoint &p) { auto item = ui.subkeysTree->itemAt(p); if (!item) { return; } const auto subkey = item->data(0, Qt::UserRole).value(); const bool isOpenPGPKey = subkey.parent().protocol() == GpgME::OpenPGP; auto menu = new QMenu(q); connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); bool hasActions = false; if (isOpenPGPKey && subkey.parent().hasSecret()) { hasActions = true; - menu->addAction(i18n("Change Expiry Date..."), q, + menu->addAction(i18n("Change End of Validity Period..."), q, [this, subkey]() { auto cmd = new ChangeExpiryCommand(subkey.parent()); cmd->setSubkey(subkey); ui.subkeysTree->setEnabled(false); connect(cmd, &ChangeExpiryCommand::finished, q, [this]() { ui.subkeysTree->setEnabled(true); key.update(); q->setKey(key); }); cmd->setParentWidget(q); cmd->start(); } ); } if (isOpenPGPKey && subkey.canAuthenticate()) { hasActions = true; menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export OpenSSH key"), q, [this, subkey]() { QScopedPointer dlg(new ExportDialog(q)); dlg->setKey(subkey, static_cast (GpgME::Context::ExportSSH)); dlg->exec(); }); } if (!subkey.isSecret()) { hasActions = true; menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-import")), i18n("Restore printed backup"), q, [this, subkey] () { auto cmd = new ImportPaperKeyCommand(subkey.parent()); ui.subkeysTree->setEnabled(false); connect(cmd, &ImportPaperKeyCommand::finished, q, [this]() { ui.subkeysTree->setEnabled(true); }); cmd->setParentWidget(q); cmd->start(); }); } if (subkey.isSecret()) { hasActions = true; auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("send-to-symbolic")), i18n("Transfer to smartcard"), q, [this, subkey]() { auto cmd = new KeyToCardCommand(subkey); ui.subkeysTree->setEnabled(false); connect(cmd, &KeyToCardCommand::finished, q, [this]() { ui.subkeysTree->setEnabled(true); }); cmd->setParentWidget(q); cmd->start(); }); action->setEnabled(!KeyToCardCommand::getSuitableCards(subkey).empty()); } #ifdef QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT const bool isPrimarySubkey = subkey.keyID() == key.keyID(); if (isOpenPGPKey && subkey.isSecret() && !isPrimarySubkey) { hasActions = true; menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export secret subkey"), q, [this, subkey]() { auto cmd = new ExportSecretSubkeyCommand{{subkey}}; ui.subkeysTree->setEnabled(false); connect(cmd, &ExportSecretSubkeyCommand::finished, q, [this]() { ui.subkeysTree->setEnabled(true); }); cmd->setParentWidget(q); cmd->start(); }); } #endif if (hasActions) { menu->popup(ui.subkeysTree->viewport()->mapToGlobal(p)); } else { delete menu; } } SubKeysWidget::SubKeysWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { } SubKeysWidget::~SubKeysWidget() { } void SubKeysWidget::setKey(const GpgME::Key &key) { d->key = key; const auto currentItem = d->ui.subkeysTree->currentItem(); const QByteArray selectedKeyFingerprint = currentItem ? QByteArray(currentItem->data(0, Qt::UserRole).value().fingerprint()) : QByteArray(); d->ui.subkeysTree->clear(); for (const auto subkeys = key.subkeys(); const auto &subkey : subkeys) { auto item = new QTreeWidgetItem; item->setData(0, Qt::DisplayRole, Formatting::prettyID(subkey.keyID())); item->setData(0, Qt::AccessibleTextRole, Formatting::accessibleHexID(subkey.keyID())); item->setData(0, Qt::UserRole, QVariant::fromValue(subkey)); item->setData(1, Qt::DisplayRole, Kleo::Formatting::type(subkey)); item->setData(2, Qt::DisplayRole, Kleo::Formatting::creationDateString(subkey)); item->setData(2, Qt::AccessibleTextRole, Formatting::accessibleCreationDate(subkey)); item->setData(3, Qt::DisplayRole, Kleo::Formatting::expirationDateString(subkey)); item->setData(3, Qt::AccessibleTextRole, Formatting::accessibleExpirationDate(subkey)); item->setData(4, Qt::DisplayRole, Kleo::Formatting::validityShort(subkey)); switch (subkey.publicKeyAlgorithm()) { case GpgME::Subkey::AlgoECDSA: case GpgME::Subkey::AlgoEDDSA: case GpgME::Subkey::AlgoECDH: item->setData(5, Qt::DisplayRole, QString::fromStdString(subkey.algoName())); break; default: item->setData(5, Qt::DisplayRole, QString::number(subkey.length())); } item->setData(6, Qt::DisplayRole, Kleo::Formatting::usageString(subkey)); const auto isPrimary = subkey.keyID() == key.keyID(); item->setData(7, Qt::DisplayRole, isPrimary ? QStringLiteral("✓") : QString()); item->setData(7, Qt::AccessibleTextRole, isPrimary ? i18nc("yes, is primary key", "yes") : i18nc("no, is not primary key", "no")); if (subkey.isCardKey()) { if (const char *serialNo = subkey.cardSerialNumber()) { item->setData(8, Qt::DisplayRole, i18nc("smart card ", "smart card %1", QString::fromUtf8(serialNo))); } else { item->setData(8, Qt::DisplayRole, i18n("smart card")); } } else if (isPrimary && key.hasSecret() && !subkey.isSecret()) { item->setData(8, Qt::DisplayRole, i18nc("key is 'offline key', i.e. secret key is not stored on this computer", "offline")); } else if (subkey.isSecret()) { item->setData(8, Qt::DisplayRole, i18n("on this computer")); } else { item->setData(8, Qt::DisplayRole, i18nc("unknown storage location", "unknown")); } d->ui.subkeysTree->addTopLevelItem(item); if (subkey.fingerprint() == selectedKeyFingerprint) { d->ui.subkeysTree->setCurrentItem(item); } } if (!key.hasSecret()) { // hide information about storage location for keys of other people d->ui.subkeysTree->hideColumn(8); } d->ui.subkeysTree->header()->resizeSections(QHeaderView::ResizeToContents); } GpgME::Key SubKeysWidget::key() const { return d->key; } SubKeysDialog::SubKeysDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Subkeys Details")); auto l = new QVBoxLayout(this); l->addWidget(new SubKeysWidget(this)); auto bbox = new QDialogButtonBox(this); auto btn = bbox->addButton(QDialogButtonBox::Close); connect(btn, &QPushButton::clicked, this, &QDialog::accept); l->addWidget(bbox); readConfig(); } SubKeysDialog::~SubKeysDialog() { writeConfig(); } void SubKeysDialog::readConfig() { KConfigGroup dialog(KSharedConfig::openStateConfig(), "SubKeysDialog"); const QSize size = dialog.readEntry("Size", QSize(820, 280)); if (size.isValid()) { resize(size); } } void SubKeysDialog::writeConfig() { KConfigGroup dialog(KSharedConfig::openStateConfig(), "SubKeysDialog"); dialog.writeEntry("Size", size()); dialog.sync(); } void SubKeysDialog::setKey(const GpgME::Key &key) { auto w = findChild(); Q_ASSERT(w); w->setKey(key); } GpgME::Key SubKeysDialog::key() const { auto w = findChild(); Q_ASSERT(w); return w->key(); } diff --git a/src/view/keylistcontroller.cpp b/src/view/keylistcontroller.cpp index 067a47ec9..3172eead9 100644 --- a/src/view/keylistcontroller.cpp +++ b/src/view/keylistcontroller.cpp @@ -1,863 +1,863 @@ /* -*- mode: c++; c-basic-offset:4 -*- controllers/keylistcontroller.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2022 Felix Tiede SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keylistcontroller.h" #include "tabwidget.h" #include #include #include #include "tooltippreferences.h" #include "kleopatra_debug.h" #include "commands/exportcertificatecommand.h" #include "commands/exportopenpgpcertstoservercommand.h" #ifdef MAILAKONADI_ENABLED #include "commands/exportopenpgpcerttoprovidercommand.h" #endif // MAILAKONADI_ENABLED #ifdef QGPGME_SUPPORTS_SECRET_KEY_EXPORT # include "commands/exportsecretkeycommand.h" #else # include "commands/exportsecretkeycommand_old.h" #endif #include "commands/importcertificatefromfilecommand.h" #include "commands/changepassphrasecommand.h" #include "commands/lookupcertificatescommand.h" #include "commands/reloadkeyscommand.h" #include "commands/refreshx509certscommand.h" #include "commands/refreshopenpgpcertscommand.h" #include "commands/detailscommand.h" #include "commands/deletecertificatescommand.h" #include "commands/decryptverifyfilescommand.h" #include "commands/signencryptfilescommand.h" #include "commands/signencryptfoldercommand.h" #include "commands/clearcrlcachecommand.h" #include "commands/dumpcrlcachecommand.h" #include "commands/dumpcertificatecommand.h" #include "commands/importcrlcommand.h" #include "commands/changeexpirycommand.h" #include "commands/changeownertrustcommand.h" #include "commands/changeroottrustcommand.h" #include "commands/certifycertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "commands/adduseridcommand.h" #include "commands/newcertificatesigningrequestcommand.h" #include "commands/newopenpgpcertificatecommand.h" #include "commands/checksumverifyfilescommand.h" #include "commands/checksumcreatefilescommand.h" #include "commands/exportpaperkeycommand.h" #include "commands/revokekeycommand.h" #include #include #include #include #include #include #include #include #include #include #include #include // needed for GPGME_VERSION_NUMBER #include using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; #ifndef QGPGME_SUPPORTS_SECRET_KEY_EXPORT using Kleo::Commands::Compat::ExportSecretKeyCommand; #endif class KeyListController::Private { friend class ::Kleo::KeyListController; KeyListController *const q; public: explicit Private(KeyListController *qq); ~Private(); void connectView(QAbstractItemView *view); void connectCommand(Command *cmd); void connectTabWidget(); void disconnectTabWidget(); void addCommand(Command *cmd) { connectCommand(cmd); commands.insert(std::lower_bound(commands.begin(), commands.end(), cmd), cmd); } void addView(QAbstractItemView *view) { connectView(view); views.insert(std::lower_bound(views.begin(), views.end(), view), view); } void removeView(QAbstractItemView *view) { view->disconnect(q); view->selectionModel()->disconnect(q); views.erase(std::remove(views.begin(), views.end(), view), views.end()); } public: void slotDestroyed(QObject *o) { qCDebug(KLEOPATRA_LOG) << (void *)o; views.erase(std::remove(views.begin(), views.end(), o), views.end()); commands.erase(std::remove(commands.begin(), commands.end(), o), commands.end()); } void slotDoubleClicked(const QModelIndex &idx); void slotActivated(const QModelIndex &idx); void slotSelectionChanged(const QItemSelection &old, const QItemSelection &new_); void slotContextMenu(const QPoint &pos); void slotCommandFinished(); void slotAddKey(const Key &key); void slotAboutToRemoveKey(const Key &key); void slotProgress(const QString &what, int current, int total) { Q_EMIT q->progress(current, total); if (!what.isEmpty()) { Q_EMIT q->message(what); } } void slotActionTriggered(QAction *action); void slotCurrentViewChanged(QAbstractItemView *view) { if (view && !std::binary_search(views.cbegin(), views.cend(), view)) { qCDebug(KLEOPATRA_LOG) << "you need to register view" << view << "before trying to set it as the current view!"; addView(view); } currentView = view; q->enableDisableActions(view ? view->selectionModel() : nullptr); } private: int toolTipOptions() const; private: static Command::Restrictions calculateRestrictionsMask(const QItemSelectionModel *sm); private: struct action_item { QPointer action; Command::Restrictions restrictions; Command *(*createCommand)(QAbstractItemView *, KeyListController *); }; std::vector actions; std::vector views; std::vector commands; QPointer parentWidget; QPointer tabWidget; QPointer currentView; QPointer flatModel, hierarchicalModel; std::vector m_connections; }; KeyListController::Private::Private(KeyListController *qq) : q(qq), actions(), views(), commands(), parentWidget(), tabWidget(), flatModel(), hierarchicalModel() { connect(KeyCache::instance().get(), &KeyCache::added, q, [this](const GpgME::Key &key) { slotAddKey(key); }); connect(KeyCache::instance().get(), &KeyCache::aboutToRemove, q, [this](const GpgME::Key &key) { slotAboutToRemoveKey(key); }); } KeyListController::Private::~Private() {} KeyListController::KeyListController(QObject *p) : QObject(p), d(new Private(this)) { } KeyListController::~KeyListController() {} void KeyListController::Private::slotAddKey(const Key &key) { // ### make model act on keycache directly... if (flatModel) { flatModel->addKey(key); } if (hierarchicalModel) { hierarchicalModel->addKey(key); } } void KeyListController::Private::slotAboutToRemoveKey(const Key &key) { // ### make model act on keycache directly... if (flatModel) { flatModel->removeKey(key); } if (hierarchicalModel) { hierarchicalModel->removeKey(key); } } void KeyListController::addView(QAbstractItemView *view) { if (!view || std::binary_search(d->views.cbegin(), d->views.cend(), view)) { return; } d->addView(view); } void KeyListController::removeView(QAbstractItemView *view) { if (!view || !std::binary_search(d->views.cbegin(), d->views.cend(), view)) { return; } d->removeView(view); } void KeyListController::setCurrentView(QAbstractItemView *view) { d->slotCurrentViewChanged(view); } std::vector KeyListController::views() const { return d->views; } void KeyListController::setFlatModel(AbstractKeyListModel *model) { if (model == d->flatModel) { return; } d->flatModel = model; if (model) { model->clear(); if (KeyCache::instance()->initialized()) { model->addKeys(KeyCache::instance()->keys()); } model->setToolTipOptions(d->toolTipOptions()); } } void KeyListController::setHierarchicalModel(AbstractKeyListModel *model) { if (model == d->hierarchicalModel) { return; } d->hierarchicalModel = model; if (model) { model->clear(); if (KeyCache::instance()->initialized()) { model->addKeys(KeyCache::instance()->keys()); } model->setToolTipOptions(d->toolTipOptions()); } } void KeyListController::setTabWidget(TabWidget *tabWidget) { if (tabWidget == d->tabWidget) { return; } d->disconnectTabWidget(); d->tabWidget = tabWidget; d->connectTabWidget(); d->slotCurrentViewChanged(tabWidget ? tabWidget->currentView() : nullptr); } void KeyListController::setParentWidget(QWidget *parent) { d->parentWidget = parent; } QWidget *KeyListController::parentWidget() const { return d->parentWidget; } void KeyListController::Private::connectTabWidget() { if (!tabWidget) { return; } const auto views = tabWidget->views(); std::for_each(views.cbegin(), views.cend(), [this](QAbstractItemView *view) { addView(view); }); m_connections.reserve(3); m_connections.push_back(connect(tabWidget, &TabWidget::viewAdded, q, &KeyListController::addView)); m_connections.push_back(connect(tabWidget, &TabWidget::viewAboutToBeRemoved, q, &KeyListController::removeView)); m_connections.push_back(connect(tabWidget, &TabWidget::currentViewChanged, q, [this](QAbstractItemView *view) { slotCurrentViewChanged(view); })); } void KeyListController::Private::disconnectTabWidget() { if (!tabWidget) { return; } for (const auto &connection : m_connections) { disconnect(connection); } m_connections.clear(); const auto views = tabWidget->views(); std::for_each(views.cbegin(), views.cend(), [this](QAbstractItemView *view) { removeView(view); }); } AbstractKeyListModel *KeyListController::flatModel() const { return d->flatModel; } AbstractKeyListModel *KeyListController::hierarchicalModel() const { return d->hierarchicalModel; } QAbstractItemView *KeyListController::currentView() const { return d->currentView; } TabWidget *KeyListController::tabWidget() const { return d->tabWidget; } void KeyListController::createActions(KActionCollection *coll) { const std::vector common_and_openpgp_action_data = { // File menu { "file_new_certificate", i18n("New OpenPGP Key Pair..."), i18n("Create a new OpenPGP certificate"), "view-certificate-add", nullptr, nullptr, QStringLiteral("Ctrl+N") }, { "file_export_certificates", i18n("Export..."), i18n("Export the selected certificate (public key) to a file"), "view-certificate-export", nullptr, nullptr, QStringLiteral("Ctrl+E") }, { "file_export_certificates_to_server", i18n("Publish on Server..."), i18n("Publish the selected certificate (public key) on a public keyserver"), "view-certificate-export-server", nullptr, nullptr, QStringLiteral("Ctrl+Shift+E") }, #ifdef MAILAKONADI_ENABLED { "file_export_certificate_to_provider", i18n("Publish at Mail Provider..."), i18n("Publish the selected certificate (public key) at mail provider's Web Key Directory if offered"), "view-certificate-export", nullptr, nullptr, QString() }, #endif // MAILAKONADI_ENABLED { "file_export_secret_keys", i18n("Backup Secret Keys..."), QString(), "view-certificate-export-secret", nullptr, nullptr, QString() }, { "file_export_paper_key", i18n("Print Secret Key..."), QString(), "document-print", nullptr, nullptr, QString() }, { "file_lookup_certificates", i18n("Lookup on Server..."), i18n("Search for certificates online using a public keyserver"), "edit-find", nullptr, nullptr, QStringLiteral("Shift+Ctrl+I") }, { "file_import_certificates", i18n("Import..."), i18n("Import a certificate from a file"), "view-certificate-import", nullptr, nullptr, QStringLiteral("Ctrl+I") }, { "file_decrypt_verify_files", i18n("Decrypt/Verify..."), i18n("Decrypt and/or verify files"), "document-edit-decrypt-verify", nullptr, nullptr, QString() }, { "file_sign_encrypt_files", i18n("Sign/Encrypt..."), i18n("Encrypt and/or sign files"), "document-edit-sign-encrypt", nullptr, nullptr, QString() }, { "file_sign_encrypt_folder", i18n("Sign/Encrypt Folder..."), i18n("Encrypt and/or sign folders"), nullptr/*"folder-edit-sign-encrypt"*/, nullptr, nullptr, QString() }, { "file_checksum_create_files", i18n("Create Checksum Files..."), QString(), nullptr/*"document-checksum-create"*/, nullptr, nullptr, QString() }, { "file_checksum_verify_files", i18n("Verify Checksum Files..."), QString(), nullptr/*"document-checksum-verify"*/, nullptr, nullptr, QString() }, // View menu { "view_redisplay", i18n("Redisplay"), QString(), "view-refresh", nullptr, nullptr, QStringLiteral("F5") }, { "view_stop_operations", i18n("Stop Operation"), QString(), "process-stop", this, [this](bool) { cancelCommands(); }, QStringLiteral("Escape"), RegularQAction, Disabled }, { "view_certificate_details", i18n("Details"), QString(), "dialog-information", nullptr, nullptr, QString() }, // Certificate menu #ifdef QGPGME_SUPPORTS_KEY_REVOCATION { "certificates_revoke", i18n("Revoke Certificate..."), i18n("Revoke the selected OpenPGP certificate"), "view-certificate-revoke", nullptr, nullptr, {} }, #endif { "certificates_delete", i18n("Delete"), i18n("Delete selected certificates"), "edit-delete", nullptr, nullptr, QStringLiteral("Delete") }, { "certificates_certify_certificate", i18n("Certify..."), i18n("Certify the validity of the selected certificate"), "view-certificate-sign", nullptr, nullptr, QString() }, { "certificates_revoke_certification", i18n("Revoke Certification..."), i18n("Revoke the certification of the selected certificate"), "view-certificate-revoke", nullptr, nullptr, QString() }, { - "certificates_change_expiry", i18n("Change Expiry Date..."), QString(), + "certificates_change_expiry", i18n("Change End of Validity Period..."), QString(), nullptr, nullptr, nullptr, QString() }, { "certificates_change_owner_trust", i18n("Change Certification Trust..."), QString(), nullptr, nullptr, nullptr, QString() }, { "certificates_change_passphrase", i18n("Change Passphrase..."), QString(), nullptr, nullptr, nullptr, QString() }, { "certificates_add_userid", i18n("Add User ID..."), QString(), nullptr, nullptr, nullptr, QString() }, // Tools menu { "tools_refresh_openpgp_certificates", i18n("Refresh OpenPGP Certificates"), QString(), "view-refresh", nullptr, nullptr, QString() }, // Window menu // (come from TabWidget) // Help menu // (come from MainWindow) }; static const action_data cms_create_csr_action_data = { "file_new_certificate_signing_request", i18n("New S/MIME Certification Request..."), i18n("Create a new S/MIME certificate signing request (CSR)"), "view-certificate-add", nullptr, nullptr, {}, }; static const std::vector cms_action_data = { // Certificate menu { "certificates_trust_root", i18n("Trust Root Certificate"), QString(), nullptr, nullptr, nullptr, QString() }, { "certificates_distrust_root", i18n("Distrust Root Certificate"), QString(), nullptr, nullptr, nullptr, QString() }, { "certificates_dump_certificate", i18n("Technical Details"), QString(), nullptr, nullptr, nullptr, QString() }, // Tools menu { "tools_refresh_x509_certificates", i18n("Refresh S/MIME Certificates"), QString(), "view-refresh", nullptr, nullptr, QString() }, { "crl_clear_crl_cache", i18n("Clear CRL Cache"), QString(), nullptr, nullptr, nullptr, QString() }, { "crl_dump_crl_cache", i18n("Dump CRL Cache"), QString(), nullptr, nullptr, nullptr, QString() }, { "crl_import_crl", i18n("Import CRL From File..."), QString(), nullptr, nullptr, nullptr, QString() }, }; std::vector action_data = common_and_openpgp_action_data; if (const Kleo::Settings settings{}; settings.cmsEnabled()) { if (settings.cmsCertificateCreationAllowed()) { action_data.push_back(cms_create_csr_action_data); } action_data.reserve(action_data.size() + cms_action_data.size()); std::copy(std::begin(cms_action_data), std::end(cms_action_data), std::back_inserter(action_data)); } make_actions_from_data(action_data, coll); if (QAction *action = coll->action(QStringLiteral("view_stop_operations"))) { connect(this, &KeyListController::commandsExecuting, action, &QAction::setEnabled); } // ### somehow make this better... registerActionForCommand(coll->action(QStringLiteral("file_new_certificate"))); registerActionForCommand(coll->action(QStringLiteral("file_new_certificate_signing_request"))); //--- registerActionForCommand(coll->action(QStringLiteral("file_lookup_certificates"))); registerActionForCommand(coll->action(QStringLiteral("file_import_certificates"))); //--- registerActionForCommand(coll->action(QStringLiteral("file_export_certificates"))); registerActionForCommand(coll->action(QStringLiteral("file_export_secret_keys"))); registerActionForCommand(coll->action(QStringLiteral("file_export_paper_key"))); registerActionForCommand(coll->action(QStringLiteral("file_export_certificates_to_server"))); #ifdef MAILAKONADI_ENABLED registerActionForCommand(coll->action(QStringLiteral("file_export_certificate_to_provider"))); #endif // MAILAKONADI_ENABLED //--- registerActionForCommand(coll->action(QStringLiteral("file_decrypt_verify_files"))); registerActionForCommand(coll->action(QStringLiteral("file_sign_encrypt_files"))); registerActionForCommand(coll->action(QStringLiteral("file_sign_encrypt_folder"))); //--- registerActionForCommand(coll->action(QStringLiteral("file_checksum_create_files"))); registerActionForCommand(coll->action(QStringLiteral("file_checksum_verify_files"))); registerActionForCommand(coll->action(QStringLiteral("view_redisplay"))); //coll->action( "view_stop_operations" ) <-- already dealt with in make_actions_from_data() registerActionForCommand(coll->action(QStringLiteral("view_certificate_details"))); registerActionForCommand(coll->action(QStringLiteral("certificates_change_owner_trust"))); registerActionForCommand(coll->action(QStringLiteral("certificates_trust_root"))); registerActionForCommand(coll->action(QStringLiteral("certificates_distrust_root"))); //--- registerActionForCommand(coll->action(QStringLiteral("certificates_certify_certificate"))); if (RevokeCertificationCommand::isSupported()) { registerActionForCommand(coll->action(QStringLiteral("certificates_revoke_certification"))); } //--- registerActionForCommand(coll->action(QStringLiteral("certificates_change_expiry"))); registerActionForCommand(coll->action(QStringLiteral("certificates_change_passphrase"))); registerActionForCommand(coll->action(QStringLiteral("certificates_add_userid"))); //--- #ifdef QGPGME_SUPPORTS_KEY_REVOCATION registerActionForCommand(coll->action(QStringLiteral("certificates_revoke"))); #endif registerActionForCommand(coll->action(QStringLiteral("certificates_delete"))); //--- registerActionForCommand(coll->action(QStringLiteral("certificates_dump_certificate"))); registerActionForCommand(coll->action(QStringLiteral("tools_refresh_x509_certificates"))); registerActionForCommand(coll->action(QStringLiteral("tools_refresh_openpgp_certificates"))); //--- registerActionForCommand(coll->action(QStringLiteral("crl_import_crl"))); //--- registerActionForCommand(coll->action(QStringLiteral("crl_clear_crl_cache"))); registerActionForCommand(coll->action(QStringLiteral("crl_dump_crl_cache"))); enableDisableActions(nullptr); } void KeyListController::registerAction(QAction *action, Command::Restrictions restrictions, Command * (*create)(QAbstractItemView *, KeyListController *)) { if (!action) { return; } Q_ASSERT(!action->isCheckable()); // can be added later, for now, disallow const Private::action_item ai = { action, restrictions, create }; connect(action, &QAction::triggered, this, [this, action]() { d->slotActionTriggered(action); }); d->actions.push_back(ai); } void KeyListController::registerCommand(Command *cmd) { if (!cmd || std::binary_search(d->commands.cbegin(), d->commands.cend(), cmd)) { return; } d->addCommand(cmd); qCDebug(KLEOPATRA_LOG) << (void *)cmd; if (d->commands.size() == 1) { Q_EMIT commandsExecuting(true); } } bool KeyListController::hasRunningCommands() const { return !d->commands.empty(); } bool KeyListController::shutdownWarningRequired() const { return std::any_of(d->commands.cbegin(), d->commands.cend(), std::mem_fn(&Command::warnWhenRunningAtShutdown)); } // slot void KeyListController::cancelCommands() { std::for_each(d->commands.begin(), d->commands.end(), std::mem_fn(&Command::cancel)); } void KeyListController::Private::connectView(QAbstractItemView *view) { connect(view, &QObject::destroyed, q, [this](QObject *obj) { slotDestroyed(obj); }); connect(view, &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) { slotDoubleClicked(index); }); connect(view, &QAbstractItemView::activated, q, [this](const QModelIndex &index) { slotActivated(index); }); connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this](const QItemSelection &oldSel, const QItemSelection &newSel) { slotSelectionChanged(oldSel, newSel); }); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QWidget::customContextMenuRequested, q, [this](const QPoint &pos) { slotContextMenu(pos); }); } void KeyListController::Private::connectCommand(Command *cmd) { if (!cmd) { return; } connect(cmd, &QObject::destroyed, q, [this](QObject *obj) { slotDestroyed(obj); }); connect(cmd, &Command::finished, q, [this] { slotCommandFinished(); }); //connect( cmd, SIGNAL(canceled()), q, SLOT(slotCommandCanceled()) ); connect(cmd, &Command::info, q, &KeyListController::message); connect(cmd, &Command::progress, q, [this](const QString &message, int current, int total) { slotProgress(message, current, total); }); } void KeyListController::Private::slotDoubleClicked(const QModelIndex &idx) { QAbstractItemView *const view = qobject_cast(q->sender()); if (!view || !std::binary_search(views.cbegin(), views.cend(), view)) { return; } DetailsCommand *const c = new DetailsCommand(view, q); if (parentWidget) { c->setParentWidget(parentWidget); } c->setIndex(idx); c->start(); } void KeyListController::Private::slotActivated(const QModelIndex &idx) { Q_UNUSED(idx) QAbstractItemView *const view = qobject_cast(q->sender()); if (!view || !std::binary_search(views.cbegin(), views.cend(), view)) { return; } } void KeyListController::Private::slotSelectionChanged(const QItemSelection &old, const QItemSelection &new_) { Q_UNUSED(old) Q_UNUSED(new_) const QItemSelectionModel *const sm = qobject_cast(q->sender()); if (!sm) { return; } q->enableDisableActions(sm); } void KeyListController::Private::slotContextMenu(const QPoint &p) { QAbstractItemView *const view = qobject_cast(q->sender()); if (view && std::binary_search(views.cbegin(), views.cend(), view)) { Q_EMIT q->contextMenuRequested(view, view->viewport()->mapToGlobal(p)); } else { qCDebug(KLEOPATRA_LOG) << "sender is not a QAbstractItemView*!"; } } void KeyListController::Private::slotCommandFinished() { Command *const cmd = qobject_cast(q->sender()); if (!cmd || !std::binary_search(commands.cbegin(), commands.cend(), cmd)) { return; } qCDebug(KLEOPATRA_LOG) << (void *)cmd; if (commands.size() == 1) { Q_EMIT q->commandsExecuting(false); } } void KeyListController::enableDisableActions(const QItemSelectionModel *sm) const { const Command::Restrictions restrictionsMask = d->calculateRestrictionsMask(sm); for (const Private::action_item &ai : std::as_const(d->actions)) if (ai.action) { ai.action->setEnabled(ai.restrictions == (ai.restrictions & restrictionsMask)); } } static bool all_secret_are_not_owner_trust_ultimate(const std::vector &keys) { for (const Key &key : keys) if (key.hasSecret() && key.ownerTrust() == Key::Ultimate) { return false; } return true; } Command::Restrictions find_root_restrictions(const std::vector &keys) { bool trusted = false, untrusted = false; for (const Key &key : keys) if (key.isRoot()) if (key.userID(0).validity() == UserID::Ultimate) { trusted = true; } else { untrusted = true; } else { return Command::NoRestriction; } if (trusted) if (untrusted) { return Command::NoRestriction; } else { return Command::MustBeTrustedRoot; } else if (untrusted) { return Command::MustBeUntrustedRoot; } else { return Command::NoRestriction; } } Command::Restrictions KeyListController::Private::calculateRestrictionsMask(const QItemSelectionModel *sm) { if (!sm) { return Command::NoRestriction; } const KeyListModelInterface *const m = dynamic_cast(sm->model()); if (!m) { return Command::NoRestriction; } const std::vector keys = m->keys(sm->selectedRows()); if (keys.empty()) { return Command::NoRestriction; } Command::Restrictions result = Command::NeedSelection; if (keys.size() == 1) { result |= Command::OnlyOneKey; } #if GPGME_VERSION_NUMBER >= 0x011102 // 1.17.2 // we need to check the primary subkey because Key::hasSecret() is also true if just the secret key stub of an offline key is available const auto primaryKeyCanBeUsedForSecretKeyOperations = [](const auto &k) { return k.subkey(0).isSecret(); }; #else // older versions of GpgME did not always set the secret flag for card keys const auto primaryKeyCanBeUsedForSecretKeyOperations = [](const auto &k) { return k.subkey(0).isSecret() || k.subkey(0).isCardKey(); }; #endif if (std::all_of(keys.cbegin(), keys.cend(), primaryKeyCanBeUsedForSecretKeyOperations)) { result |= Command::NeedSecretKey; } if (std::all_of(std::begin(keys), std::end(keys), [](const auto &k) { return k.subkey(0).isSecret() && !k.subkey(0).isCardKey(); })) { result |= Command::NeedSecretKeyData; } if (std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { return key.protocol() == OpenPGP; })) { result |= Command::MustBeOpenPGP; } else if (std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { return key.protocol() == CMS; })) { result |= Command::MustBeCMS; } if (all_secret_are_not_owner_trust_ultimate(keys)) { result |= Command::MayOnlyBeSecretKeyIfOwnerTrustIsNotYetUltimate; } result |= find_root_restrictions(keys); if (const ReaderStatus *rs = ReaderStatus::instance()) { if (!rs->firstCardWithNullPin().empty()) { result |= Command::AnyCardHasNullPin; } if (rs->anyCardCanLearnKeys()) { result |= Command::AnyCardCanLearnKeys; } } return result; } void KeyListController::Private::slotActionTriggered(QAction *sender) { const auto it = std::find_if(actions.cbegin(), actions.cend(), [sender](const action_item &item) { return item.action == sender; }); if (it != actions.end()) if (Command *const c = it->createCommand(this->currentView, q)) { if (parentWidget) { c->setParentWidget(parentWidget); } c->start(); } else qCDebug(KLEOPATRA_LOG) << "createCommand() == NULL for action(?) \"" << qPrintable(sender->objectName()) << "\""; else { qCDebug(KLEOPATRA_LOG) << "I don't know anything about action(?) \"%s\"", qPrintable(sender->objectName()); } } int KeyListController::Private::toolTipOptions() const { using namespace Kleo::Formatting; static const int validityFlags = Validity | Issuer | ExpiryDates | CertificateUsage; static const int ownerFlags = Subject | UserIDs | OwnerTrust; static const int detailsFlags = StorageLocation | CertificateType | SerialNumber | Fingerprint; const TooltipPreferences prefs; int flags = KeyID; flags |= prefs.showValidity() ? validityFlags : 0; flags |= prefs.showOwnerInformation() ? ownerFlags : 0; flags |= prefs.showCertificateDetails() ? detailsFlags : 0; return flags; } void KeyListController::updateConfig() { const int opts = d->toolTipOptions(); if (d->flatModel) { d->flatModel->setToolTipOptions(opts); } if (d->hierarchicalModel) { d->hierarchicalModel->setToolTipOptions(opts); } } #include "moc_keylistcontroller.cpp"