diff --git a/CMakeLists.txt b/CMakeLists.txt index e17675b12..39838e50b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,259 +1,259 @@ # SPDX-FileCopyrightText: none # SPDX-License-Identifier: BSD-3-Clause cmake_minimum_required(VERSION 3.16 FATAL_ERROR) set(RELEASE_SERVICE_VERSION_MAJOR "24") set(RELEASE_SERVICE_VERSION_MINOR "04") set(RELEASE_SERVICE_VERSION_MICRO "70") # The RELEASE_SERVICE_VERSION is used by Gpg4win to add the Gpg4win version if (NOT RELEASE_SERVICE_VERSION) set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") endif() if(RELEASE_SERVICE_VERSION_MICRO LESS 10) set(KDE_APPLICATIONS_COMPACT_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}${RELEASE_SERVICE_VERSION_MINOR}0${RELEASE_SERVICE_VERSION_MICRO}") else() set(KDE_APPLICATIONS_COMPACT_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}${RELEASE_SERVICE_VERSION_MINOR}${RELEASE_SERVICE_VERSION_MICRO}") endif() set(KLEOPATRA_VERSION_MAJOR "3") set(KLEOPATRA_VERSION_MINOR "2") set(KLEOPATRA_VERSION_MICRO "0") set(kleopatra_version "${KLEOPATRA_VERSION_MAJOR}.${KLEOPATRA_VERSION_MINOR}.${KLEOPATRA_VERSION_MICRO}.${KDE_APPLICATIONS_COMPACT_VERSION}") # The following is for Windows set(kleopatra_version_win "${KLEOPATRA_VERSION_MAJOR}.${KLEOPATRA_VERSION_MINOR}.${KLEOPATRA_VERSION_MICRO}") set(kleopatra_fileversion_win "${KLEOPATRA_VERSION_MAJOR},${KLEOPATRA_VERSION_MINOR},${KLEOPATRA_VERSION_MICRO},0") if (NOT KLEOPATRA_DISTRIBUTION_TEXT) # This is only used on Windows for the file attributes of Kleopatra set(KLEOPATRA_DISTRIBUTION_TEXT "KDE") endif() project(kleopatra VERSION ${kleopatra_version}) option(DISABLE_KWATCHGNUPG "Don't build the kwatchgnupg tool [default=OFF]" OFF) # Standalone build. Find / include everything necessary. set(KF_MIN_VERSION "5.248.0") set(KIDENTITYMANAGEMENT_VERSION "6.0.40") set(KMAILTRANSPORT_VERSION "6.0.40") set(AKONADI_MIME_VERSION "6.0.40") set(KMIME_VERSION "6.0.40") -set(LIBKLEO_VERSION "6.0.41") +set(LIBKLEO_VERSION "6.0.42") set(QT_REQUIRED_VERSION "6.6.0") set(MIMETREEPARSER_VERSION "6.0.40") set(GPGME_REQUIRED_VERSION "1.20.0") set(LIBASSUAN_REQUIRED_VERSION "2.4.2") set(GPG_ERROR_REQUIRED_VERSION "1.36") if (WIN32) set(KF6_WANT_VERSION "6.0.40") set(KMIME_WANT_VERSION "5.248.0") else () set(KF6_WANT_VERSION ${KF_MIN_VERSION}) set(KMIME_WANT_VERSION ${KMIME_VERSION}) endif () set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(ECM ${KF6_WANT_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH}) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMAddTests) include(GenerateExportHeader) include(ECMGenerateHeaders) include(FeatureSummary) include(CheckFunctionExists) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) include(ECMDeprecationSettings) include(KDEClangFormat) include(KDEGitCommitHooks) # Find KF6 packages find_package(KF6 ${KF6_WANT_VERSION} REQUIRED COMPONENTS Codecs Config CoreAddons Crash I18n IconThemes ItemModels KCMUtils KIO WidgetsAddons WindowSystem XmlGui StatusNotifierItem OPTIONAL_COMPONENTS DocTools ) set_package_properties(KF6DocTools PROPERTIES DESCRIPTION "Documentation tools" PURPOSE "Required to generate Kleopatra documentation." TYPE OPTIONAL) # Optional packages if (WIN32) # Only a replacement available for Windows so this # is required on other platforms. find_package(KF6DBusAddons ${KF6_WANT_VERSION} CONFIG) set_package_properties(KF6DBusAddons PROPERTIES DESCRIPTION "Support library to work with DBus" PURPOSE "DBus session integration" URL "https://inqlude.org/libraries/kdbusaddons.html" TYPE OPTIONAL) else() find_package(KF6DBusAddons ${KF6_WANT_VERSION} CONFIG REQUIRED) set(_kleopatra_dbusaddons_libs KF6::DBusAddons) endif() set(HAVE_QDBUS ${Qt6DBus_FOUND}) find_package(Gpgmepp ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) set(QGPGME_NAME "QGpgmeQt6") find_package(${QGPGME_NAME} ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.21.0") set(QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME 1) set(QGPGME_ARCHIVE_JOBS_SUPPORT_INPUT_FILENAME 1) endif() if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.22.0") set(QGPGME_HAS_TOLOGSTRING 1) set(QGPGME_SUPPORTS_IS_MIME 1) endif() if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.23.0") set(QGPGME_SUPPORTS_WKD_REFRESH_JOB 1) endif() if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.23.3") set(QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO 1) endif() find_package(KPim6Libkleo ${LIBKLEO_VERSION} CONFIG REQUIRED) find_package(KPim6Mime ${KMIME_WANT_VERSION} CONFIG REQUIRED) find_package(KPim6IdentityManagementCore ${KIDENTITYMANAGEMENT_VERSION} CONFIG) find_package(KPim6MailTransport ${KMAILTRANSPORT_VERSION} CONFIG) find_package(KPim6AkonadiMime ${AKONADI_MIME_VERSION} CONFIG) find_package(KPim6MimeTreeParserWidgets ${MIMETREEPARSER_VERSION} CONFIG REQUIRED) find_package(Qt6 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Widgets Test Network PrintSupport) find_package(LibAssuan ${LIBASSUAN_REQUIRED_VERSION} REQUIRED) set_package_properties(LibAssuan PROPERTIES TYPE REQUIRED PURPOSE "Needed for Kleopatra to act as the GnuPG UI Server" ) find_package(LibGpgError ${GPG_ERROR_REQUIRED_VERSION} REQUIRED) set_package_properties(LibGpgError PROPERTIES TYPE REQUIRED ) set(kleopatra_release FALSE) if(NOT kleopatra_release) find_package(Git) if(GIT_FOUND) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} RESULT_VARIABLE rc ERROR_QUIET) if(rc EQUAL 0) execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --oneline --format=%h ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE Kleopatra_WC_REVISION) string(REGEX REPLACE "\n" "" Kleopatra_WC_REVISION "${Kleopatra_WC_REVISION}") execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --oneline --format=%cI ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE Kleopatra_WC_LAST_CHANGED_DATE) string(REGEX REPLACE "^([0-9]+)-([0-9]+)-([0-9]+)T([0-9]+):([0-9]+):([0-9]+).*$" "\\1\\2\\3T\\4\\5\\6" Kleopatra_WC_LAST_CHANGED_DATE "${Kleopatra_WC_LAST_CHANGED_DATE}") set(kleopatra_version "${kleopatra_version}+git${Kleopatra_WC_LAST_CHANGED_DATE}~${Kleopatra_WC_REVISION}") endif() endif() endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version-kleopatra.h.in ${CMAKE_CURRENT_BINARY_DIR}/version-kleopatra.h) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-kleopatra.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-kleopatra.h) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ) if (WIN32) # On Windows, we need to use stuff deprecated since Qt 5.11, e.g. from QDesktopWidget add_definitions(-DQT_NO_CONTEXTLESS_CONNECT) ecm_set_disabled_deprecation_versions(QT 5.10.0 KF 5.248.0) else () add_definitions(-DQT_NO_CONTEXTLESS_CONNECT) ecm_set_disabled_deprecation_versions(QT 6.6.0 KF 5.248.0) endif () if(CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-missing-braces -Wno-parentheses -Wno-ignored-qualifiers") endif() if(MINGW) # we do not care about different signedness of passed pointer arguments add_compile_options($<$:-Wno-pointer-sign>) endif() add_definitions(-DQT_NO_EMIT) remove_definitions(-DQT_NO_FOREACH) # Disable the use of QStringBuilder for operator+ to prevent crashes when # returning the result of concatenating string temporaries in lambdas. We do # this for example in some std::transform expressions. # This is a known issue: https://bugreports.qt.io/browse/QTBUG-47066 # Alternatively, one would always have to remember to force the lambdas to # return a QString instead of QStringBuilder, but that's just too easy to # forget and, unfortunately, the compiler doesn't issue a warning if one forgets # this. So, it's just too dangerous. # One can still use QStringBuilder explicitly with the operator% if necessary. remove_definitions(-DQT_USE_FAST_OPERATOR_PLUS) remove_definitions(-DQT_USE_QSTRINGBUILDER) kde_enable_exceptions() option(USE_UNITY_CMAKE_SUPPORT "Use UNITY cmake support (speedup compile time)" OFF) set(COMPILE_WITH_UNITY_CMAKE_SUPPORT OFF) if (USE_UNITY_CMAKE_SUPPORT) set(COMPILE_WITH_UNITY_CMAKE_SUPPORT ON) endif() add_subdirectory(pics) add_subdirectory(src) if(BUILD_TESTING) add_subdirectory(tests) add_subdirectory(autotests) endif() ecm_qt_install_logging_categories( EXPORT KLEOPATRA FILE kleopatra.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ) ki18n_install(po) if(KF6DocTools_FOUND) kdoctools_install(po) add_subdirectory(doc) endif() feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) # add clang-format target for all our real source files file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h *.c) kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp index af7ded56e..416b8332e 100644 --- a/src/dialogs/certificatedetailswidget.cpp +++ b/src/dialogs/certificatedetailswidget.cpp @@ -1,1136 +1,1136 @@ /* 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 "exportdialog.h" #include "kleopatra_debug.h" #include "subkeyswidget.h" #include "trustchainwidget.h" #include "weboftrustdialog.h" #include "commands/certifycertificatecommand.h" #include "commands/changeexpirycommand.h" #include "commands/changepassphrasecommand.h" #ifdef MAILAKONADI_ENABLED #include "commands/exportopenpgpcerttoprovidercommand.h" #endif // MAILAKONADI_ENABLED #include "commands/adduseridcommand.h" #include "commands/detailscommand.h" #include "commands/dumpcertificatecommand.h" #include "commands/genrevokecommand.h" #include "commands/refreshcertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "commands/revokeuseridcommand.h" #include "commands/setprimaryuseridcommand.h" #include "utils/accessibility.h" #include "utils/tags.h" #include "view/infofield.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 #include #include #if __has_include() #include #if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L #define USE_RANGES #endif #endif #include Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; 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 setPrimaryUserID(const GpgME::UserID &uid = {}); 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; + TreeWidget *userIDTable = nullptr; QPushButton *addUserIDBtn = nullptr; QPushButton *setPrimaryUserIDBtn = 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}; + userIDTable = new TreeWidget{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); setPrimaryUserIDBtn = new QPushButton{i18nc("@action:button", "Flag as Primary"), parent}; setPrimaryUserIDBtn->setToolTip(i18nc("@info:tooltip", "Flag the selected user ID as the primary user ID of this key.")); buttonRow->addWidget(setPrimaryUserIDBtn); 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("Valid until:"), parent); changeExpirationAction = new QAction{parent}; changeExpirationAction->setIcon(QIcon::fromTheme(QStringLiteral("editor"))); changeExpirationAction->setToolTip(i18nc("@info:tooltip", "Change the end of the validity period")); Kleo::setAccessibleName(changeExpirationAction, 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")); Kleo::setAccessibleName(copyFingerprintAction, 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")); Kleo::setAccessibleName(showIssuerCertificateAction, 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}; 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.setPrimaryUserIDBtn, &QPushButton::clicked, q, [this]() { setPrimaryUserID(); }); 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(); const auto isLocalKey = !isRemoteKey(key); const auto keyCanBeCertified = Kleo::canBeCertified(key); // update visibility of UI elements ui.userIDs->setVisible(isOpenPGP); ui.addUserIDBtn->setVisible(isOwnKey); ui.setPrimaryUserIDBtn->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(isLocalKey); ui.moreDetailsBtn->setText(isSMIME ? i18nc("@action:button", "More Details...") : isOwnKey ? i18nc("@action:button", "Manage Subkeys") : i18nc("@action:button", "Show Subkeys")); ui.trustChainDetailsBtn->setVisible(isSMIME); ui.refreshBtn->setVisible(isLocalKey); ui.changePassphraseBtn->setVisible(isSecretKeyStoredInKeyRing(key)); ui.exportBtn->setVisible(isLocalKey); ui.genRevokeBtn->setVisible(isOpenPGP && isOwnKey); // update availability of buttons const auto userCanSignUserIDs = userHasCertificationKey(); ui.addUserIDBtn->setEnabled(canBeUsedForSecretKeyOperations(key)); ui.setPrimaryUserIDBtn->setEnabled(false); // requires a selected user ID ui.certifyBtn->setEnabled(isLocalKey && keyCanBeCertified && userCanSignUserIDs); ui.webOfTrustBtn->setEnabled(isLocalKey); ui.revokeCertificationsBtn->setEnabled(userCanSignUserIDs && isLocalKey); 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("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); const auto singleUserID = userIDs.size() == 1 ? userIDs.front() : GpgME::UserID{}; const bool isPrimaryUserID = !singleUserID.isNull() && (ui.userIDTable->selectedItems().front() == ui.userIDTable->topLevelItem(0)); ui.setPrimaryUserIDBtn->setEnabled(!singleUserID.isNull() // && !isPrimaryUserID // && !Kleo::isRevokedOrExpired(singleUserID) // && canBeUsedForSecretKeyOperations(key)); ui.revokeUserIDBtn->setEnabled(!singleUserID.isNull() && canCreateCertifications(key) && canRevokeUserID(singleUserID)); } 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::ok(); confirmButton.setText(i18nc("@action:button", "Revoke User ID")); confirmButton.setToolTip({}); const auto choice = KMessageBox::questionTwoActions(q->window(), message, i18nc("@title:window", "Confirm Revocation"), confirmButton, KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::WindowModal); if (choice != KMessageBox::ButtonCode::PrimaryAction) { 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(); } void CertificateDetailsWidget::Private::setPrimaryUserID(const GpgME::UserID &uid) { auto userId = uid; if (userId.isNull()) { const auto userIDs = selectedUserIDs(ui.userIDTable); if (userIDs.size() != 1) { return; } userId = userIDs.front(); } auto cmd = new Kleo::Commands::SetPrimaryUserIDCommand(userId); QObject::connect(cmd, &Kleo::Commands::SetPrimaryUserIDCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); // the Flag As Primary button will be updated by the key update updateKey(); }); ui.userIDTable->setEnabled(false); ui.setPrimaryUserIDBtn->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 isPrimaryUserID = !singleUserID.isNull() && (ui.userIDTable->selectedItems().front() == ui.userIDTable->topLevelItem(0)); const bool canSignUserIDs = userHasCertificationKey(); const auto isLocalKey = !isRemoteKey(key); const auto keyCanBeCertified = Kleo::canBeCertified(key); auto menu = new QMenu(q); if (key.hasSecret()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("favorite")), i18nc("@action:inmenu", "Flag as Primary User ID"), q, [this, singleUserID]() { setPrimaryUserID(singleUserID); }); action->setEnabled(!singleUserID.isNull() // && !isPrimaryUserID // && !Kleo::isRevokedOrExpired(singleUserID) // && canBeUsedForSecretKeyOperations(key)); } { 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(isLocalKey && keyCanBeCertified && 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(isLocalKey && 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() { if (key.protocol() == GpgME::CMS) { auto cmd = new Kleo::Commands::DumpCertificateCommand(key); cmd->setParentWidget(q); cmd->setUseDialog(true); cmd->start(); } else { auto dlg = new SubKeysDialog{q}; dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setKey(key); dlg->open(); } } 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()); 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/certifywidget.cpp b/src/dialogs/certifywidget.cpp index f3c824676..0e8e6a865 100644 --- a/src/dialogs/certifywidget.cpp +++ b/src/dialogs/certifywidget.cpp @@ -1,1084 +1,1084 @@ /* dialogs/certifywidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2019, 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certifywidget.h" #include "view/infofield.h" #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; using namespace GpgME; static QDebug operator<<(QDebug s, const GpgME::UserID &userID) { return s << Formatting::prettyUserID(userID); } namespace { // Maybe move this in its own file // based on code from StackOverflow class AnimatedExpander : public QWidget { Q_OBJECT public: explicit AnimatedExpander(const QString &title, const QString &accessibleTitle = {}, QWidget *parent = nullptr); void setContentLayout(QLayout *contentLayout); bool isExpanded() const; void setExpanded(bool expanded); private: static const int animationDuration = 300; QGridLayout mainLayout; QToolButton toggleButton; QFrame headerLine; QParallelAnimationGroup toggleAnimation; QWidget contentArea; }; AnimatedExpander::AnimatedExpander(const QString &title, const QString &accessibleTitle, QWidget *parent) : QWidget{parent} { #ifdef Q_OS_WIN // draw dotted focus frame if button has focus; otherwise, draw invisible frame using background color toggleButton.setStyleSheet( QStringLiteral("QToolButton { border: 1px solid palette(window); }" "QToolButton:focus { border: 1px dotted palette(window-text); }")); #else // this works with Breeze style because Breeze draws the focus frame when drawing CE_ToolButtonLabel // while the Windows styles (and Qt's common base style) draw the focus frame before drawing CE_ToolButtonLabel toggleButton.setStyleSheet(QStringLiteral("QToolButton { border: none; }")); #endif toggleButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toggleButton.setArrowType(Qt::ArrowType::RightArrow); toggleButton.setText(title); if (!accessibleTitle.isEmpty()) { toggleButton.setAccessibleName(accessibleTitle); } toggleButton.setCheckable(true); toggleButton.setChecked(false); headerLine.setFrameShape(QFrame::HLine); headerLine.setFrameShadow(QFrame::Sunken); headerLine.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); contentArea.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // start out collapsed contentArea.setMaximumHeight(0); contentArea.setMinimumHeight(0); contentArea.setVisible(false); // let the entire widget grow and shrink with its content toggleAnimation.addAnimation(new QPropertyAnimation(this, "minimumHeight")); toggleAnimation.addAnimation(new QPropertyAnimation(this, "maximumHeight")); toggleAnimation.addAnimation(new QPropertyAnimation(&contentArea, "maximumHeight")); mainLayout.setVerticalSpacing(0); mainLayout.setContentsMargins(0, 0, 0, 0); int row = 0; mainLayout.addWidget(&toggleButton, row, 0, 1, 1, Qt::AlignLeft); mainLayout.addWidget(&headerLine, row++, 2, 1, 1); mainLayout.addWidget(&contentArea, row, 0, 1, 3); setLayout(&mainLayout); connect(&toggleButton, &QToolButton::toggled, this, [this](const bool checked) { if (checked) { // make the content visible when expanding starts contentArea.setVisible(true); } // use instant animation if widget isn't visible (e.g. before widget is shown) const int duration = isVisible() ? animationDuration : 0; // update the size of the content area const auto collapsedHeight = sizeHint().height() - contentArea.maximumHeight(); const auto contentHeight = contentArea.layout()->sizeHint().height(); for (int i = 0; i < toggleAnimation.animationCount() - 1; ++i) { auto expanderAnimation = static_cast(toggleAnimation.animationAt(i)); expanderAnimation->setDuration(duration); expanderAnimation->setStartValue(collapsedHeight); expanderAnimation->setEndValue(collapsedHeight + contentHeight); } auto contentAnimation = static_cast(toggleAnimation.animationAt(toggleAnimation.animationCount() - 1)); contentAnimation->setDuration(duration); contentAnimation->setStartValue(0); contentAnimation->setEndValue(contentHeight); toggleButton.setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow); toggleAnimation.setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); toggleAnimation.start(); }); connect(&toggleAnimation, &QAbstractAnimation::finished, this, [this]() { // hide the content area when it is fully collapsed if (!toggleButton.isChecked()) { contentArea.setVisible(false); } }); } void AnimatedExpander::setContentLayout(QLayout *contentLayout) { delete contentArea.layout(); contentArea.setLayout(contentLayout); } bool AnimatedExpander::isExpanded() const { return toggleButton.isChecked(); } void AnimatedExpander::setExpanded(bool expanded) { toggleButton.setChecked(expanded); } class SecKeyFilter : public DefaultKeyFilter { public: SecKeyFilter() : DefaultKeyFilter() { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); setCanCertify(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::Set); } bool matches(const GpgME::Key &key, Kleo::KeyFilter::MatchContexts contexts) const override { if (!(availableMatchContexts() & contexts)) { return false; } if (_detail::ByFingerprint()(key, mExcludedKey)) { return false; } return DefaultKeyFilter::matches(key, contexts); } void setExcludedKey(const GpgME::Key &key) { mExcludedKey = key; } private: GpgME::Key mExcludedKey; }; auto checkBoxSize(const QCheckBox *checkBox) { QStyleOptionButton opt; return checkBox->style()->sizeFromContents(QStyle::CT_CheckBox, &opt, QSize(), checkBox); } -class TreeWidget : public NavigatableTreeWidget +class TreeWidgetInternal : public TreeWidget { Q_OBJECT public: - using NavigatableTreeWidget::NavigatableTreeWidget; + using TreeWidget::TreeWidget; protected: void focusInEvent(QFocusEvent *event) override { - NavigatableTreeWidget::focusInEvent(event); + TreeWidget::focusInEvent(event); // queue the invokation, so that it happens after the widget itself got focus - QMetaObject::invokeMethod(this, &TreeWidget::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &TreeWidgetInternal::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); } bool edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) override { if (event && event->type() == QEvent::KeyPress) { const auto *const keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Select) { // toggle checked state regardless of the index's column - return NavigatableTreeWidget::edit(index.siblingAtColumn(0), trigger, event); + return TreeWidget::edit(index.siblingAtColumn(0), trigger, event); } } - return NavigatableTreeWidget::edit(index, trigger, event); + return TreeWidget::edit(index, trigger, event); } private: void forceAccessibleFocusEventForCurrentItem() { // force Qt to send a focus event for the current item to accessibility // tools; otherwise, the user has no idea which item is selected when the // list gets keyboard input focus const auto current = currentIndex(); setCurrentIndex({}); setCurrentIndex(current); } }; struct UserIDCheckState { GpgME::UserID userId; Qt::CheckState checkState; }; } class CertifyWidget::Private { public: enum Role { UserIdRole = Qt::UserRole }; enum Mode { SingleCertification, BulkCertification, }; enum TagsState { TagsMustBeChecked, TagsLoading, TagsLoaded, }; Private(CertifyWidget *qq) : q{qq} { auto mainLay = new QVBoxLayout{q}; { mInfoLabel = new QLabel{i18n("Verify the fingerprint, mark the user IDs you want to certify, " "and select the key you want to certify the user IDs with.
" "Note: Only the fingerprint clearly identifies the key and its owner."), q}; mInfoLabel->setWordWrap(true); labelHelper.addLabel(mInfoLabel); mainLay->addWidget(mInfoLabel); } mainLay->addWidget(new KSeparator{Qt::Horizontal, q}); { auto grid = new QGridLayout; grid->setColumnStretch(1, 1); int row = -1; row++; mFprField = std::make_unique(i18n("Fingerprint:"), q); grid->addWidget(mFprField->label(), row, 0); grid->addLayout(mFprField->layout(), row, 1); row++; auto label = new QLabel{i18n("Certify with:"), q}; mSecKeySelect = new KeySelectionCombo{/* secretOnly= */ true, q}; mSecKeySelect->setKeyFilter(std::make_shared()); label->setBuddy(mSecKeySelect); grid->addWidget(label, row, 0); grid->addWidget(mSecKeySelect); mainLay->addLayout(grid); } mMissingOwnerTrustInfo = new KMessageWidget{q}; mSetOwnerTrustAction = new QAction{q}; mSetOwnerTrustAction->setText(i18nc("@action:button", "Set Owner Trust")); mSetOwnerTrustAction->setToolTip(i18nc("@info:tooltip", "Click to set the trust level of the selected certification key to ultimate trust. " "This is what you usually want to do for your own keys.")); connect(mSetOwnerTrustAction, &QAction::triggered, q, [this]() { setOwnerTrust(); }); mMissingOwnerTrustInfo->addAction(mSetOwnerTrustAction); mMissingOwnerTrustInfo->setVisible(false); mainLay->addWidget(mMissingOwnerTrustInfo); mainLay->addWidget(new KSeparator{Qt::Horizontal, q}); mBadCertificatesInfo = new KMessageWidget{q}; mBadCertificatesInfo->setMessageType(KMessageWidget::Warning); mBadCertificatesInfo->setIcon(QIcon::fromTheme(QStringLiteral("data-warning"), QIcon::fromTheme(QStringLiteral("dialog-warning")))); mBadCertificatesInfo->setText(i18nc("@info", "One or more certificates cannot be certified.")); mBadCertificatesInfo->setCloseButtonVisible(false); mBadCertificatesInfo->setVisible(false); mainLay->addWidget(mBadCertificatesInfo); userIdListView = new TreeWidget{q}; userIdListView->setAccessibleName(i18n("User IDs")); userIdListView->setEditTriggers(QAbstractItemView::NoEditTriggers); userIdListView->setSelectionMode(QAbstractItemView::SingleSelection); userIdListView->setRootIsDecorated(false); userIdListView->setUniformRowHeights(true); userIdListView->setAllColumnsShowFocus(false); userIdListView->setHeaderHidden(true); userIdListView->setHeaderLabels({i18nc("@title:column", "User ID")}); mainLay->addWidget(userIdListView, 1); // Setup the advanced area mAdvancedOptionsExpander = new AnimatedExpander{i18n("Advanced"), i18n("Show advanced options"), q}; mainLay->addWidget(mAdvancedOptionsExpander); auto advLay = new QVBoxLayout; mExportCB = new QCheckBox{q}; mExportCB->setText(i18n("Certify for everyone to see (exportable)")); mExportCB->setToolTip(xi18nc("@info:tooltip", "Check this option, if you want to share your certifications with others. " "If you just want to mark certificates as certified for yourself, then you can uncheck it.")); advLay->addWidget(mExportCB); { auto layout = new QHBoxLayout; mPublishCB = new QCheckBox{q}; mPublishCB->setText(i18n("Publish on keyserver afterwards")); mPublishCB->setToolTip(xi18nc("@info:tooltip", "Check this option, if you want to upload your certifications to a certificate " "directory after successful certification.")); mPublishCB->setEnabled(mExportCB->isChecked()); layout->addSpacing(checkBoxSize(mExportCB).width()); layout->addWidget(mPublishCB); advLay->addLayout(layout); } { auto tagsLay = new QHBoxLayout; auto label = new QLabel{i18n("Tags:"), q}; mTagsLE = new QLineEdit{q}; label->setBuddy(mTagsLE); const auto tooltip = i18n("You can use this to add additional info to a certification.") + QStringLiteral("

") + i18n("Tags created by anyone with full certification trust " "are shown in the keylist and can be searched."); label->setToolTip(tooltip); mTagsLE->setToolTip(tooltip); tagsLay->addWidget(label); tagsLay->addWidget(mTagsLE, 1); advLay->addLayout(tagsLay); } { auto layout = new QHBoxLayout; mExpirationCheckBox = new QCheckBox{q}; mExpirationCheckBox->setText(i18n("Expiration:")); mExpirationDateEdit = new KDateComboBox{q}; Kleo::setUpExpirationDateComboBox(mExpirationDateEdit, {QDate::currentDate().addDays(1), QDate{}}); mExpirationDateEdit->setDate(Kleo::defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); mExpirationDateEdit->setEnabled(mExpirationCheckBox->isChecked()); const auto tooltip = i18n("You can use this to set an expiration date for a certification.") + QStringLiteral("

") + i18n("By setting an expiration date, you can limit the validity of " "your certification to a certain amount of time. Once the expiration " "date has passed, your certification is no longer valid."); mExpirationCheckBox->setToolTip(tooltip); mExpirationDateEdit->setToolTip(tooltip); layout->addWidget(mExpirationCheckBox); layout->addWidget(mExpirationDateEdit, 1); advLay->addLayout(layout); } { mTrustSignatureCB = new QCheckBox{q}; mTrustSignatureWidgets.addWidget(mTrustSignatureCB); mTrustSignatureCB->setText(i18n("Certify as trusted introducer")); const auto tooltip = i18n("You can use this to certify a trusted introducer for a domain.") + QStringLiteral("

") + i18n("All certificates with email addresses belonging to the domain " "that have been certified by the trusted introducer are treated " "as certified, i.e. a trusted introducer acts as a kind of " "intermediate CA for a domain."); mTrustSignatureCB->setToolTip(tooltip); advLay->addWidget(mTrustSignatureCB); } { auto layout = new QHBoxLayout; auto label = new QLabel{i18n("Domain:"), q}; mTrustSignatureWidgets.addWidget(label); mTrustSignatureDomainLE = new QLineEdit{q}; mTrustSignatureWidgets.addWidget(mTrustSignatureDomainLE); mTrustSignatureDomainLE->setEnabled(mTrustSignatureCB->isChecked()); label->setBuddy(mTrustSignatureDomainLE); layout->addSpacing(checkBoxSize(mTrustSignatureCB).width()); layout->addWidget(label); layout->addWidget(mTrustSignatureDomainLE); advLay->addLayout(layout); } mAdvancedOptionsExpander->setContentLayout(advLay); connect(userIdListView, &QTreeWidget::itemChanged, q, [this](auto item, auto) { onItemChanged(item); }); connect(mExportCB, &QCheckBox::toggled, q, [this](bool on) { mPublishCB->setEnabled(on); }); connect(mSecKeySelect, &KeySelectionCombo::currentKeyChanged, q, [this](const GpgME::Key &) { updateSelectedUserIds(); updateTags(); checkOwnerTrust(); Q_EMIT q->changed(); }); connect(mExpirationCheckBox, &QCheckBox::toggled, q, [this](bool checked) { mExpirationDateEdit->setEnabled(checked); Q_EMIT q->changed(); }); connect(mExpirationDateEdit, &KDateComboBox::dateChanged, q, &CertifyWidget::changed); connect(mTrustSignatureCB, &QCheckBox::toggled, q, [this](bool on) { mTrustSignatureDomainLE->setEnabled(on); Q_EMIT q->changed(); }); connect(mTrustSignatureDomainLE, &QLineEdit::textChanged, q, &CertifyWidget::changed); loadConfig(true); } ~Private() = default; void loadConfig(bool loadAll = false) { const KConfigGroup conf(KSharedConfig::openConfig(), QStringLiteral("CertifySettings")); if (loadAll) { const Settings settings; mExpirationCheckBox->setChecked(settings.certificationValidityInDays() > 0); if (settings.certificationValidityInDays() > 0) { const QDate expirationDate = QDate::currentDate().addDays(settings.certificationValidityInDays()); mExpirationDateEdit->setDate(expirationDate > mExpirationDateEdit->maximumDate() // ? mExpirationDateEdit->maximumDate() // : expirationDate); } mSecKeySelect->setDefaultKey(conf.readEntry("LastKey", QString())); } switch (mMode) { case SingleCertification: { mExportCB->setChecked(conf.readEntry("ExportCheckState", false)); mPublishCB->setChecked(conf.readEntry("PublishCheckState", false)); mAdvancedOptionsExpander->setExpanded(conf.readEntry("AdvancedOptionsExpanded", false)); break; } case BulkCertification: { mExportCB->setChecked(conf.readEntry("BulkExportCheckState", true)); mPublishCB->setChecked(conf.readEntry("BulkPublishCheckState", false)); mAdvancedOptionsExpander->setExpanded(conf.readEntry("BulkAdvancedOptionsExpanded", true)); break; } } } void saveConfig() { KConfigGroup conf{KSharedConfig::openConfig(), QLatin1String("CertifySettings")}; if (!secKey().isNull()) { conf.writeEntry("LastKey", secKey().primaryFingerprint()); } switch (mMode) { case SingleCertification: { conf.writeEntry("ExportCheckState", mExportCB->isChecked()); conf.writeEntry("PublishCheckState", mPublishCB->isChecked()); conf.writeEntry("AdvancedOptionsExpanded", mAdvancedOptionsExpander->isExpanded()); break; } case BulkCertification: { conf.writeEntry("BulkExportCheckState", mExportCB->isChecked()); conf.writeEntry("BulkPublishCheckState", mPublishCB->isChecked()); conf.writeEntry("BulkAdvancedOptionsExpanded", mAdvancedOptionsExpander->isExpanded()); break; } } conf.sync(); } void setMode(Mode mode) { mMode = mode; switch (mMode) { case SingleCertification: break; case BulkCertification: { mInfoLabel->setText(i18nc("@info", "Verify the fingerprints, mark the user IDs you want to certify, " "and select the certificate you want to certify the user IDs with.
" "Note: Only the fingerprints clearly identify the certificate and its owner.")); mFprField->setVisible(false); mTrustSignatureWidgets.setVisible(false); break; } } loadConfig(); } void setUpUserIdList(const std::vector &uids = {}) { userIdListView->clear(); if (mMode == SingleCertification) { userIdListView->setColumnCount(1); userIdListView->setHeaderHidden(true); // set header labels for accessibility tools to overwrite the default "1" userIdListView->setHeaderLabels({i18nc("@title:column", "User ID")}); for (const auto &uid : uids) { if (uid.isInvalid() || Kleo::isRevokedOrExpired(uid)) { // Skip user IDs that cannot really be certified. continue; } auto item = new QTreeWidgetItem; item->setData(0, UserIdRole, QVariant::fromValue(uid)); item->setData(0, Qt::DisplayRole, Kleo::Formatting::prettyUserID(uid)); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); item->setCheckState(0, Qt::Checked); userIdListView->addTopLevelItem(item); } } else { const QStringList headers = {i18nc("@title:column", "User ID"), i18nc("@title:column", "Fingerprint")}; userIdListView->setColumnCount(headers.count()); userIdListView->setHeaderHidden(false); userIdListView->setHeaderLabels(headers); for (const auto &key : mKeys) { const auto &uid = key.userID(0); auto item = new QTreeWidgetItem; item->setData(0, UserIdRole, QVariant::fromValue(uid)); item->setData(0, Qt::DisplayRole, Kleo::Formatting::prettyUserID(uid)); item->setData(1, Qt::DisplayRole, Kleo::Formatting::prettyID(key.primaryFingerprint())); item->setData(1, Qt::AccessibleTextRole, Kleo::Formatting::accessibleHexID(key.primaryFingerprint())); if ((key.protocol() != OpenPGP) || uid.isInvalid() || Kleo::isRevokedOrExpired(uid)) { item->setFlags(Qt::NoItemFlags); item->setCheckState(0, Qt::Unchecked); if (key.protocol() == CMS) { item->setData(0, Qt::ToolTipRole, i18nc("@info:tooltip", "S/MIME certificates cannot be certified.")); item->setData(1, Qt::ToolTipRole, i18nc("@info:tooltip", "S/MIME certificates cannot be certified.")); } else { item->setData(0, Qt::ToolTipRole, i18nc("@info:tooltip", "Expired or revoked certificates cannot be certified.")); item->setData(1, Qt::ToolTipRole, i18nc("@info:tooltip", "Expired or revoked certificates cannot be certified.")); } } else { item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); item->setCheckState(0, Qt::Checked); } userIdListView->addTopLevelItem(item); } userIdListView->sortItems(0, Qt::AscendingOrder); userIdListView->resizeColumnToContents(0); userIdListView->resizeColumnToContents(1); } } void updateSelectedUserIds() { if (mMode == SingleCertification) { return; } if (userIdListView->topLevelItemCount() == 0) { return; } // restore check state of primary user ID of previous certification key if (!mCertificationKey.isNull()) { for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) { const auto uidItem = userIdListView->topLevelItem(i); const auto itemUserId = getUserId(uidItem); if (userIDBelongsToKey(itemUserId, mCertificationKey)) { uidItem->setCheckState(0, mCertificationKeyUserIDCheckState); uidItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); break; // we only show the primary user IDs } } } mCertificationKey = mSecKeySelect->currentKey(); // save and unset check state of primary user ID of current certification key if (!mCertificationKey.isNull()) { for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) { const auto uidItem = userIdListView->topLevelItem(i); const auto itemUserId = getUserId(uidItem); if (userIDBelongsToKey(itemUserId, mCertificationKey)) { mCertificationKeyUserIDCheckState = uidItem->checkState(0); if (mCertificationKeyUserIDCheckState) { uidItem->setCheckState(0, Qt::Unchecked); } uidItem->setFlags(Qt::ItemIsSelectable); break; // we only show the primary user IDs } } } } void updateTags() { struct ItemAndRemark { QTreeWidgetItem *item; QString remark; }; if (mTagsState != TagsLoaded) { return; } if (mTagsLE->isModified()) { return; } GpgME::Key remarkKey = mSecKeySelect->currentKey(); if (!remarkKey.isNull()) { std::vector itemsAndRemarks; // first choose the remark we want to prefill the Tags field with QString remark; for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) { const auto item = userIdListView->topLevelItem(i); if (item->isDisabled()) { continue; } const auto uid = getUserId(item); GpgME::Error err; const char *c_remark = uid.remark(remarkKey, err); const QString itemRemark = (!err && c_remark) ? QString::fromUtf8(c_remark) : QString{}; if (!itemRemark.isEmpty() && (itemRemark != remark)) { if (!remark.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "Different remarks on user IDs. Taking last."; } remark = itemRemark; } itemsAndRemarks.push_back({item, itemRemark}); } // then select the user IDs with the chosen remark; this prevents overwriting existing // different remarks on the other user IDs (as long as the user doesn't select any of // the unselected user IDs with a different remark) if (!remark.isEmpty()) { for (const auto &[item, itemRemark] : itemsAndRemarks) { item->setCheckState(0, itemRemark == remark ? Qt::Checked : Qt::Unchecked); } } mTagsLE->setText(remark); } } void updateTrustSignatureDomain() { if (mMode == SingleCertification) { if (mTrustSignatureDomainLE->text().isEmpty() && certificate().numUserIDs() == 1) { // try to guess the domain to use for the trust signature const auto address = certificate().userID(0).addrSpec(); const auto atPos = address.find('@'); if (atPos != std::string::npos) { const auto domain = address.substr(atPos + 1); mTrustSignatureDomainLE->setText(QString::fromUtf8(domain.c_str(), domain.size())); } } } } void loadAllTags() { const auto keyWithoutTags = std::find_if(mKeys.cbegin(), mKeys.cend(), [](const auto &key) { return (key.protocol() == GpgME::OpenPGP) && !(key.keyListMode() & GpgME::SignatureNotations); }); const auto indexOfKeyWithoutTags = std::distance(mKeys.cbegin(), keyWithoutTags); if (indexOfKeyWithoutTags < signed(mKeys.size())) { auto loadTags = [this, indexOfKeyWithoutTags]() { Q_ASSERT(indexOfKeyWithoutTags < signed(mKeys.size())); // call update() on the reference to the vector element because it swaps key with the updated key mKeys[indexOfKeyWithoutTags].update(); loadAllTags(); }; QMetaObject::invokeMethod(q, loadTags, Qt::QueuedConnection); return; } mTagsState = TagsLoaded; QMetaObject::invokeMethod( q, [this]() { setUpWidget(); }, Qt::QueuedConnection); } bool ensureTagsLoaded() { Q_ASSERT(mTagsState != TagsLoading); if (mTagsState == TagsLoaded) { return true; } const auto allTagsAreLoaded = Kleo::all_of(mKeys, [](const auto &key) { return (key.protocol() != GpgME::OpenPGP) || (key.keyListMode() & GpgME::SignatureNotations); }); if (allTagsAreLoaded) { mTagsState = TagsLoaded; } else { mTagsState = TagsLoading; QMetaObject::invokeMethod( q, [this]() { loadAllTags(); }, Qt::QueuedConnection); } return mTagsState == TagsLoaded; } void setUpWidget() { if (!ensureTagsLoaded()) { return; } if (mMode == SingleCertification) { const auto key = certificate(); mFprField->setValue(QStringLiteral("") + Formatting::prettyID(key.primaryFingerprint()) + QStringLiteral(""), Formatting::accessibleHexID(key.primaryFingerprint())); setUpUserIdList(mUserIds.empty() ? key.userIDs() : mUserIds); auto keyFilter = std::make_shared(); keyFilter->setExcludedKey(key); mSecKeySelect->setKeyFilter(keyFilter); updateTrustSignatureDomain(); } else { // check for certificates that cannot be certified const auto haveBadCertificates = Kleo::any_of(mKeys, [](const auto &key) { const auto &uid = key.userID(0); return (key.protocol() != OpenPGP) || uid.isInvalid() || Kleo::isRevokedOrExpired(uid); }); if (haveBadCertificates) { mBadCertificatesInfo->animatedShow(); } setUpUserIdList(); } updateTags(); updateSelectedUserIds(); Q_EMIT q->changed(); } GpgME::Key certificate() const { Q_ASSERT(mMode == SingleCertification); return !mKeys.empty() ? mKeys.front() : Key{}; } void setCertificates(const std::vector &keys, const std::vector &uids) { mKeys = keys; mUserIds = uids; mTagsState = TagsMustBeChecked; setUpWidget(); } std::vector certificates() const { Q_ASSERT(mMode != SingleCertification); return mKeys; } GpgME::Key secKey() const { return mSecKeySelect->currentKey(); } GpgME::UserID getUserId(const QTreeWidgetItem *item) const { return item ? item->data(0, UserIdRole).value() : UserID{}; } void selectUserIDs(const std::vector &uids) { for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) { const auto uidItem = userIdListView->topLevelItem(i); const auto itemUserId = getUserId(uidItem); const bool userIdIsInList = Kleo::any_of(uids, [itemUserId](const auto &uid) { return Kleo::userIDsAreEqual(itemUserId, uid); }); uidItem->setCheckState(0, userIdIsInList ? Qt::Checked : Qt::Unchecked); } } std::vector selectedUserIDs() const { std::vector userIds; userIds.reserve(userIdListView->topLevelItemCount()); for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) { const auto *const uidItem = userIdListView->topLevelItem(i); if (uidItem->checkState(0) == Qt::Checked) { userIds.push_back(getUserId(uidItem)); } } qCDebug(KLEOPATRA_LOG) << "Checked user IDs:" << userIds; return userIds; } bool exportableSelected() const { return mExportCB->isChecked(); } bool publishSelected() const { return mPublishCB->isChecked(); } QString tags() const { return mTagsLE->text().trimmed(); } bool isValid() const { static const QRegularExpression domainNameRegExp{QStringLiteral(R"(^\s*((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}\s*$)"), QRegularExpression::CaseInsensitiveOption}; if (mTagsState != TagsLoaded) { return false; } // do not accept null keys if (mKeys.empty() || mSecKeySelect->currentKey().isNull()) { return false; } // do not accept empty list of user IDs const auto userIds = selectedUserIDs(); if (userIds.empty()) { return false; } // do not accept if any of the selected user IDs belongs to the certification key const auto certificationKey = mSecKeySelect->currentKey(); const auto userIdToCertifyBelongsToCertificationKey = std::any_of(userIds.cbegin(), userIds.cend(), [certificationKey](const auto &userId) { return Kleo::userIDBelongsToKey(userId, certificationKey); }); if (userIdToCertifyBelongsToCertificationKey) { return false; } if (mExpirationCheckBox->isChecked() && !mExpirationDateEdit->isValid()) { return false; } if (mTrustSignatureCB->isChecked() && !domainNameRegExp.match(mTrustSignatureDomainLE->text()).hasMatch()) { return false; } return true; } void checkOwnerTrust() { const auto secretKey = secKey(); if (secretKey.ownerTrust() != GpgME::Key::Ultimate) { mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Information); mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("question"))); mMissingOwnerTrustInfo->setText(i18n("Is this your own key?")); mSetOwnerTrustAction->setEnabled(true); mMissingOwnerTrustInfo->animatedShow(); } else { mMissingOwnerTrustInfo->animatedHide(); } } void setOwnerTrust() { mSetOwnerTrustAction->setEnabled(false); QGpgME::ChangeOwnerTrustJob *const j = QGpgME::openpgp()->changeOwnerTrustJob(); connect(j, &QGpgME::ChangeOwnerTrustJob::result, q, [this](const GpgME::Error &err) { if (err) { KMessageBox::error(q, i18n("

Changing the certification trust of the key %1 failed:

%2

", Formatting::formatForComboBox(secKey()), Formatting::errorAsString(err)), i18nc("@title:window", "Certification Trust Change Failed")); } if (err || err.isCanceled()) { mSetOwnerTrustAction->setEnabled(true); } else { mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Positive); mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); mMissingOwnerTrustInfo->setText(i18n("Owner trust set successfully.")); } }); j->start(secKey(), GpgME::Key::Ultimate); } void onItemChanged(QTreeWidgetItem *item) { Q_EMIT q->changed(); #ifndef QT_NO_ACCESSIBILITY if (item) { // assume that the checked state changed QAccessible::State st; st.checked = true; QAccessibleStateChangeEvent e(userIdListView, st); e.setChild(userIdListView->indexOfTopLevelItem(item)); QAccessible::updateAccessibility(&e); } #endif } public: CertifyWidget *const q; QLabel *mInfoLabel = nullptr; std::unique_ptr mFprField; KeySelectionCombo *mSecKeySelect = nullptr; KMessageWidget *mMissingOwnerTrustInfo = nullptr; KMessageWidget *mBadCertificatesInfo = nullptr; - NavigatableTreeWidget *userIdListView = nullptr; + TreeWidget *userIdListView = nullptr; AnimatedExpander *mAdvancedOptionsExpander = nullptr; QCheckBox *mExportCB = nullptr; QCheckBox *mPublishCB = nullptr; QLineEdit *mTagsLE = nullptr; BulkStateChanger mTrustSignatureWidgets; QCheckBox *mTrustSignatureCB = nullptr; QLineEdit *mTrustSignatureDomainLE = nullptr; QCheckBox *mExpirationCheckBox = nullptr; KDateComboBox *mExpirationDateEdit = nullptr; QAction *mSetOwnerTrustAction = nullptr; LabelHelper labelHelper; Mode mMode = SingleCertification; std::vector mKeys; std::vector mUserIds; TagsState mTagsState = TagsMustBeChecked; GpgME::Key mCertificationKey; Qt::CheckState mCertificationKeyUserIDCheckState; }; CertifyWidget::CertifyWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } Kleo::CertifyWidget::~CertifyWidget() = default; void CertifyWidget::setCertificate(const GpgME::Key &key, const std::vector &uids) { Q_ASSERT(!key.isNull()); d->setMode(Private::SingleCertification); d->setCertificates({key}, uids); } GpgME::Key CertifyWidget::certificate() const { return d->certificate(); } void CertifyWidget::setCertificates(const std::vector &keys) { d->setMode(Private::BulkCertification); d->setCertificates(keys, {}); } std::vector CertifyWidget::certificates() const { return d->certificates(); } void CertifyWidget::selectUserIDs(const std::vector &uids) { d->selectUserIDs(uids); } std::vector CertifyWidget::selectedUserIDs() const { return d->selectedUserIDs(); } GpgME::Key CertifyWidget::secKey() const { return d->secKey(); } bool CertifyWidget::exportableSelected() const { return d->exportableSelected(); } QString CertifyWidget::tags() const { return d->tags(); } bool CertifyWidget::publishSelected() const { return d->publishSelected(); } bool CertifyWidget::trustSignatureSelected() const { return d->mTrustSignatureCB->isChecked(); } QString CertifyWidget::trustSignatureDomain() const { return d->mTrustSignatureDomainLE->text().trimmed(); } QDate CertifyWidget::expirationDate() const { return d->mExpirationCheckBox->isChecked() ? d->mExpirationDateEdit->date() : QDate{}; } bool CertifyWidget::isValid() const { return d->isValid(); } void CertifyWidget::saveState() const { d->saveConfig(); } #include "certifywidget.moc" #include "moc_certifywidget.cpp" diff --git a/src/dialogs/selftestdialog.cpp b/src/dialogs/selftestdialog.cpp index 26e04091c..03051c519 100644 --- a/src/dialogs/selftestdialog.cpp +++ b/src/dialogs/selftestdialog.cpp @@ -1,518 +1,518 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/selftestdialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "selftestdialog.h" #include #include #include -#include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Dialogs; namespace { class Model : public QAbstractTableModel { Q_OBJECT public: explicit Model(QObject *parent = nullptr) : QAbstractTableModel(parent) , m_tests() { } enum Column { TestName, TestResult, NumColumns }; const std::shared_ptr &fromModelIndex(const QModelIndex &idx) const { const unsigned int row = idx.row(); if (row < m_tests.size()) { return m_tests[row]; } static const std::shared_ptr null; return null; } int rowCount(const QModelIndex &idx) const override { return idx.isValid() ? 0 : m_tests.size(); } int columnCount(const QModelIndex &) const override { return NumColumns; } QVariant data(const QModelIndex &idx, int role) const override { const unsigned int row = idx.row(); if (idx.isValid() && row < m_tests.size()) switch (role) { case Qt::DisplayRole: case Qt::ToolTipRole: switch (idx.column()) { case TestName: return m_tests[row]->name(); case TestResult: return m_tests[row]->skipped() ? i18n("Skipped") // : m_tests[row]->passed() ? i18n("Passed") : m_tests[row]->shortError(); } break; case Qt::BackgroundRole: if (!SystemInfo::isHighContrastModeActive()) { KColorScheme scheme(qApp->palette().currentColorGroup()); return (m_tests[row]->skipped() ? scheme.background(KColorScheme::NeutralBackground) : m_tests[row]->passed() ? scheme.background(KColorScheme::PositiveBackground) : scheme.background(KColorScheme::NegativeBackground)) .color(); } } return QVariant(); } QVariant headerData(int section, Qt::Orientation o, int role) const override { if (o == Qt::Horizontal && section >= 0 && section < NumColumns && role == Qt::DisplayRole) switch (section) { case TestName: return i18n("Test Name"); case TestResult: return i18n("Result"); } return QVariant(); } void clear() { if (m_tests.empty()) { return; } beginRemoveRows(QModelIndex(), 0, m_tests.size() - 1); m_tests.clear(); endRemoveRows(); } void append(const std::vector> &tests) { if (tests.empty()) { return; } beginInsertRows(QModelIndex(), m_tests.size(), m_tests.size() + tests.size()); m_tests.insert(m_tests.end(), tests.begin(), tests.end()); endInsertRows(); } void reloadData() { if (!m_tests.empty()) { Q_EMIT dataChanged(index(0, 0), index(m_tests.size() - 1, NumColumns - 1)); } } const std::shared_ptr &at(unsigned int idx) const { return m_tests.at(idx); } private: std::vector> m_tests; }; class Proxy : public QSortFilterProxyModel { Q_OBJECT public: explicit Proxy(QObject *parent = nullptr) : QSortFilterProxyModel(parent) , m_showAll(true) { setDynamicSortFilter(true); } bool showAll() const { return m_showAll; } Q_SIGNALS: void showAllChanged(bool); public Q_SLOTS: void setShowAll(bool on) { if (on == m_showAll) { return; } m_showAll = on; invalidateFilter(); Q_EMIT showAllChanged(on); } private: bool filterAcceptsRow(int src_row, const QModelIndex &src_parent) const override { if (m_showAll) { return true; } if (const Model *const model = qobject_cast(sourceModel())) { if (!src_parent.isValid() && src_row >= 0 && src_row < model->rowCount(src_parent)) { if (const std::shared_ptr &t = model->at(src_row)) { return !t->passed(); } else { qCWarning(KLEOPATRA_LOG) << "NULL test??"; } } else { if (src_parent.isValid()) { qCWarning(KLEOPATRA_LOG) << "view asks for subitems!"; } else { qCWarning(KLEOPATRA_LOG) << "index " << src_row << " is out of range [" << 0 << "," << model->rowCount(src_parent) << "]"; } } } else { qCWarning(KLEOPATRA_LOG) << "expected a ::Model, got "; if (!sourceModel()) { qCWarning(KLEOPATRA_LOG) << "a null pointer"; } else { qCWarning(KLEOPATRA_LOG) << sourceModel()->metaObject()->className(); } } return false; } private: bool m_showAll; }; -class TreeView : public NavigatableTreeView +class TreeViewInternal : public TreeView { Q_OBJECT public: - using NavigatableTreeView::NavigatableTreeView; + using TreeView::TreeView; protected: void focusInEvent(QFocusEvent *event) override { - NavigatableTreeView::focusInEvent(event); + TreeView::focusInEvent(event); // queue the invokation, so that it happens after the widget itself got focus - QMetaObject::invokeMethod(this, &TreeView::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &TreeViewInternal::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); } private: void forceAccessibleFocusEventForCurrentItem() { // force Qt to send a focus event for the current item to accessibility // tools; otherwise, the user has no idea which item is selected when the // list gets keyboard input focus const auto current = currentIndex(); setCurrentIndex({}); setCurrentIndex(current); } }; } class SelfTestDialog::Private { friend class ::Kleo::Dialogs::SelfTestDialog; SelfTestDialog *const q; public: explicit Private(SelfTestDialog *qq) : q(qq) , model(q) , proxy(q) , ui(q) { proxy.setSourceModel(&model); ui.resultsTV->setModel(&proxy); ui.detailsGB->hide(); ui.proposedCorrectiveActionGB->hide(); connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); connect(ui.doItPB, &QAbstractButton::clicked, q, [this]() { slotDoItClicked(); }); connect(ui.rerunPB, &QAbstractButton::clicked, q, &SelfTestDialog::updateRequested); connect(ui.resultsTV->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() { slotSelectionChanged(); }); connect(ui.showAllCB, &QAbstractButton::toggled, q, [this](bool checked) { proxy.setShowAll(checked); if (checked) { updateColumnSizes(); } ensureCurrentItemIsVisible(); }); proxy.setShowAll(ui.showAllCB->isChecked()); ui.resultsTV->setFocus(); } private: void slotSelectionChanged() { const int row = selectedRowIndex(); if (row < 0) { ui.detailsLB->setText(i18n("(select test first)")); ui.detailsGB->hide(); ui.proposedCorrectiveActionGB->hide(); } else { const std::shared_ptr &t = model.at(row); ui.detailsLB->setText(t->longError()); ui.detailsGB->setVisible(!t->passed()); const QString action = t->proposedFix(); ui.proposedCorrectiveActionGB->setVisible(!t->passed() && !action.isEmpty()); ui.proposedCorrectiveActionLB->setText(action); ui.doItPB->setVisible(!t->passed() && t->canFixAutomatically()); QMetaObject::invokeMethod( q, [this]() { ensureCurrentItemIsVisible(); }, Qt::QueuedConnection); } } void slotDoItClicked() { if (const std::shared_ptr st = model.fromModelIndex(selectedRow())) if (st->fix()) { model.reloadData(); } } private: void ensureCurrentItemIsVisible() { ui.resultsTV->scrollTo(ui.resultsTV->currentIndex()); } void updateColumnSizes() { ui.resultsTV->header()->resizeSections(QHeaderView::ResizeToContents); } private: QModelIndex selectedRow() const { const QItemSelectionModel *const ism = ui.resultsTV->selectionModel(); if (!ism) { return QModelIndex(); } const QModelIndexList mil = ism->selectedRows(); return mil.empty() ? QModelIndex() : proxy.mapToSource(mil.front()); } int selectedRowIndex() const { return selectedRow().row(); } private: Model model; Proxy proxy; struct UI { - TreeView *resultsTV = nullptr; + TreeViewInternal *resultsTV = nullptr; QCheckBox *showAllCB = nullptr; QGroupBox *detailsGB = nullptr; QLabel *detailsLB = nullptr; QGroupBox *proposedCorrectiveActionGB = nullptr; QLabel *proposedCorrectiveActionLB = nullptr; QPushButton *doItPB = nullptr; QCheckBox *runAtStartUpCB; QDialogButtonBox *buttonBox; QPushButton *rerunPB = nullptr; LabelHelper labelHelper; explicit UI(SelfTestDialog *qq) { auto mainLayout = new QVBoxLayout{qq}; { auto label = new QLabel{xi18n("These are the results of the Kleopatra self-test suite. Click on a test for details." "Note that all but the first failure might be due to prior tests failing."), qq}; label->setWordWrap(true); labelHelper.addLabel(label); mainLayout->addWidget(label); } auto splitter = new QSplitter{qq}; splitter->setOrientation(Qt::Vertical); { auto widget = new QWidget{qq}; auto vbox = new QVBoxLayout{widget}; vbox->setContentsMargins(0, 0, 0, 0); - resultsTV = new TreeView{qq}; + resultsTV = new TreeViewInternal{qq}; resultsTV->setAccessibleName(i18n("test results")); QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(1); sizePolicy.setHeightForWidth(resultsTV->sizePolicy().hasHeightForWidth()); resultsTV->setSizePolicy(sizePolicy); resultsTV->setMinimumHeight(100); resultsTV->setRootIsDecorated(false); resultsTV->setAllColumnsShowFocus(true); vbox->addWidget(resultsTV); splitter->addWidget(widget); } { detailsGB = new QGroupBox{i18nc("@title:group", "Details"), qq}; auto groupBoxLayout = new QVBoxLayout{detailsGB}; auto scrollArea = new Kleo::ScrollArea{qq}; scrollArea->setFocusPolicy(Qt::NoFocus); scrollArea->setMinimumHeight(100); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); auto scrollAreaLayout = qobject_cast(scrollArea->widget()->layout()); detailsLB = new QLabel{qq}; detailsLB->setTextFormat(Qt::RichText); detailsLB->setTextInteractionFlags(Qt::TextSelectableByMouse); detailsLB->setWordWrap(true); labelHelper.addLabel(detailsLB); scrollAreaLayout->addWidget(detailsLB); scrollAreaLayout->addStretch(); groupBoxLayout->addWidget(scrollArea); splitter->addWidget(detailsGB); } { proposedCorrectiveActionGB = new QGroupBox{i18nc("@title:group", "Proposed Corrective Action"), qq}; auto groupBoxLayout = new QVBoxLayout{proposedCorrectiveActionGB}; auto scrollArea = new Kleo::ScrollArea{qq}; scrollArea->setFocusPolicy(Qt::NoFocus); scrollArea->setMinimumHeight(100); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); auto scrollAreaLayout = qobject_cast(scrollArea->widget()->layout()); proposedCorrectiveActionLB = new QLabel{qq}; proposedCorrectiveActionLB->setTextFormat(Qt::RichText); proposedCorrectiveActionLB->setTextInteractionFlags(Qt::TextSelectableByMouse); proposedCorrectiveActionLB->setWordWrap(true); labelHelper.addLabel(proposedCorrectiveActionLB); scrollAreaLayout->addWidget(proposedCorrectiveActionLB); scrollAreaLayout->addStretch(); groupBoxLayout->addWidget(scrollArea); { auto hbox = new QHBoxLayout; hbox->addStretch(); doItPB = new QPushButton{i18nc("@action:button", "Do It"), qq}; doItPB->setEnabled(false); hbox->addWidget(doItPB); groupBoxLayout->addLayout(hbox); } splitter->addWidget(proposedCorrectiveActionGB); } mainLayout->addWidget(splitter); showAllCB = new QCheckBox{i18nc("@option:check", "Show all test results"), qq}; showAllCB->setChecked(true); mainLayout->addWidget(showAllCB); runAtStartUpCB = new QCheckBox{i18nc("@option:check", "Run these tests at startup"), qq}; runAtStartUpCB->setChecked(true); mainLayout->addWidget(runAtStartUpCB); buttonBox = new QDialogButtonBox{qq}; buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Close | QDialogButtonBox::Ok); buttonBox->button(QDialogButtonBox::Ok)->setText(i18nc("@action:button", "Continue")); rerunPB = buttonBox->addButton(i18nc("@action:button", "Rerun Tests"), QDialogButtonBox::ActionRole); mainLayout->addWidget(buttonBox); } } ui; }; SelfTestDialog::SelfTestDialog(QWidget *p, Qt::WindowFlags f) : QDialog(p, f) , d(new Private(this)) { setWindowTitle(i18nc("@title:window", "Self Test")); resize(448, 610); setAutomaticMode(false); } SelfTestDialog::~SelfTestDialog() = default; void SelfTestDialog::setTests(const std::vector> &tests) { d->model.clear(); d->model.append(tests); d->updateColumnSizes(); } void SelfTestDialog::setRunAtStartUp(bool on) { d->ui.runAtStartUpCB->setChecked(on); } bool SelfTestDialog::runAtStartUp() const { return d->ui.runAtStartUpCB->isChecked(); } void SelfTestDialog::setAutomaticMode(bool automatic) { d->ui.showAllCB->setChecked(!automatic); d->ui.buttonBox->button(QDialogButtonBox::Ok)->setVisible(automatic); d->ui.buttonBox->button(QDialogButtonBox::Cancel)->setVisible(automatic); d->ui.buttonBox->button(QDialogButtonBox::Close)->setVisible(!automatic); } #include "moc_selftestdialog.cpp" #include "selftestdialog.moc" diff --git a/src/dialogs/subkeyswidget.cpp b/src/dialogs/subkeyswidget.cpp index de7a93018..b1b54365f 100644 --- a/src/dialogs/subkeyswidget.cpp +++ b/src/dialogs/subkeyswidget.cpp @@ -1,413 +1,413 @@ /* 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" #include "commands/exportsecretsubkeycommand.h" #include "commands/importpaperkeycommand.h" #include "commands/keytocardcommand.h" #include "exportdialog.h" #include #include #include #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); }); connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { keysMayHaveChanged(); }); } void changeValidity(const GpgME::Subkey &subkey); void exportSSH(const GpgME::Subkey &subkey); void keyToCard(const GpgME::Subkey &subkey); void exportSecret(const GpgME::Subkey &subkey); void importPaperKey(); private: void tableContextMenuRequested(const QPoint &p); void keysMayHaveChanged(); public: GpgME::Key key; public: struct UI { QVBoxLayout *mainLayout; - NavigatableTreeWidget *subkeysTree; + TreeWidget *subkeysTree; QPushButton *changeValidityBtn = nullptr; QPushButton *exportOpenSSHBtn = nullptr; QPushButton *restoreBtn = nullptr; QPushButton *transferToSmartcardBtn = nullptr; QPushButton *exportSecretBtn = nullptr; 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}; + subkeysTree = new TreeWidget{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); { auto buttonRow = new QHBoxLayout; changeValidityBtn = new QPushButton(i18nc("@action:button", "Change validity"), widget); buttonRow->addWidget(changeValidityBtn); exportOpenSSHBtn = new QPushButton{i18nc("@action:button", "Export OpenSSH key"), widget}; buttonRow->addWidget(exportOpenSSHBtn); restoreBtn = new QPushButton(i18nc("@action:button", "Restore printed backup"), widget); buttonRow->addWidget(restoreBtn); transferToSmartcardBtn = new QPushButton(i18nc("@action:button", "Transfer to smartcard"), widget); buttonRow->addWidget(transferToSmartcardBtn); exportSecretBtn = new QPushButton(i18nc("@action:button", "Export secret subkey"), widget); buttonRow->addWidget(exportSecretBtn); buttonRow->addStretch(1); mainLayout->addLayout(buttonRow); } } } ui; }; void SubKeysWidget::Private::changeValidity(const GpgME::Subkey &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(); } void SubKeysWidget::Private::exportSSH(const GpgME::Subkey &subkey) { QScopedPointer dlg(new ExportDialog(q)); dlg->setKey(subkey, static_cast(GpgME::Context::ExportSSH)); dlg->exec(); } void SubKeysWidget::Private::importPaperKey() { auto cmd = new ImportPaperKeyCommand(key); ui.subkeysTree->setEnabled(false); connect(cmd, &ImportPaperKeyCommand::finished, q, [this]() { ui.subkeysTree->setEnabled(true); }); cmd->setParentWidget(q); cmd->start(); } void SubKeysWidget::Private::keyToCard(const GpgME::Subkey &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(); } void SubKeysWidget::Private::exportSecret(const GpgME::Subkey &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(); } 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 isOwnKey = subkey.parent().hasSecret(); const bool secretSubkeyStoredInKeyRing = subkey.isSecret() && !subkey.isCardKey(); auto menu = new QMenu(q); connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); if (isOwnKey) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("change-date-symbolic")), i18n("Change validity"), q, [this, subkey]() { changeValidity(subkey); }); action->setEnabled(canBeUsedForSecretKeyOperations(subkey.parent())); } if (subkey.canAuthenticate()) { menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export OpenSSH key"), q, [this, subkey]() { exportSSH(subkey); }); } auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-import")), i18n("Restore printed backup"), q, [this, subkey]() { importPaperKey(); }); action->setEnabled(!secretSubkeyStoredInKeyRing); if (isOwnKey) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("send-to-symbolic")), i18n("Transfer to smartcard"), q, [this, subkey]() { keyToCard(subkey); }); action->setEnabled(secretSubkeyStoredInKeyRing && !KeyToCardCommand::getSuitableCards(subkey).empty()); } const bool isPrimarySubkey = subkey.keyID() == key.keyID(); if (isOwnKey && !isPrimarySubkey) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export secret subkey"), q, [this, subkey]() { exportSecret(subkey); }); action->setEnabled(secretSubkeyStoredInKeyRing); } menu->popup(ui.subkeysTree->viewport()->mapToGlobal(p)); } void SubKeysWidget::Private::keysMayHaveChanged() { qCDebug(KLEOPATRA_LOG) << q << __func__; const auto updatedKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint()); if (!updatedKey.isNull()) { q->setKey(updatedKey); } } SubKeysWidget::SubKeysWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { - connect(d->ui.subkeysTree, &NavigatableTreeWidget::currentItemChanged, this, [this] { + connect(d->ui.subkeysTree, &TreeWidget::currentItemChanged, this, [this] { const auto currentIndex = d->ui.subkeysTree->currentIndex().row(); const auto &subkey = d->key.subkey(currentIndex); const bool secretSubkeyStoredInKeyRing = subkey.isSecret() && !subkey.isCardKey(); d->ui.exportOpenSSHBtn->setEnabled(subkey.canAuthenticate()); d->ui.changeValidityBtn->setEnabled(d->key.hasSecret() && canBeUsedForSecretKeyOperations(subkey.parent())); d->ui.exportSecretBtn->setEnabled(d->key.hasSecret() && subkey.fingerprint() != d->key.primaryFingerprint() && secretSubkeyStoredInKeyRing); d->ui.restoreBtn->setEnabled(!secretSubkeyStoredInKeyRing); d->ui.transferToSmartcardBtn->setEnabled(secretSubkeyStoredInKeyRing && !KeyToCardCommand::getSuitableCards(subkey).empty()); }); connect(d->ui.changeValidityBtn, &QPushButton::clicked, this, [this] { d->changeValidity(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); }); connect(d->ui.exportOpenSSHBtn, &QPushButton::clicked, this, [this] { d->exportSSH(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); }); connect(d->ui.restoreBtn, &QPushButton::clicked, this, [this] { d->importPaperKey(); }); connect(d->ui.transferToSmartcardBtn, &QPushButton::clicked, this, [this] { d->keyToCard(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); }); connect(d->ui.exportSecretBtn, &QPushButton::clicked, this, [this] { d->exportSecret(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); }); } SubKeysWidget::~SubKeysWidget() = default; void SubKeysWidget::setKey(const GpgME::Key &key) { if (key.protocol() != GpgME::OpenPGP) { return; } 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(); const auto subkeys = key.subkeys(); for (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, subkey.neverExpires() ? Kleo::Formatting::expirationDateString(subkey.parent()) : Kleo::Formatting::expirationDateString(subkey)); item->setData(3, Qt::AccessibleTextRole, subkey.neverExpires() ? Kleo::Formatting::accessibleExpirationDate(subkey.parent()) : Kleo::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); d->ui.changeValidityBtn->setVisible(key.hasSecret()); d->ui.exportSecretBtn->setVisible(key.hasSecret()); d->ui.transferToSmartcardBtn->setVisible(key.hasSecret()); } 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(), QStringLiteral("SubKeysDialog")); const QSize size = dialog.readEntry("Size", QSize(820, 280)); if (size.isValid()) { resize(size); } } void SubKeysDialog::writeConfig() { KConfigGroup dialog(KSharedConfig::openStateConfig(), QStringLiteral("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(); } #include "moc_subkeyswidget.cpp" diff --git a/src/dialogs/weboftrustwidget.cpp b/src/dialogs/weboftrustwidget.cpp index 9d66e35ae..975143b54 100644 --- a/src/dialogs/weboftrustwidget.cpp +++ b/src/dialogs/weboftrustwidget.cpp @@ -1,346 +1,346 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "weboftrustwidget.h" #include "commands/certifycertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "utils/tags.h" #include #include #include -#include +#include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; class WebOfTrustWidget::Private { friend class ::Kleo::WebOfTrustWidget; WebOfTrustWidget *const q; private: GpgME::Key key; UserIDListModel certificationsModel; QGpgME::KeyListJob *keyListJob = nullptr; - NavigatableTreeView *certificationsTV = nullptr; + TreeView *certificationsTV = nullptr; QAction *detailsAction = nullptr; QAction *certifyAction = nullptr; QAction *revokeAction = nullptr; public: Private(WebOfTrustWidget *qq) : q{qq} { certificationsModel.enableRemarks(Tags::tagsEnabled()); - certificationsTV = new NavigatableTreeView{q}; + certificationsTV = new TreeView{q}; certificationsTV->setAccessibleName(i18n("User IDs and certifications")); certificationsTV->setModel(&certificationsModel); certificationsTV->setAllColumnsShowFocus(false); certificationsTV->setSelectionMode(QAbstractItemView::SingleSelection); if (!Tags::tagsEnabled()) { certificationsTV->hideColumn(static_cast(UserIDListModel::Column::Tags)); } auto vLay = new QVBoxLayout(q); vLay->setContentsMargins(0, 0, 0, 0); vLay->addWidget(certificationsTV); detailsAction = new QAction{QIcon::fromTheme(QStringLiteral("dialog-information")), i18nc("@action", "Show Certificate Details"), q}; connect(detailsAction, &QAction::triggered, q, [this]() { showCertificateDetails(); }); certifyAction = new QAction{QIcon::fromTheme(QStringLiteral("view-certificate-sign")), i18nc("@action", "Add Certification"), q}; connect(certifyAction, &QAction::triggered, q, [this]() { addCertification(); }); if (Kleo::Commands::RevokeCertificationCommand::isSupported()) { revokeAction = new QAction{QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), i18nc("@action", "Revoke Certification"), q}; connect(revokeAction, &QAction::triggered, q, [this]() { revokeCertification(); }); } connect(certificationsTV, &QAbstractItemView::doubleClicked, q, [this]() { certificationDblClicked(); }); certificationsTV->setContextMenuPolicy(Qt::CustomContextMenu); connect(certificationsTV, &QWidget::customContextMenuRequested, q, [this](const QPoint &p) { contextMenuRequested(p); }); connect(certificationsTV->selectionModel(), &QItemSelectionModel::currentRowChanged, q, [this]() { updateActions(); }); updateActions(); } GpgME::UserID selectedUserID() { return certificationsModel.userID(certificationsTV->currentIndex()); } GpgME::UserID::Signature selectedCertification() { return certificationsModel.signature(certificationsTV->currentIndex()); } void certificationDblClicked() { showCertificateDetails(); } void showCertificateDetails() { const auto signature = selectedCertification(); if (signature.isNull()) { qCDebug(KLEOPATRA_LOG) << __func__ << "- no certification selected"; return; } auto cmd = Command::commandForQuery(QString::fromUtf8(signature.signerKeyID())); cmd->setParentWId(q->winId()); cmd->start(); } void addCertification() { auto userID = selectedUserID(); if (userID.isNull()) { userID = selectedCertification().parent(); } if (userID.isNull()) { qCDebug(KLEOPATRA_LOG) << __func__ << "- no user ID or certification selected"; return; } auto cmd = new Kleo::Commands::CertifyCertificateCommand(userID); cmd->setParentWidget(q); certificationsTV->setEnabled(false); connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this]() { certificationsTV->setEnabled(true); // Trigger an update when done q->setKey(key); }); cmd->start(); } void revokeCertification() { Command *cmd = nullptr; if (const auto signature = selectedCertification(); !signature.isNull()) { cmd = new Kleo::Commands::RevokeCertificationCommand(signature); } else if (const auto userID = selectedUserID(); !userID.isNull()) { cmd = new Kleo::Commands::RevokeCertificationCommand(userID); } else { qCDebug(KLEOPATRA_LOG) << __func__ << "- no user ID or certification selected"; return; } cmd->setParentWidget(q); certificationsTV->setEnabled(false); connect(cmd, &Kleo::Commands::RevokeCertificationCommand::finished, q, [this]() { certificationsTV->setEnabled(true); // Trigger an update when done q->setKey(key); }); cmd->start(); } void addActionsForUserID(QMenu *menu) { menu->addAction(certifyAction); if (revokeAction) { menu->addAction(revokeAction); } } void addActionsForSignature(QMenu *menu) { menu->addAction(detailsAction); menu->addAction(certifyAction); if (revokeAction) { menu->addAction(revokeAction); if (!revokeAction->isEnabled()) { menu->setToolTipsVisible(true); } } } void updateActions() { const auto userCanSignUserIDs = userHasCertificationKey(); const auto keyCanBeCertified = Kleo::canBeCertified(key); const auto userID = selectedUserID(); const auto signature = selectedCertification(); detailsAction->setEnabled(!signature.isNull()); certifyAction->setEnabled(keyCanBeCertified && userCanSignUserIDs && (!userID.isNull() || !signature.isNull())); if (revokeAction) { revokeAction->setToolTip({}); if (!signature.isNull()) { const auto revocationFeasibility = userCanRevokeCertification(signature); revokeAction->setEnabled(revocationFeasibility == CertificationCanBeRevoked); switch (revocationFeasibility) { case CertificationCanBeRevoked: break; case CertificationNotMadeWithOwnKey: revokeAction->setToolTip( i18n("You cannot revoke this certification because it wasn't made with one of your keys (or the required secret key is missing).")); break; case CertificationIsSelfSignature: revokeAction->setToolTip(i18n("Revocation of self-certifications is currently not possible.")); break; case CertificationIsRevocation: revokeAction->setToolTip(i18n("You cannot revoke this revocation certification. (But you can re-certify the corresponding user ID.)")); break; case CertificationIsExpired: revokeAction->setToolTip(i18n("You cannot revoke this expired certification.")); break; case CertificationIsInvalid: revokeAction->setToolTip(i18n("You cannot revoke this invalid certification.")); break; case CertificationKeyNotAvailable: revokeAction->setToolTip(i18n("You cannot revoke this certification because the required secret key is not available.")); break; }; } else if (!userID.isNull()) { const bool canRevokeCertification = userCanRevokeCertifications(userID); revokeAction->setEnabled(canRevokeCertification); if (!canRevokeCertification) { revokeAction->setToolTip( i18n("You cannot revoke any of the certifications of this user ID. Select any of the certifications for details.")); } } else { revokeAction->setEnabled(false); } } } void contextMenuRequested(const QPoint &p) { const auto index = certificationsTV->indexAt(p); const auto userID = certificationsModel.userID(index); const auto signature = certificationsModel.signature(index); if (userID.isNull() && signature.isNull()) { return; } auto menu = new QMenu(q); if (!userID.isNull()) { addActionsForUserID(menu); } else if (!signature.isNull()) { addActionsForSignature(menu); } connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); menu->popup(certificationsTV->viewport()->mapToGlobal(p)); } void startSignatureListing() { if (keyListJob) { return; } QGpgME::KeyListJob *const job = QGpgME::openpgp()->keyListJob(/*remote*/ false, /*includeSigs*/ true, /*validate*/ true); if (!job) { return; } if (Tags::tagsEnabled()) { job->addMode(GpgME::SignatureNotations); } connect(job, &QGpgME::KeyListJob::result, q, &WebOfTrustWidget::signatureListingDone); connect(job, &QGpgME::KeyListJob::nextKey, q, &WebOfTrustWidget::signatureListingNextKey); job->start(QStringList(QString::fromLatin1(key.primaryFingerprint()))); keyListJob = job; } }; WebOfTrustWidget::WebOfTrustWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } WebOfTrustWidget::~WebOfTrustWidget() = default; QAction *WebOfTrustWidget::detailsAction() const { return d->detailsAction; } QAction *WebOfTrustWidget::certifyAction() const { return d->certifyAction; } QAction *WebOfTrustWidget::revokeAction() const { return d->revokeAction; } GpgME::Key WebOfTrustWidget::key() const { return d->key; } void WebOfTrustWidget::setKey(const GpgME::Key &key) { if (key.protocol() != GpgME::OpenPGP) { qCDebug(KLEOPATRA_LOG) << "List of Certifications is only supported for OpenPGP keys"; return; } d->key = key; d->certificationsModel.setKey(key); d->updateActions(); d->certificationsTV->expandAll(); d->certificationsTV->header()->resizeSections(QHeaderView::ResizeToContents); d->startSignatureListing(); } void WebOfTrustWidget::signatureListingNextKey(const GpgME::Key &key) { GpgME::Key merged = key; merged.mergeWith(d->key); setKey(merged); } void WebOfTrustWidget::signatureListingDone(const GpgME::KeyListResult &result) { if (result.error()) { KMessageBox::information(this, xi18nc("@info", "An error occurred while loading the certifications: " "%1", Formatting::errorAsString(result.error())), i18nc("@title", "Certifications Loading Failed")); } d->keyListJob = nullptr; } #include "moc_weboftrustwidget.cpp" diff --git a/src/view/keytreeview.cpp b/src/view/keytreeview.cpp index b6355a204..d08ba4142 100644 --- a/src/view/keytreeview.cpp +++ b/src/view/keytreeview.cpp @@ -1,768 +1,736 @@ /* -*- mode: c++; c-basic-offset:4 -*- view/keytreeview.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keytreeview.h" #include "searchbar.h" #include #include #include #include -#include #include +#include #include "utils/headerview.h" #include "utils/tags.h" #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #define TAGS_COLUMN 13 using namespace Kleo; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) namespace { -class TreeView : public NavigatableTreeView +class TreeViewInternal : public Kleo::TreeView { public: - explicit TreeView(QWidget *parent = nullptr) - : NavigatableTreeView{parent} + explicit TreeViewInternal(QWidget *parent = nullptr) + : Kleo::TreeView{parent} { - header()->installEventFilter(this); + connect(this, &TreeView::columnEnabled, this, [this](int column) { + if (column == TAGS_COLUMN) { + Tags::enableTags(); + } + auto tv = qobject_cast(this->parent()); + if (tv) { + tv->resizeColumns(); + } + }); + connect(this, &TreeView::columnDisabled, this, [this]() { + auto tv = qobject_cast(this->parent()); + if (tv) { + tv->resizeColumns(); + } + }); } QSize minimumSizeHint() const override { const QSize min = QTreeView::minimumSizeHint(); return QSize(min.width(), min.height() + 5 * fontMetrics().height()); } protected: - bool eventFilter(QObject *watched, QEvent *event) override - { - Q_UNUSED(watched) - if (event->type() == QEvent::ContextMenu) { - auto e = static_cast(event); - - if (!mHeaderPopup) { - mHeaderPopup = new QMenu(this); - mHeaderPopup->setTitle(i18n("View Columns")); - for (int i = 0; i < model()->columnCount(); ++i) { - QAction *tmp = mHeaderPopup->addAction(model()->headerData(i, Qt::Horizontal).toString()); - tmp->setData(QVariant(i)); - tmp->setCheckable(true); - mColumnActions << tmp; - } - - connect(mHeaderPopup, &QMenu::triggered, this, [this](QAction *action) { - const int col = action->data().toInt(); - if ((col == TAGS_COLUMN) && action->isChecked()) { - Tags::enableTags(); - } - if (action->isChecked()) { - showColumn(col); - } else { - hideColumn(col); - } - - auto tv = qobject_cast(parent()); - if (tv) { - tv->resizeColumns(); - } - }); - } - - for (QAction *action : std::as_const(mColumnActions)) { - const int column = action->data().toInt(); - action->setChecked(!isColumnHidden(column)); - } - - mHeaderPopup->popup(mapToGlobal(e->pos())); - return true; - } - - return false; - } - void focusInEvent(QFocusEvent *event) override { QTreeView::focusInEvent(event); // queue the invokation, so that it happens after the widget itself got focus - QMetaObject::invokeMethod(this, &TreeView::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &TreeViewInternal::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); } private: void forceAccessibleFocusEventForCurrentItem() { // force Qt to send a focus event for the current item to accessibility // tools; otherwise, the user has no idea which item is selected when the // list gets keyboard input focus const auto current = currentIndex(); setCurrentIndex({}); setCurrentIndex(current); } private: QMenu *mHeaderPopup = nullptr; QList mColumnActions; }; const KeyListModelInterface *keyListModel(const QTreeView &view) { const KeyListModelInterface *const klmi = dynamic_cast(view.model()); Q_ASSERT(klmi); return klmi; } } // anon namespace KeyTreeView::KeyTreeView(QWidget *parent) : QWidget(parent) , m_proxy(new KeyListSortFilterProxyModel(this)) , m_additionalProxy(nullptr) - , m_view(new TreeView(this)) + , m_view(new TreeViewInternal(this)) , m_flatModel(nullptr) , m_hierarchicalModel(nullptr) , m_stringFilter() , m_keyFilter() , m_isHierarchical(true) { init(); } KeyTreeView::KeyTreeView(const KeyTreeView &other) : QWidget(nullptr) , m_proxy(new KeyListSortFilterProxyModel(this)) , m_additionalProxy(other.m_additionalProxy ? other.m_additionalProxy->clone() : nullptr) - , m_view(new TreeView(this)) + , m_view(new TreeViewInternal(this)) , m_flatModel(other.m_flatModel) , m_hierarchicalModel(other.m_hierarchicalModel) , m_stringFilter(other.m_stringFilter) , m_keyFilter(other.m_keyFilter) , m_group(other.m_group) , m_isHierarchical(other.m_isHierarchical) { init(); setColumnSizes(other.columnSizes()); setSortColumn(other.sortColumn(), other.sortOrder()); } KeyTreeView::KeyTreeView(const QString &text, const std::shared_ptr &kf, AbstractKeyListSortFilterProxyModel *proxy, QWidget *parent, const KConfigGroup &group) : QWidget(parent) , m_proxy(new KeyListSortFilterProxyModel(this)) , m_additionalProxy(proxy) - , m_view(new TreeView(this)) + , m_view(new TreeViewInternal(this)) , m_flatModel(nullptr) , m_hierarchicalModel(nullptr) , m_stringFilter(text) , m_keyFilter(kf) , m_group(group) , m_isHierarchical(true) , m_onceResized(false) { init(); } void KeyTreeView::setColumnSizes(const std::vector &sizes) { if (sizes.empty()) { return; } Q_ASSERT(m_view); Q_ASSERT(m_view->header()); Q_ASSERT(qobject_cast(m_view->header()) == static_cast(m_view->header())); if (auto const hv = static_cast(m_view->header())) { hv->setSectionSizes(sizes); } } void KeyTreeView::setSortColumn(int sortColumn, Qt::SortOrder sortOrder) { Q_ASSERT(m_view); m_view->sortByColumn(sortColumn, sortOrder); } int KeyTreeView::sortColumn() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); return m_view->header()->sortIndicatorSection(); } Qt::SortOrder KeyTreeView::sortOrder() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); return m_view->header()->sortIndicatorOrder(); } std::vector KeyTreeView::columnSizes() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); Q_ASSERT(qobject_cast(m_view->header()) == static_cast(m_view->header())); if (auto const hv = static_cast(m_view->header())) { return hv->sectionSizes(); } else { return std::vector(); } } void KeyTreeView::init() { KDAB_SET_OBJECT_NAME(m_proxy); KDAB_SET_OBJECT_NAME(m_view); if (m_group.isValid()) { // Reopen as non const KConfig *conf = m_group.config(); m_group = conf->group(m_group.name()); } if (m_additionalProxy && m_additionalProxy->objectName().isEmpty()) { KDAB_SET_OBJECT_NAME(m_additionalProxy); } QLayout *layout = new QVBoxLayout(this); KDAB_SET_OBJECT_NAME(layout); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_view); auto headerView = new HeaderView(Qt::Horizontal); KDAB_SET_OBJECT_NAME(headerView); headerView->installEventFilter(m_view); headerView->setSectionsMovable(true); m_view->setHeader(headerView); m_view->setSelectionBehavior(QAbstractItemView::SelectRows); m_view->setSelectionMode(QAbstractItemView::ExtendedSelection); m_view->setAllColumnsShowFocus(false); m_view->setSortingEnabled(true); m_view->setAccessibleName(i18n("Certificates")); m_view->setAccessibleDescription(m_isHierarchical ? i18n("Hierarchical list of certificates") : i18n("List of certificates")); // we show details on double-click m_view->setExpandsOnDoubleClick(false); if (model()) { if (m_additionalProxy) { m_additionalProxy->setSourceModel(model()); } else { m_proxy->setSourceModel(model()); } } if (m_additionalProxy) { m_proxy->setSourceModel(m_additionalProxy); if (!m_additionalProxy->parent()) { m_additionalProxy->setParent(this); } } m_proxy->setFilterRegularExpression(QRegularExpression::escape(m_stringFilter)); m_proxy->setKeyFilter(m_keyFilter); m_proxy->setSortCaseSensitivity(Qt::CaseInsensitive); auto rearangingModel = new KeyRearrangeColumnsProxyModel(this); rearangingModel->setSourceModel(m_proxy); rearangingModel->setSourceColumns(QList() << KeyList::PrettyName // << KeyList::PrettyEMail // << KeyList::Validity // << KeyList::ValidFrom // << KeyList::ValidUntil // << KeyList::TechnicalDetails // << KeyList::KeyID // << KeyList::Fingerprint // << KeyList::OwnerTrust // << KeyList::Origin // << KeyList::LastUpdate // << KeyList::Issuer // << KeyList::SerialNumber // // If a column is added before this TAGS_COLUMN define has to be updated accordingly << KeyList::Remarks); m_view->setModel(rearangingModel); /* Handle expansion state */ if (m_group.isValid()) { m_expandedKeys = m_group.readEntry("Expanded", QStringList()); } connect(m_view, &QTreeView::expanded, this, [this](const QModelIndex &index) { if (!index.isValid()) { return; } const auto &key = index.data(KeyList::KeyRole).value(); if (key.isNull()) { return; } const auto fpr = QString::fromLatin1(key.primaryFingerprint()); if (m_expandedKeys.contains(fpr)) { return; } m_expandedKeys << fpr; if (m_group.isValid()) { m_group.writeEntry("Expanded", m_expandedKeys); } }); connect(m_view, &QTreeView::collapsed, this, [this](const QModelIndex &index) { if (!index.isValid()) { return; } const auto &key = index.data(KeyList::KeyRole).value(); if (key.isNull()) { return; } m_expandedKeys.removeAll(QString::fromLatin1(key.primaryFingerprint())); if (m_group.isValid()) { m_group.writeEntry("Expanded", m_expandedKeys); } }); updateModelConnections(nullptr, model()); resizeColumns(); if (m_group.isValid()) { restoreLayout(m_group); } } void KeyTreeView::restoreExpandState() { if (!KeyCache::instance()->initialized()) { qCWarning(KLEOPATRA_LOG) << "Restore expand state before keycache available. Aborting."; return; } for (const auto &fpr : std::as_const(m_expandedKeys)) { const KeyListModelInterface *const km = keyListModel(*m_view); if (!km) { qCWarning(KLEOPATRA_LOG) << "invalid model"; return; } const auto key = KeyCache::instance()->findByFingerprint(fpr.toLatin1().constData()); if (key.isNull()) { qCDebug(KLEOPATRA_LOG) << "Cannot find:" << fpr << "anymore in cache"; m_expandedKeys.removeAll(fpr); return; } const auto idx = km->index(key); if (!idx.isValid()) { qCDebug(KLEOPATRA_LOG) << "Cannot find:" << fpr << "anymore in model"; m_expandedKeys.removeAll(fpr); return; } m_view->expand(idx); } } void KeyTreeView::setUpTagKeys() { const auto tagKeys = Tags::tagKeys(); if (m_hierarchicalModel) { m_hierarchicalModel->setRemarkKeys(tagKeys); } if (m_flatModel) { m_flatModel->setRemarkKeys(tagKeys); } } void KeyTreeView::saveLayout(KConfigGroup &group) { QHeaderView *header = m_view->header(); QVariantList columnVisibility; QVariantList columnOrder; QVariantList columnWidths; const int headerCount = header->count(); columnVisibility.reserve(headerCount); columnWidths.reserve(headerCount); columnOrder.reserve(headerCount); for (int i = 0; i < headerCount; ++i) { columnVisibility << QVariant(!m_view->isColumnHidden(i)); columnWidths << QVariant(header->sectionSize(i)); columnOrder << QVariant(header->visualIndex(i)); } group.writeEntry("ColumnVisibility", columnVisibility); group.writeEntry("ColumnOrder", columnOrder); group.writeEntry("ColumnWidths", columnWidths); group.writeEntry("SortAscending", (int)header->sortIndicatorOrder()); if (header->isSortIndicatorShown()) { group.writeEntry("SortColumn", header->sortIndicatorSection()); } else { group.writeEntry("SortColumn", -1); } } void KeyTreeView::restoreLayout(const KConfigGroup &group) { QHeaderView *header = m_view->header(); QVariantList columnVisibility = group.readEntry("ColumnVisibility", QVariantList()); QVariantList columnOrder = group.readEntry("ColumnOrder", QVariantList()); QVariantList columnWidths = group.readEntry("ColumnWidths", QVariantList()); if (columnVisibility.isEmpty()) { // if config is empty then use default settings // The numbers have to be in line with the order in // setsSourceColumns above m_view->hideColumn(5); for (int i = 7; i < m_view->model()->columnCount(); ++i) { m_view->hideColumn(i); } if (KeyCache::instance()->initialized()) { QTimer::singleShot(0, this, &KeyTreeView::resizeColumns); } } else { for (int i = 0; i < header->count(); ++i) { if (i >= columnOrder.size() || i >= columnWidths.size() || i >= columnVisibility.size()) { // An additional column that was not around last time we saved. // We default to hidden. m_view->hideColumn(i); continue; } bool visible = columnVisibility[i].toBool(); int width = columnWidths[i].toInt(); int order = columnOrder[i].toInt(); header->resizeSection(i, width ? width : 100); header->moveSection(header->visualIndex(i), order); if ((i == TAGS_COLUMN) && visible) { Tags::enableTags(); } if (!visible) { m_view->hideColumn(i); } } m_onceResized = true; } int sortOrder = group.readEntry("SortAscending", (int)Qt::AscendingOrder); int sortColumn = group.readEntry("SortColumn", 0); if (sortColumn >= 0) { m_view->sortByColumn(sortColumn, (Qt::SortOrder)sortOrder); } } KeyTreeView::~KeyTreeView() { if (m_group.isValid()) { saveLayout(m_group); } } static QAbstractProxyModel *find_last_proxy(QAbstractProxyModel *pm) { Q_ASSERT(pm); while (auto const sm = qobject_cast(pm->sourceModel())) { pm = sm; } return pm; } void KeyTreeView::updateModelConnections(AbstractKeyListModel *oldModel, AbstractKeyListModel *newModel) { if (oldModel == newModel) { return; } if (oldModel) { disconnect(oldModel, &QAbstractItemModel::modelAboutToBeReset, this, &KeyTreeView::saveStateBeforeModelChange); disconnect(oldModel, &QAbstractItemModel::modelReset, this, &KeyTreeView::restoreStateAfterModelChange); disconnect(oldModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &KeyTreeView::saveStateBeforeModelChange); disconnect(oldModel, &QAbstractItemModel::rowsInserted, this, &KeyTreeView::restoreStateAfterModelChange); disconnect(oldModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &KeyTreeView::saveStateBeforeModelChange); disconnect(oldModel, &QAbstractItemModel::rowsRemoved, this, &KeyTreeView::restoreStateAfterModelChange); } if (newModel) { connect(newModel, &QAbstractItemModel::modelAboutToBeReset, this, &KeyTreeView::saveStateBeforeModelChange); connect(newModel, &QAbstractItemModel::modelReset, this, &KeyTreeView::restoreStateAfterModelChange); connect(newModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &KeyTreeView::saveStateBeforeModelChange); connect(newModel, &QAbstractItemModel::rowsInserted, this, &KeyTreeView::restoreStateAfterModelChange); connect(newModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &KeyTreeView::saveStateBeforeModelChange); connect(newModel, &QAbstractItemModel::rowsRemoved, this, &KeyTreeView::restoreStateAfterModelChange); } } void KeyTreeView::setFlatModel(AbstractKeyListModel *model) { if (model == m_flatModel) { return; } auto oldModel = m_flatModel; m_flatModel = model; if (!m_isHierarchical) // TODO: this fails when called after setHierarchicalView( false )... { find_last_proxy(m_proxy)->setSourceModel(model); updateModelConnections(oldModel, model); } } void KeyTreeView::setHierarchicalModel(AbstractKeyListModel *model) { if (model == m_hierarchicalModel) { return; } auto oldModel = m_hierarchicalModel; m_hierarchicalModel = model; if (m_isHierarchical) { find_last_proxy(m_proxy)->setSourceModel(model); updateModelConnections(oldModel, model); m_view->expandAll(); for (int column = 0; column < m_view->header()->count(); ++column) { m_view->header()->resizeSection(column, qMax(m_view->header()->sectionSize(column), m_view->header()->sectionSizeHint(column))); } } } void KeyTreeView::setStringFilter(const QString &filter) { if (filter == m_stringFilter) { return; } m_stringFilter = filter; m_proxy->setFilterRegularExpression(QRegularExpression::escape(filter)); Q_EMIT stringFilterChanged(filter); } void KeyTreeView::setKeyFilter(const std::shared_ptr &filter) { if (filter == m_keyFilter || (filter && m_keyFilter && filter->id() == m_keyFilter->id())) { return; } m_keyFilter = filter; m_proxy->setKeyFilter(filter); Q_EMIT keyFilterChanged(filter); } namespace { QItemSelection itemSelectionFromKeys(const std::vector &keys, const QTreeView &view) { const QModelIndexList indexes = keyListModel(view)->indexes(keys); return std::accumulate(indexes.cbegin(), indexes.cend(), QItemSelection(), [](QItemSelection selection, const QModelIndex &index) { if (index.isValid()) { selection.merge(QItemSelection(index, index), QItemSelectionModel::Select); } return selection; }); } } void KeyTreeView::selectKeys(const std::vector &keys) { m_view->selectionModel()->select(itemSelectionFromKeys(keys, *m_view), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } std::vector KeyTreeView::selectedKeys() const { return keyListModel(*m_view)->keys(m_view->selectionModel()->selectedRows()); } void KeyTreeView::setHierarchicalView(bool on) { if (on == m_isHierarchical) { return; } if (on && !hierarchicalModel()) { qCWarning(KLEOPATRA_LOG) << "hierarchical view requested, but no hierarchical model set"; return; } if (!on && !flatModel()) { qCWarning(KLEOPATRA_LOG) << "flat view requested, but no flat model set"; return; } const std::vector selectedKeys = this->selectedKeys(); const Key currentKey = keyListModel(*m_view)->key(m_view->currentIndex()); auto oldModel = model(); m_isHierarchical = on; find_last_proxy(m_proxy)->setSourceModel(model()); updateModelConnections(oldModel, model()); if (on) { m_view->expandAll(); } selectKeys(selectedKeys); if (!currentKey.isNull()) { const QModelIndex currentIndex = keyListModel(*m_view)->index(currentKey); if (currentIndex.isValid()) { m_view->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); m_view->scrollTo(currentIndex); } } m_view->setAccessibleDescription(m_isHierarchical ? i18n("Hierarchical list of certificates") : i18n("List of certificates")); Q_EMIT hierarchicalChanged(on); } void KeyTreeView::setKeys(const std::vector &keys) { std::vector sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); m_keys = sorted; if (m_flatModel) { m_flatModel->setKeys(sorted); } if (m_hierarchicalModel) { m_hierarchicalModel->setKeys(sorted); } } void KeyTreeView::addKeysImpl(const std::vector &keys, bool select) { if (keys.empty()) { return; } if (m_keys.empty()) { setKeys(keys); return; } std::vector sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); std::vector newKeys = _detail::union_by_fpr(sorted, m_keys); m_keys.swap(newKeys); if (m_flatModel) { m_flatModel->addKeys(sorted); } if (m_hierarchicalModel) { m_hierarchicalModel->addKeys(sorted); } if (select) { selectKeys(sorted); } } void KeyTreeView::addKeysSelected(const std::vector &keys) { addKeysImpl(keys, true); } void KeyTreeView::addKeysUnselected(const std::vector &keys) { addKeysImpl(keys, false); } void KeyTreeView::removeKeys(const std::vector &keys) { if (keys.empty()) { return; } std::vector sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); std::vector newKeys; newKeys.reserve(m_keys.size()); std::set_difference(m_keys.begin(), m_keys.end(), sorted.begin(), sorted.end(), std::back_inserter(newKeys), _detail::ByFingerprint()); m_keys.swap(newKeys); if (m_flatModel) { std::for_each(sorted.cbegin(), sorted.cend(), [this](const Key &key) { m_flatModel->removeKey(key); }); } if (m_hierarchicalModel) { std::for_each(sorted.cbegin(), sorted.cend(), [this](const Key &key) { m_hierarchicalModel->removeKey(key); }); } } void KeyTreeView::disconnectSearchBar() { for (const auto &connection : m_connections) { disconnect(connection); } m_connections.clear(); } bool KeyTreeView::connectSearchBar(const SearchBar *bar) { m_connections.reserve(4); m_connections.push_back(connect(this, &KeyTreeView::stringFilterChanged, bar, &SearchBar::setStringFilter)); m_connections.push_back(connect(bar, &SearchBar::stringFilterChanged, this, &KeyTreeView::setStringFilter)); m_connections.push_back(connect(this, &KeyTreeView::keyFilterChanged, bar, &SearchBar::setKeyFilter)); m_connections.push_back(connect(bar, &SearchBar::keyFilterChanged, this, &KeyTreeView::setKeyFilter)); return std::all_of(m_connections.cbegin(), m_connections.cend(), [](const QMetaObject::Connection &conn) { return conn; }); } void KeyTreeView::resizeColumns() { m_view->setColumnWidth(KeyList::PrettyName, 260); m_view->setColumnWidth(KeyList::PrettyEMail, 260); for (int i = 2; i < m_view->model()->columnCount(); ++i) { m_view->resizeColumnToContents(i); } } void KeyTreeView::saveStateBeforeModelChange() { m_currentKey = keyListModel(*m_view)->key(m_view->currentIndex()); m_selectedKeys = selectedKeys(); } void KeyTreeView::restoreStateAfterModelChange() { restoreExpandState(); selectKeys(m_selectedKeys); if (!m_currentKey.isNull()) { const QModelIndex currentIndex = keyListModel(*m_view)->index(m_currentKey); if (currentIndex.isValid()) { m_view->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); m_view->scrollTo(currentIndex); } } setUpTagKeys(); if (!m_onceResized) { m_onceResized = true; resizeColumns(); } } #include "moc_keytreeview.cpp"