diff --git a/src/conf/appearanceconfigwidget.cpp b/src/conf/appearanceconfigwidget.cpp index 5d92109f1..0d3f0a5af 100644 --- a/src/conf/appearanceconfigwidget.cpp +++ b/src/conf/appearanceconfigwidget.cpp @@ -1,908 +1,908 @@ /* This file is part of kleopatra, the KDE key manager SPDX-FileCopyrightText: 2002, 2004, 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2002, 2003 Marc Mutz <mutz@kde.org> SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-kleopatra.h> #include "appearanceconfigwidget.h" #include <settings.h> #include <Libkleo/DNAttributeOrderConfigWidget> -#include <Libkleo/Dn> +#include <Libkleo/DnAttributes> #include <Libkleo/ExpiryCheckerConfig> #include <Libkleo/KeyFilterManager> #include <Libkleo/SystemInfo> #include <KConfig> #include <KConfigGroup> #include <KIconDialog> #include <KLocalization> #include <KLocalizedString> #include <KMessageWidget> #include <KSeparator> #include <QApplication> #include <QCheckBox> #include <QColor> #include <QColorDialog> #include <QFont> #include <QFontDialog> #include <QGridLayout> #include <QIcon> #include <QLabel> #include <QListWidget> #include <QRegularExpression> #include <QSpinBox> #include <QString> #include <QVBoxLayout> #include <algorithm> using namespace Kleo; using namespace Kleo::Config; enum { HasNameRole = Qt::UserRole + 0x1234, /*!< Records that the user has assigned a name (to avoid comparing with i18n-strings) */ HasFontRole, /*!< Records that the user has chosen completely different font (as opposed to italic/bold/strikeout) */ IconNameRole, /*!< Records the name of the icon (since QIcon won't give it out again, once set) */ MayChangeNameRole, MayChangeForegroundRole, MayChangeBackgroundRole, MayChangeFontRole, MayChangeItalicRole, MayChangeBoldRole, MayChangeStrikeOutRole, MayChangeIconRole, StoredForegroundRole, /*!< Stores the actual configured foreground color */ StoredBackgroundRole, /*!< Stores the actual configured background color */ ConfigGroupRole, EndDummy, }; static QFont tryToFindFontFor(const QListWidgetItem *item) { if (item) if (const QListWidget *const lw = item->listWidget()) { return lw->font(); } return QApplication::font("QListWidget"); } static bool is(const QListWidgetItem *item, bool (QFont::*func)() const) { if (!item) { return false; } const QVariant v = item->data(Qt::FontRole); if (!v.isValid() || v.userType() != QMetaType::QFont) { return false; } return (v.value<QFont>().*func)(); } static bool is_italic(const QListWidgetItem *item) { return is(item, &QFont::italic); } static bool is_bold(const QListWidgetItem *item) { return is(item, &QFont::bold); } static bool is_strikeout(const QListWidgetItem *item) { return is(item, &QFont::strikeOut); } static void set(QListWidgetItem *item, bool on, void (QFont::*func)(bool)) { if (!item) { return; } const QVariant v = item->data(Qt::FontRole); QFont font = v.isValid() && v.userType() == QMetaType::QFont ? v.value<QFont>() : tryToFindFontFor(item); (font.*func)(on); item->setData(Qt::FontRole, font); } static void set_italic(QListWidgetItem *item, bool on) { set(item, on, &QFont::setItalic); } static void set_bold(QListWidgetItem *item, bool on) { set(item, on, &QFont::setBold); } static void set_strikeout(QListWidgetItem *item, bool on) { set(item, on, &QFont::setStrikeOut); } static void apply_config(const KConfigGroup &group, QListWidgetItem *item) { if (!item) { return; } const QString name = group.readEntry("Name"); item->setText(name.isEmpty() ? i18nc("Key filter without user-assigned name", "<unnamed>") : name); item->setData(HasNameRole, !name.isEmpty()); item->setData(MayChangeNameRole, !group.isEntryImmutable("Name")); item->setData(ConfigGroupRole, QVariant::fromValue(group)); const QColor fg = group.readEntry("foreground-color", QColor()); item->setData(StoredForegroundRole, fg.isValid() ? QBrush(fg) : QVariant()); if (!SystemInfo::isHighContrastModeActive()) { item->setData(Qt::ForegroundRole, fg.isValid() ? QBrush(fg) : QVariant()); } item->setData(MayChangeForegroundRole, !group.isEntryImmutable("foreground-color")); const QColor bg = group.readEntry("background-color", QColor()); item->setData(StoredBackgroundRole, bg.isValid() ? QBrush(bg) : QVariant()); if (!SystemInfo::isHighContrastModeActive()) { item->setData(Qt::BackgroundRole, bg.isValid() ? QBrush(bg) : QVariant()); } item->setData(MayChangeBackgroundRole, !group.isEntryImmutable("background-color")); const QFont defaultFont = tryToFindFontFor(item); if (group.hasKey("font")) { const QFont font = group.readEntry("font", defaultFont); item->setData(Qt::FontRole, font != defaultFont ? font : QVariant()); item->setData(HasFontRole, font != defaultFont); } else { QFont font = defaultFont; font.setStrikeOut(group.readEntry("font-strikeout", false)); font.setItalic(group.readEntry("font-italic", false)); font.setBold(group.readEntry("font-bold", false)); item->setData(Qt::FontRole, font); item->setData(HasFontRole, false); } item->setData(MayChangeFontRole, !group.isEntryImmutable("font")); item->setData(MayChangeItalicRole, !group.isEntryImmutable("font-italic")); item->setData(MayChangeBoldRole, !group.isEntryImmutable("font-bold")); item->setData(MayChangeStrikeOutRole, !group.isEntryImmutable("font-strikeout")); const QString iconName = group.readEntry("icon"); item->setData(Qt::DecorationRole, iconName.isEmpty() ? QVariant() : QIcon::fromTheme(iconName)); item->setData(IconNameRole, iconName.isEmpty() ? QVariant() : iconName); item->setData(MayChangeIconRole, !group.isEntryImmutable("icon")); } static void resetString(QListWidgetItem *item, int role, int allowRole, const char *key) { if (item && item->data(allowRole).toBool()) { auto config = item->data(ConfigGroupRole).value<KConfigGroup>(); config.revertToDefault(key); item->setData(role, config.readEntry(key, QString())); } } static void resetColor(QListWidgetItem *item, int role, int allowRole, const char *key) { if (item && item->data(allowRole).toBool()) { auto config = item->data(ConfigGroupRole).value<KConfigGroup>(); config.revertToDefault(key); const auto value = config.readEntry(key, QColor{}); item->setData(role, value.isValid() ? value : QVariant()); } } static void erase_if_allowed(QListWidgetItem *item, const int allowRole[], size_t numAllowRoles) { if (!item) { return; } for (unsigned int i = 0; i < numAllowRoles; ++i) if (!item->data(allowRole[i]).toBool()) { return; } auto config = item->data(ConfigGroupRole).value<KConfigGroup>(); config.revertToDefault("font"); config.revertToDefault("font-bold"); config.revertToDefault("font-strikeout"); config.revertToDefault("font-italic"); auto value = config.readEntry<QFont>("font", tryToFindFontFor(item)); value.setBold(config.readEntry("font-bold", false)); value.setStrikeOut(config.readEntry("font-strikeout", false)); value.setItalic(config.readEntry("font-italic", false)); item->setData(Qt::FontRole, value); } static void set_default_appearance(QListWidgetItem *item) { if (!item) { return; } if (!SystemInfo::isHighContrastModeActive()) { resetColor(item, Qt::ForegroundRole, MayChangeForegroundRole, "foreground-color"); resetColor(item, Qt::BackgroundRole, MayChangeBackgroundRole, "background-color"); } resetColor(item, StoredForegroundRole, MayChangeForegroundRole, "foreground-color"); resetColor(item, StoredBackgroundRole, MayChangeBackgroundRole, "background-color"); resetString(item, IconNameRole, MayChangeIconRole, "icon"); item->setData(Qt::DecorationRole, QIcon::fromTheme(item->data(ConfigGroupRole).value<KConfigGroup>().readEntry("icon", QString()))); static const int fontAllowRoles[] = { MayChangeFontRole, MayChangeItalicRole, MayChangeBoldRole, MayChangeStrikeOutRole, }; erase_if_allowed(item, fontAllowRoles, sizeof(fontAllowRoles) / sizeof(int)); } static void writeOrDelete(KConfigGroup &group, const char *key, const QVariant &value) { if (value.isValid()) { group.writeEntry(key, value); } else { group.revertToDefault(key); } } static QVariant brush2color(const QVariant &v) { if (v.isValid()) { if (v.userType() == QMetaType::QColor) { return v; } else if (v.userType() == QMetaType::QBrush) { return v.value<QBrush>().color(); } } return QVariant(); } static void save_to_config(const QListWidgetItem *item, KConfigGroup &group) { if (!item) { return; } writeOrDelete(group, "Name", item->data(HasNameRole).toBool() ? item->text() : QVariant()); writeOrDelete(group, "foreground-color", brush2color(item->data(StoredForegroundRole))); writeOrDelete(group, "background-color", brush2color(item->data(StoredBackgroundRole))); writeOrDelete(group, "icon", item->data(IconNameRole)); group.deleteEntry("font"); group.deleteEntry("font-strikeout"); group.deleteEntry("font-italic"); group.deleteEntry("font-bold"); if (item->data(HasFontRole).toBool()) { writeOrDelete(group, "font", item->data(Qt::FontRole)); return; } if (is_strikeout(item)) { group.writeEntry("font-strikeout", true); } if (is_italic(item)) { group.writeEntry("font-italic", true); } if (is_bold(item)) { group.writeEntry("font-bold", true); } } static void kiosk_enable(QWidget *w, const QListWidgetItem *item, int allowRole) { if (!w) { return; } if (item && !item->data(allowRole).toBool()) { w->setEnabled(false); w->setToolTip(i18nc("@info:tooltip", "This parameter has been locked down by the system administrator.")); } else { w->setEnabled(item); w->setToolTip(QString()); } } class Ui_AppearanceConfigWidget { public: QTabWidget *tabWidget; KMessageWidget *highContrastMsg; QListWidget *categoriesLV; QPushButton *iconButton; QPushButton *foregroundButton; QPushButton *backgroundButton; QPushButton *fontButton; QCheckBox *italicCB; QCheckBox *boldCB; QCheckBox *strikeoutCB; QPushButton *defaultLookPB; QCheckBox *tooltipValidityCheckBox; QCheckBox *tooltipOwnerCheckBox; QCheckBox *tooltipDetailsCheckBox; QCheckBox *showExpirationCheckBox; QSpinBox *ownCertificateThresholdSpinBox; QSpinBox *otherCertificateThresholdSpinBox; void setupUi(QWidget *parent) { if (parent->objectName().isEmpty()) parent->setObjectName(QString::fromUtf8("AppearanceConfigWidget")); auto mainLayout = new QVBoxLayout{parent}; mainLayout->setContentsMargins({}); tabWidget = new QTabWidget(parent); tabWidget->setDocumentMode(true); tabWidget->setObjectName(QString::fromUtf8("tabWidget")); { auto tab = new QWidget{parent}; auto tabLayout = new QVBoxLayout{tab}; tabLayout->addWidget(new KSeparator{tab}); auto label = new QLabel{tab}; label->setText(i18nc("@info", "Show the following information in certificate list tooltips:")); tabLayout->addWidget(label); tooltipValidityCheckBox = new QCheckBox{i18nc("@option:check", "Show validity"), tab}; tabLayout->addWidget(tooltipValidityCheckBox); tooltipOwnerCheckBox = new QCheckBox{i18nc("@option:check", "Show owner information"), tab}; tabLayout->addWidget(tooltipOwnerCheckBox); tooltipDetailsCheckBox = new QCheckBox{i18nc("@option:check", "Show technical details"), tab}; tabLayout->addWidget(tooltipDetailsCheckBox); tabLayout->addWidget(new KSeparator{tab}); showExpirationCheckBox = new QCheckBox{i18nc("@option:check", "Show upcoming certificate expiration"), tab}; tabLayout->addWidget(showExpirationCheckBox); { auto gridLayout = new QGridLayout; const ExpiryCheckerConfig expiryConfig; { auto label = new QLabel{i18nc("@label:spinbox", "Threshold for own certificates:"), tab}; ownCertificateThresholdSpinBox = new QSpinBox{tab}; label->setBuddy(ownCertificateThresholdSpinBox); const auto configItem = expiryConfig.ownKeyThresholdInDaysItem(); ownCertificateThresholdSpinBox->setMinimum(configItem->minValue().toInt()); ownCertificateThresholdSpinBox->setMaximum(configItem->maxValue().toInt()); ownCertificateThresholdSpinBox->setSpecialValueText(i18nc("@item never show expiry notification", "never")); KLocalization::setupSpinBoxFormatString(ownCertificateThresholdSpinBox, ki18ncp("@item:valuesuffix", "%v day", "%v days")); ownCertificateThresholdSpinBox->setToolTip( i18nc("@info:tooltip", "Select the number of days you want to be warned in advance, if your own certificate is about to expire soon.")); gridLayout->addWidget(label, 0, 0); gridLayout->addWidget(ownCertificateThresholdSpinBox, 0, 1); } { auto label = new QLabel{i18nc("@label:spinbox", "Threshold for other certificates:"), tab}; otherCertificateThresholdSpinBox = new QSpinBox{tab}; label->setBuddy(otherCertificateThresholdSpinBox); const auto configItem = expiryConfig.otherKeyThresholdInDaysItem(); otherCertificateThresholdSpinBox->setMinimum(configItem->minValue().toInt()); otherCertificateThresholdSpinBox->setMaximum(configItem->maxValue().toInt()); otherCertificateThresholdSpinBox->setSpecialValueText(i18nc("@item never show expiry notification", "never")); KLocalization::setupSpinBoxFormatString(otherCertificateThresholdSpinBox, ki18ncp("@item:valuesuffix", "%v day", "%v days")); otherCertificateThresholdSpinBox->setToolTip( i18nc("@info:tooltip", "Select the number of days you want to be warned in advance, if another person's certificate is about to expire soon.")); gridLayout->addWidget(label, 1, 0); gridLayout->addWidget(otherCertificateThresholdSpinBox, 1, 1); } gridLayout->setColumnStretch(2, 1); tabLayout->addLayout(gridLayout); } tabLayout->addStretch(1); tabWidget->addTab(tab, i18nc("@title:tab", "General")); } auto tab_2 = new QWidget(); tab_2->setObjectName(QString::fromUtf8("tab_2")); auto gridLayout = new QGridLayout(tab_2); gridLayout->setObjectName(QString::fromUtf8("gridLayout")); highContrastMsg = new KMessageWidget(tab_2); highContrastMsg->setObjectName(QString::fromUtf8("highContrastMsg")); gridLayout->addWidget(highContrastMsg, 0, 0, 1, 2); categoriesLV = new QListWidget(tab_2); categoriesLV->setObjectName(QString::fromUtf8("categoriesLV")); gridLayout->addWidget(categoriesLV, 1, 0, 1, 1); auto vboxLayout = new QVBoxLayout(); vboxLayout->setObjectName(QString::fromUtf8("vboxLayout")); iconButton = new QPushButton(tab_2); iconButton->setText(i18nc("@action:button", "Set Icon...")); iconButton->setObjectName(QString::fromUtf8("iconButton")); iconButton->setEnabled(false); vboxLayout->addWidget(iconButton); foregroundButton = new QPushButton(tab_2); foregroundButton->setText(i18nc("@action:button", "Set Text Color...")); foregroundButton->setObjectName(QString::fromUtf8("foregroundButton")); foregroundButton->setEnabled(false); vboxLayout->addWidget(foregroundButton); backgroundButton = new QPushButton(tab_2); backgroundButton->setText(i18nc("@action:button", "Set Background Color...")); backgroundButton->setObjectName(QString::fromUtf8("backgroundButton")); backgroundButton->setEnabled(false); vboxLayout->addWidget(backgroundButton); fontButton = new QPushButton(tab_2); fontButton->setText(i18nc("@action:button", "Set Font...")); fontButton->setObjectName(QString::fromUtf8("fontButton")); fontButton->setEnabled(false); vboxLayout->addWidget(fontButton); italicCB = new QCheckBox(tab_2); italicCB->setText(i18nc("@option:check", "Italic")); italicCB->setObjectName(QString::fromUtf8("italicCB")); italicCB->setEnabled(false); vboxLayout->addWidget(italicCB); boldCB = new QCheckBox(tab_2); boldCB->setText(i18nc("@option:check", "Bold")); boldCB->setObjectName(QString::fromUtf8("boldCB")); boldCB->setEnabled(false); vboxLayout->addWidget(boldCB); strikeoutCB = new QCheckBox(tab_2); strikeoutCB->setText(i18nc("@option:check", "Strikeout")); strikeoutCB->setObjectName(QString::fromUtf8("strikeoutCB")); strikeoutCB->setEnabled(false); vboxLayout->addWidget(strikeoutCB); vboxLayout->addStretch(1); defaultLookPB = new QPushButton(tab_2); defaultLookPB->setText(i18nc("@action:button", "Default Appearance")); defaultLookPB->setObjectName(QString::fromUtf8("defaultLookPB")); defaultLookPB->setEnabled(false); vboxLayout->addWidget(defaultLookPB); gridLayout->addLayout(vboxLayout, 1, 1, 1, 1); tabWidget->addTab(tab_2, i18nc("@title:tab", "Certificate Categories")); mainLayout->addWidget(tabWidget); } }; class AppearanceConfigWidget::Private : public Ui_AppearanceConfigWidget { friend class ::Kleo::Config::AppearanceConfigWidget; AppearanceConfigWidget *const q; public: explicit Private(AppearanceConfigWidget *qq) : Ui_AppearanceConfigWidget() , q(qq) { setupUi(q); if (QLayout *const l = q->layout()) { l->setContentsMargins(0, 0, 0, 0); } highContrastMsg->setVisible(SystemInfo::isHighContrastModeActive()); highContrastMsg->setMessageType(KMessageWidget::Warning); highContrastMsg->setIcon(q->style()->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, q)); highContrastMsg->setText(i18n("The preview of colors is disabled because high-contrast mode is active.")); highContrastMsg->setCloseButtonVisible(false); if (Kleo::Settings{}.cmsEnabled()) { auto w = new QWidget; dnOrderWidget = new DNAttributeOrderConfigWidget{w}; dnOrderWidget->setObjectName(QLatin1StringView("dnOrderWidget")); (new QVBoxLayout(w))->addWidget(dnOrderWidget); tabWidget->addTab(w, i18n("DN-Attribute Order")); connect(dnOrderWidget, &DNAttributeOrderConfigWidget::changed, q, &AppearanceConfigWidget::changed); } connect(iconButton, SIGNAL(clicked()), q, SLOT(slotIconClicked())); #ifndef QT_NO_COLORDIALOG connect(foregroundButton, SIGNAL(clicked()), q, SLOT(slotForegroundClicked())); connect(backgroundButton, SIGNAL(clicked()), q, SLOT(slotBackgroundClicked())); #else foregroundButton->hide(); backgroundButton->hide(); #endif #ifndef QT_NO_FONTDIALOG connect(fontButton, SIGNAL(clicked()), q, SLOT(slotFontClicked())); #else fontButton->hide(); #endif auto emitChanged = [this]() { Q_EMIT q->changed(); }; connect(categoriesLV, SIGNAL(itemSelectionChanged()), q, SLOT(slotSelectionChanged())); connect(defaultLookPB, SIGNAL(clicked()), q, SLOT(slotDefaultClicked())); connect(italicCB, SIGNAL(toggled(bool)), q, SLOT(slotItalicToggled(bool))); connect(boldCB, SIGNAL(toggled(bool)), q, SLOT(slotBoldToggled(bool))); connect(strikeoutCB, SIGNAL(toggled(bool)), q, SLOT(slotStrikeOutToggled(bool))); connect(tooltipValidityCheckBox, SIGNAL(toggled(bool)), q, SLOT(slotTooltipValidityChanged(bool))); connect(tooltipOwnerCheckBox, SIGNAL(toggled(bool)), q, SLOT(slotTooltipOwnerChanged(bool))); connect(tooltipDetailsCheckBox, SIGNAL(toggled(bool)), q, SLOT(slotTooltipDetailsChanged(bool))); connect(showExpirationCheckBox, &QCheckBox::toggled, q, emitChanged); connect(ownCertificateThresholdSpinBox, &QSpinBox::valueChanged, q, emitChanged); connect(otherCertificateThresholdSpinBox, &QSpinBox::valueChanged, q, emitChanged); } private: void enableDisableActions(QListWidgetItem *item); QListWidgetItem *selectedItem() const; private: void slotIconClicked(); #ifndef QT_NO_COLORDIALOG void slotForegroundClicked(); void slotBackgroundClicked(); #endif #ifndef QT_NO_FONTDIALOG void slotFontClicked(); #endif void slotSelectionChanged(); void slotDefaultClicked(); void slotItalicToggled(bool); void slotBoldToggled(bool); void slotStrikeOutToggled(bool); void slotTooltipValidityChanged(bool); void slotTooltipOwnerChanged(bool); void slotTooltipDetailsChanged(bool); private: Kleo::DNAttributeOrderConfigWidget *dnOrderWidget = nullptr; }; AppearanceConfigWidget::AppearanceConfigWidget(QWidget *p, Qt::WindowFlags f) : QWidget(p, f) , d(new Private(this)) { // load(); } AppearanceConfigWidget::~AppearanceConfigWidget() { } void AppearanceConfigWidget::Private::slotSelectionChanged() { enableDisableActions(selectedItem()); } QListWidgetItem *AppearanceConfigWidget::Private::selectedItem() const { const QList<QListWidgetItem *> items = categoriesLV->selectedItems(); return items.empty() ? nullptr : items.front(); } void AppearanceConfigWidget::Private::enableDisableActions(QListWidgetItem *item) { kiosk_enable(iconButton, item, MayChangeIconRole); #ifndef QT_NO_COLORDIALOG kiosk_enable(foregroundButton, item, MayChangeForegroundRole); kiosk_enable(backgroundButton, item, MayChangeBackgroundRole); #endif #ifndef QT_NO_FONTDIALOG kiosk_enable(fontButton, item, MayChangeFontRole); #endif kiosk_enable(italicCB, item, MayChangeItalicRole); kiosk_enable(boldCB, item, MayChangeBoldRole); kiosk_enable(strikeoutCB, item, MayChangeStrikeOutRole); defaultLookPB->setEnabled(item); italicCB->setChecked(is_italic(item)); boldCB->setChecked(is_bold(item)); strikeoutCB->setChecked(is_strikeout(item)); } void AppearanceConfigWidget::Private::slotDefaultClicked() { QListWidgetItem *const item = selectedItem(); if (!item) { return; } set_default_appearance(item); enableDisableActions(item); Q_EMIT q->changed(); } void AppearanceConfigWidget::defaults() { // use temporary KConfigSkeleton instances for (re)setting the values to the defaults; // the setters respect the immutability of the individual settings, so that we don't have // to check this ourselves Settings settings; settings.setShowExpiryNotifications(settings.findItem(QStringLiteral("ShowExpiryNotifications"))->getDefault().toBool()); d->showExpirationCheckBox->setChecked(settings.showExpiryNotifications()); { ExpiryCheckerConfig expiryConfig; expiryConfig.setOwnKeyThresholdInDays(expiryConfig.ownKeyThresholdInDaysItem()->getDefault().toInt()); d->ownCertificateThresholdSpinBox->setValue(expiryConfig.ownKeyThresholdInDays()); expiryConfig.setOtherKeyThresholdInDays(expiryConfig.otherKeyThresholdInDaysItem()->getDefault().toInt()); d->otherCertificateThresholdSpinBox->setValue(expiryConfig.otherKeyThresholdInDays()); } // This simply means "default look for every category" for (int i = 0, end = d->categoriesLV->count(); i != end; ++i) { set_default_appearance(d->categoriesLV->item(i)); } settings.setShowValidity(settings.findItem(QStringLiteral("ShowValidity"))->getDefault().toBool()); d->tooltipValidityCheckBox->setChecked(settings.showValidity()); settings.setShowOwnerInformation(settings.findItem(QStringLiteral("ShowOwnerInformation"))->getDefault().toBool()); d->tooltipOwnerCheckBox->setChecked(settings.showOwnerInformation()); settings.setShowCertificateDetails(settings.findItem(QStringLiteral("ShowCertificateDetails"))->getDefault().toBool()); d->tooltipDetailsCheckBox->setChecked(settings.showCertificateDetails()); if (d->dnOrderWidget) { if (!settings.isImmutable(QStringLiteral("AttributeOrder"))) { - d->dnOrderWidget->setAttributeOrder(DN::defaultAttributeOrder()); + d->dnOrderWidget->setAttributeOrder(DNAttributes::defaultOrder()); } } Q_EMIT changed(); } bool greaterThanBySpecificity(const QString &lhs, const QString &rhs) { const auto lFilter = KeyFilterManager::instance()->keyFilterByID(lhs); const auto rFilter = KeyFilterManager::instance()->keyFilterByID(rhs); if (!lFilter) { return false; } if (!rFilter) { return true; } return lFilter->specificity() > rFilter->specificity(); } void AppearanceConfigWidget::load() { const Settings settings; d->showExpirationCheckBox->setChecked(settings.showExpiryNotifications()); d->showExpirationCheckBox->setEnabled(!settings.isImmutable(QStringLiteral("ShowExpiryNotifications"))); { const ExpiryCheckerConfig expiryConfig; d->ownCertificateThresholdSpinBox->setValue(expiryConfig.ownKeyThresholdInDays()); d->ownCertificateThresholdSpinBox->setEnabled(!expiryConfig.ownKeyThresholdInDaysItem()->isImmutable()); d->otherCertificateThresholdSpinBox->setValue(expiryConfig.otherKeyThresholdInDays()); d->otherCertificateThresholdSpinBox->setEnabled(!expiryConfig.otherKeyThresholdInDaysItem()->isImmutable()); } if (d->dnOrderWidget) { - d->dnOrderWidget->setAttributeOrder(DN::attributeOrder()); + d->dnOrderWidget->setAttributeOrder(DNAttributes::order()); d->dnOrderWidget->setEnabled(!settings.isImmutable(QStringLiteral("AttributeOrder"))); } d->categoriesLV->clear(); KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("libkleopatrarc")); if (!config) { return; } QStringList groups = config->groupList().filter(QRegularExpression(QStringLiteral("^Key Filter #\\d+$"))); std::ranges::stable_sort(groups, greaterThanBySpecificity); for (const QString &group : groups) { const KConfigGroup configGroup{config, group}; const bool isCmsSpecificKeyFilter = !configGroup.readEntry("is-openpgp-key", true); auto item = new QListWidgetItem{d->categoriesLV}; // hide CMS-specific filters if CMS is disabled; we hide those filters // instead of skipping them, so that they are not removed on save item->setHidden(isCmsSpecificKeyFilter && !Kleo::Settings{}.cmsEnabled()); apply_config(configGroup, item); } d->tooltipValidityCheckBox->setChecked(settings.showValidity()); d->tooltipValidityCheckBox->setEnabled(!settings.isImmutable(QStringLiteral("ShowValidity"))); d->tooltipOwnerCheckBox->setChecked(settings.showOwnerInformation()); d->tooltipOwnerCheckBox->setEnabled(!settings.isImmutable(QStringLiteral("ShowOwnerInformation"))); d->tooltipDetailsCheckBox->setChecked(settings.showCertificateDetails()); d->tooltipDetailsCheckBox->setEnabled(!settings.isImmutable(QStringLiteral("ShowCertificateDetails"))); } void AppearanceConfigWidget::save() { Settings settings; settings.setShowExpiryNotifications(d->showExpirationCheckBox->isChecked()); if (d->dnOrderWidget) { settings.setAttributeOrder(d->dnOrderWidget->attributeOrder()); - DN::setAttributeOrder(settings.attributeOrder()); + DNAttributes::setOrder(settings.attributeOrder()); } { ExpiryCheckerConfig expiryConfig; expiryConfig.setOwnKeyThresholdInDays(d->ownCertificateThresholdSpinBox->value()); expiryConfig.setOtherKeyThresholdInDays(d->otherCertificateThresholdSpinBox->value()); expiryConfig.save(); } settings.setShowValidity(d->tooltipValidityCheckBox->isChecked()); settings.setShowOwnerInformation(d->tooltipOwnerCheckBox->isChecked()); settings.setShowCertificateDetails(d->tooltipDetailsCheckBox->isChecked()); settings.save(); KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("libkleopatrarc")); if (!config) { return; } // We know (assume) that the groups in the config object haven't changed, // so we just iterate over them and over the listviewitems, and map one-to-one. QStringList groups = config->groupList().filter(QRegularExpression(QStringLiteral("^Key Filter #\\d+$"))); std::ranges::stable_sort(groups, greaterThanBySpecificity); #if 0 if (groups.isEmpty()) { // If we created the default categories ourselves just now, then we need to make up their list Q3ListViewItemIterator lvit(categoriesLV); for (; lvit.current(); ++lvit) { groups << lvit.current()->text(0); } } #endif for (int i = 0, end = std::min<int>(groups.size(), d->categoriesLV->count()); i != end; ++i) { const QListWidgetItem *const item = d->categoriesLV->item(i); Q_ASSERT(item); KConfigGroup group(config, groups[i]); save_to_config(item, group); } config->sync(); KeyFilterManager::instance()->reload(); } void AppearanceConfigWidget::Private::slotIconClicked() { QListWidgetItem *const item = selectedItem(); if (!item) { return; } const QString iconName = KIconDialog::getIcon(/* repeating default arguments begin */ KIconLoader::Desktop, KIconLoader::Application, false, 0, false, /* repeating default arguments end */ q); if (iconName.isEmpty()) { return; } item->setIcon(QIcon::fromTheme(iconName)); item->setData(IconNameRole, iconName); Q_EMIT q->changed(); } #ifndef QT_NO_COLORDIALOG void AppearanceConfigWidget::Private::slotForegroundClicked() { QListWidgetItem *const item = selectedItem(); if (!item) { return; } const QVariant v = brush2color(item->data(StoredForegroundRole)); const QColor initial = v.isValid() ? v.value<QColor>() : categoriesLV->palette().color(QPalette::Normal, QPalette::Text); const QColor c = QColorDialog::getColor(initial, q); if (c.isValid()) { item->setData(StoredForegroundRole, QBrush(c)); if (!SystemInfo::isHighContrastModeActive()) { item->setData(Qt::ForegroundRole, QBrush(c)); } Q_EMIT q->changed(); } } void AppearanceConfigWidget::Private::slotBackgroundClicked() { QListWidgetItem *const item = selectedItem(); if (!item) { return; } const QVariant v = brush2color(item->data(StoredBackgroundRole)); const QColor initial = v.isValid() ? v.value<QColor>() : categoriesLV->palette().color(QPalette::Normal, QPalette::Base); const QColor c = QColorDialog::getColor(initial, q); if (c.isValid()) { item->setData(StoredBackgroundRole, QBrush(c)); if (!SystemInfo::isHighContrastModeActive()) { item->setData(Qt::BackgroundRole, QBrush(c)); } Q_EMIT q->changed(); } } #endif // QT_NO_COLORDIALOG #ifndef QT_NO_FONTDIALOG void AppearanceConfigWidget::Private::slotFontClicked() { QListWidgetItem *const item = selectedItem(); if (!item) { return; } const QVariant v = item->data(Qt::FontRole); bool ok = false; const QFont defaultFont = tryToFindFontFor(item); const QFont initial = v.isValid() && v.userType() == QMetaType::QFont ? v.value<QFont>() : defaultFont; QFont f = QFontDialog::getFont(&ok, initial, q); if (!ok) { return; } // disallow circumventing KIOSK: if (!item->data(MayChangeItalicRole).toBool()) { f.setItalic(initial.italic()); } if (!item->data(MayChangeBoldRole).toBool()) { f.setBold(initial.bold()); } if (!item->data(MayChangeStrikeOutRole).toBool()) { f.setStrikeOut(initial.strikeOut()); } item->setData(Qt::FontRole, f != defaultFont ? f : QVariant()); item->setData(HasFontRole, true); Q_EMIT q->changed(); } #endif // QT_NO_FONTDIALOG void AppearanceConfigWidget::Private::slotItalicToggled(bool on) { set_italic(selectedItem(), on); Q_EMIT q->changed(); } void AppearanceConfigWidget::Private::slotBoldToggled(bool on) { set_bold(selectedItem(), on); Q_EMIT q->changed(); } void AppearanceConfigWidget::Private::slotStrikeOutToggled(bool on) { set_strikeout(selectedItem(), on); Q_EMIT q->changed(); } void AppearanceConfigWidget::Private::slotTooltipValidityChanged(bool) { Q_EMIT q->changed(); } void AppearanceConfigWidget::Private::slotTooltipOwnerChanged(bool) { Q_EMIT q->changed(); } void AppearanceConfigWidget::Private::slotTooltipDetailsChanged(bool) { Q_EMIT q->changed(); } #include "moc_appearanceconfigwidget.cpp" diff --git a/src/dialogs/certificatedetailsinputwidget.cpp b/src/dialogs/certificatedetailsinputwidget.cpp index b7ea5e1de..c6be9e771 100644 --- a/src/dialogs/certificatedetailsinputwidget.cpp +++ b/src/dialogs/certificatedetailsinputwidget.cpp @@ -1,363 +1,363 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #include "certificatedetailsinputwidget.h" #include <utils/userinfo.h> -#include <Libkleo/Dn> +#include <Libkleo/DnAttributes> #include <Libkleo/OidMap> #include <Libkleo/Stl_Util> #include <Libkleo/Validation> #include <KConfigGroup> #include <KLocalizedString> #include <KSharedConfig> #include <QGpgME/DN> #include <QLabel> #include <QLineEdit> #include <QVBoxLayout> #include <QValidator> #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Dialogs; namespace { struct Line { QString attr; QString label; QString regex; QLineEdit *edit; std::shared_ptr<QValidator> validator; bool required; }; QString attributeFromKey(QString key) { return key.remove(QLatin1Char('!')); } QString attributeLabel(const QString &attr) { if (attr.isEmpty()) { return QString(); } - const QString label = DN::attributeNameToLabel(attr); + const QString label = DNAttributes::nameToLabel(attr); if (!label.isEmpty()) { return i18nc("Format string for the labels in the \"Your Personal Data\" page", "%1 (%2)", label, attr); } else { return attr; } } QLineEdit *addRow(QGridLayout *l, const QString &label, const QString &preset, const std::shared_ptr<QValidator> &validator, bool readonly, bool required) { Q_ASSERT(l); auto lb = new QLabel(l->parentWidget()); lb->setText(i18nc("interpunctation for labels", "%1:", label)); auto le = new QLineEdit(l->parentWidget()); le->setText(preset); if (validator) { le->setValidator(validator.get()); } le->setReadOnly(readonly && le->hasAcceptableInput()); auto reqLB = new QLabel(l->parentWidget()); reqLB->setText(required ? i18n("(required)") : i18n("(optional)")); const int row = l->rowCount(); l->addWidget(lb, row, 0); l->addWidget(le, row, 1); l->addWidget(reqLB, row, 2); return le; } bool hasIntermediateInput(const QLineEdit *le) { QString text = le->text(); int pos = le->cursorPosition(); const QValidator *const v = le->validator(); return v && v->validate(text, pos) == QValidator::Intermediate; } QString requirementsAreMet(const QList<Line> &lines) { for (const Line &line : lines) { const QLineEdit *le = line.edit; if (!le) { continue; } qCDebug(KLEOPATRA_LOG) << "requirementsAreMet(): checking \"" << line.attr << "\" against \"" << le->text() << "\":"; if (le->text().trimmed().isEmpty()) { if (line.required) { if (line.regex.isEmpty()) { return xi18nc("@info", "<interface>%1</interface> is required, but empty.", line.label); } else { return xi18nc("@info", "<interface>%1</interface> is required, but empty.<nl/>" "Local Admin rule: <icode>%2</icode>", line.label, line.regex); } } } else if (hasIntermediateInput(le)) { if (line.regex.isEmpty()) { return xi18nc("@info", "<interface>%1</interface> is incomplete.", line.label); } else { return xi18nc("@info", "<interface>%1</interface> is incomplete.<nl/>" "Local Admin rule: <icode>%2</icode>", line.label, line.regex); } } else if (!le->hasAcceptableInput()) { if (line.regex.isEmpty()) { return xi18nc("@info", "<interface>%1</interface> is invalid.", line.label); } else { return xi18nc("@info", "<interface>%1</interface> is invalid.<nl/>" "Local Admin rule: <icode>%2</icode>", line.label, line.regex); } } } return QString(); } } class CertificateDetailsInputWidget::Private { friend class ::Kleo::Dialogs::CertificateDetailsInputWidget; CertificateDetailsInputWidget *const q; struct { QGridLayout *gridLayout; QList<Line> lines; QLineEdit *dn; QLabel *error; } ui; public: Private(CertificateDetailsInputWidget *qq) : q(qq) { auto mainLayout = new QVBoxLayout(q); ui.gridLayout = new QGridLayout(); mainLayout->addLayout(ui.gridLayout); createForm(); mainLayout->addStretch(1); ui.dn = new QLineEdit(); ui.dn->setFrame(false); ui.dn->setAlignment(Qt::AlignCenter); ui.dn->setReadOnly(true); mainLayout->addWidget(ui.dn); ui.error = new QLabel(); { QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); sizePolicy.setHeightForWidth(ui.error->sizePolicy().hasHeightForWidth()); ui.error->setSizePolicy(sizePolicy); } { QPalette palette; QBrush brush(QColor(255, 0, 0, 255)); brush.setStyle(Qt::SolidPattern); palette.setBrush(QPalette::Active, QPalette::WindowText, brush); palette.setBrush(QPalette::Inactive, QPalette::WindowText, brush); QBrush brush1(QColor(114, 114, 114, 255)); brush1.setStyle(Qt::SolidPattern); palette.setBrush(QPalette::Disabled, QPalette::WindowText, brush1); ui.error->setPalette(palette); } ui.error->setTextFormat(Qt::RichText); // set error label to have a fixed height of two lines: ui.error->setText(QStringLiteral("2<br>1")); ui.error->setFixedHeight(ui.error->minimumSizeHint().height()); ui.error->clear(); mainLayout->addWidget(ui.error); // select the preset text in the first line edit if (!ui.lines.empty()) { ui.lines.first().edit->selectAll(); } // explicitly update DN and check requirements after setup is complete updateDN(); checkRequirements(); } ~Private() { // remember current attribute values as presets for next certificate KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("CertificateCreationWizard")); for (const Line &line : ui.lines) { const QString attr = attributeFromKey(line.attr); const QString value = line.edit->text().trimmed(); config.writeEntry(attr, value); } config.sync(); } void createForm() { static const QStringList defaultAttributeOrder = { QStringLiteral("CN!"), QStringLiteral("EMAIL!"), QStringLiteral("L"), QStringLiteral("OU"), QStringLiteral("O"), QStringLiteral("C"), }; const KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("CertificateCreationWizard")); const QStringList attrOrder = config.readEntry("DNAttributeOrder", defaultAttributeOrder); for (const QString &rawKey : attrOrder) { const QString key = rawKey.trimmed().toUpper(); const QString attr = attributeFromKey(key); if (attr.isEmpty()) { continue; } const QString defaultPreset = // (attr == QLatin1StringView("CN")) ? userFullName() : (attr == QLatin1StringView("EMAIL")) ? userEmailAddress() : QString(); const QString preset = config.readEntry(attr, defaultPreset); const bool required = key.endsWith(QLatin1Char('!')); const bool readonly = config.isEntryImmutable(attr); const QString label = config.readEntry(attr + QLatin1StringView("_label"), attributeLabel(attr)); const QString regex = config.readEntry(attr + QLatin1StringView("_regex")); std::shared_ptr<QValidator> validator; if (attr == QLatin1StringView("EMAIL")) { validator = regex.isEmpty() ? Validation::email() : Validation::email(regex); } else if (!regex.isEmpty()) { validator = std::make_shared<QRegularExpressionValidator>(QRegularExpression{regex}); } QLineEdit *le = addRow(ui.gridLayout, label, preset, validator, readonly, required); const Line line = {attr, label, regex, le, validator, required}; ui.lines.push_back(line); if (attr != QLatin1StringView("EMAIL")) { connect(le, &QLineEdit::textChanged, le, [this]() { updateDN(); }); } connect(le, &QLineEdit::textChanged, le, [this]() { checkRequirements(); }); } } void updateDN() { ui.dn->setText(cmsDN()); } QString cmsDN() const { QGpgME::DN dn; for (const Line &line : ui.lines) { const QString text = line.edit->text().trimmed(); if (text.isEmpty()) { continue; } QString attr = attributeFromKey(line.attr); if (attr == QLatin1StringView("EMAIL")) { continue; } if (const char *const oid = oidForAttributeName(attr)) { attr = QString::fromUtf8(oid); } dn.append(QGpgME::DN::Attribute(attr, text)); } return dn.dn(); } void checkRequirements() { const QString error = requirementsAreMet(ui.lines); ui.error->setText(error); Q_EMIT q->validityChanged(error.isEmpty()); } QLineEdit *attributeWidget(const QString &attribute) { for (const Line &line : ui.lines) { if (attributeFromKey(line.attr) == attribute) { return line.edit; } } qCWarning(KLEOPATRA_LOG) << "CertificateDetailsInputWidget: No widget for attribute" << attribute; return nullptr; } void setAttributeValue(const QString &attribute, const QString &value) { QLineEdit *w = attributeWidget(attribute); if (w) { w->setText(value); } } QString attributeValue(const QString &attribute) { const QLineEdit *w = attributeWidget(attribute); return w ? w->text().trimmed() : QString(); } }; CertificateDetailsInputWidget::CertificateDetailsInputWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { } CertificateDetailsInputWidget::~CertificateDetailsInputWidget() { } void CertificateDetailsInputWidget::setName(const QString &name) { d->setAttributeValue(QStringLiteral("CN"), name); } void CertificateDetailsInputWidget::setEmail(const QString &email) { d->setAttributeValue(QStringLiteral("EMAIL"), email); } QString CertificateDetailsInputWidget::email() const { return d->attributeValue(QStringLiteral("EMAIL")); } QString CertificateDetailsInputWidget::dn() const { return d->ui.dn->text(); } #include "moc_certificatedetailsinputwidget.cpp" diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp index 4fcabde59..90ccd19c2 100644 --- a/src/dialogs/certificatedetailswidget.cpp +++ b/src/dialogs/certificatedetailswidget.cpp @@ -1,724 +1,724 @@ /* 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 <dev@ingo-kloecker.de> SPDX-FileCopyrightText: 2022 Felix Tiede SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-kleopatra.h> #include "certificatedetailswidget.h" #include "certificatedumpwidget.h" #include "dialogs/weboftrustwidget.h" #include "kleopatra_debug.h" #include "revokerswidget.h" #include "subkeyswidget.h" #include "trustchainwidget.h" #include "useridswidget.h" #include "commands/changeexpirycommand.h" #include "commands/detailscommand.h" #include "utils/accessibility.h" #include "view/infofield.h" #include <Libkleo/Algorithm> #include <Libkleo/Compliance> -#include <Libkleo/Dn> +#include <Libkleo/DnAttributes> #include <Libkleo/Formatting> #include <Libkleo/GnuPG> #include <Libkleo/KeyCache> #include <Libkleo/KeyHelpers> #include <Libkleo/TreeWidget> #include <KLocalizedString> #include <KMessageBox> #include <KSeparator> #include <gpgme++/context.h> #include <gpgme++/key.h> #include <gpgme++/keylistresult.h> #include <gpgme.h> #include <QGpgME/DN> #include <QGpgME/Debug> #include <QGpgME/KeyListJob> #include <QGpgME/Protocol> #include <QClipboard> #include <QDateTime> #include <QGridLayout> #include <QGuiApplication> #include <QHBoxLayout> #include <QLabel> #include <QListWidget> #include <QLocale> #include <QMenu> #include <QPushButton> #include <QStackedWidget> #include <QStringBuilder> #include <QTreeWidget> #include <QVBoxLayout> #include <map> #if __has_include(<ranges>) #include <ranges> #if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L #define USE_RANGES #endif #endif #include <set> Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; class CertificateDetailsWidget::Private { public: Private(CertificateDetailsWidget *qq); void setupCommonProperties(); void setUpSMIMEAdressList(); void setupPGPProperties(); void setupSMIMEProperties(); void refreshCertificate(); void changeExpiration(); void keysMayHaveChanged(); 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<GpgME::Key> &, const QString &, const GpgME::Error &); void copyFingerprintToClipboard(); void setUpdateInProgress(bool updateInProgress); void setTabVisible(QWidget *tab, bool visible); 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 { UserIdsWidget *userIDs = nullptr; std::map<QString, std::unique_ptr<InfoField>> smimeAttributeFields; std::unique_ptr<InfoField> smimeTrustLevelField; std::unique_ptr<InfoField> validFromField; std::unique_ptr<InfoField> expiresField; QAction *changeExpirationAction = nullptr; std::unique_ptr<InfoField> fingerprintField; QAction *copyFingerprintAction = nullptr; std::unique_ptr<InfoField> smimeIssuerField; QAction *showIssuerCertificateAction = nullptr; std::unique_ptr<InfoField> complianceField; std::unique_ptr<InfoField> trustedIntroducerField; std::unique_ptr<InfoField> primaryUserIdField; std::unique_ptr<InfoField> privateKeyInfoField; std::unique_ptr<InfoField> statusField; std::unique_ptr<InfoField> usageField; QListWidget *smimeAddressList = nullptr; QTabWidget *tabWidget = nullptr; SubKeysWidget *subKeysWidget = nullptr; WebOfTrustWidget *webOfTrustWidget = nullptr; TrustChainWidget *trustChainWidget = nullptr; CertificateDumpWidget *certificateDumpWidget = nullptr; RevokersWidget *revokersWidget = nullptr; void setupUi(QWidget *parent) { auto mainLayout = new QVBoxLayout{parent}; { auto gridLayout = new QGridLayout; gridLayout->setColumnStretch(1, 1); int row = -1; row++; primaryUserIdField = std::make_unique<InfoField>(i18n("User ID:"), parent); gridLayout->addWidget(primaryUserIdField->label(), row, 0); gridLayout->addLayout(primaryUserIdField->layout(), row, 1); - for (const auto &attribute : DN::attributeOrder()) { - const auto attributeLabel = DN::attributeNameToLabel(attribute); + for (const auto &attribute : DNAttributes::order()) { + const auto attributeLabel = DNAttributes::nameToLabel(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<InfoField>(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<InfoField>(i18n("Trust level:"), parent); gridLayout->addWidget(smimeTrustLevelField->label(), row, 0); gridLayout->addLayout(smimeTrustLevelField->layout(), row, 1); row++; validFromField = std::make_unique<InfoField>(i18n("Valid from:"), parent); gridLayout->addWidget(validFromField->label(), row, 0); gridLayout->addLayout(validFromField->layout(), row, 1); row++; expiresField = std::make_unique<InfoField>(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++; statusField = std::make_unique<InfoField>(i18n("Status:"), parent); gridLayout->addWidget(statusField->label(), row, 0); gridLayout->addLayout(statusField->layout(), row, 1); row++; fingerprintField = std::make_unique<InfoField>(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<InfoField>(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<InfoField>(i18n("Compliance:"), parent); gridLayout->addWidget(complianceField->label(), row, 0); gridLayout->addLayout(complianceField->layout(), row, 1); row++; usageField = std::make_unique<InfoField>(i18n("Usage:"), parent); gridLayout->addWidget(usageField->label(), row, 0); gridLayout->addLayout(usageField->layout(), row, 1); row++; trustedIntroducerField = std::make_unique<InfoField>(i18n("Trusted introducer for:"), parent); gridLayout->addWidget(trustedIntroducerField->label(), row, 0); trustedIntroducerField->setToolTip(i18nc("@info:tooltip", "See certifications for details.")); gridLayout->addLayout(trustedIntroducerField->layout(), row, 1); row++; privateKeyInfoField = std::make_unique<InfoField>(i18n("Private Key:"), parent); gridLayout->addWidget(privateKeyInfoField->label(), row, 0); gridLayout->addLayout(privateKeyInfoField->layout(), row, 1); mainLayout->addLayout(gridLayout); } tabWidget = new QTabWidget(parent); tabWidget->setDocumentMode(true); // we don't want a frame around the page widgets tabWidget->tabBar()->setDrawBase(false); // only draw the tabs mainLayout->addWidget(tabWidget); userIDs = new UserIdsWidget(parent); tabWidget->addTab(userIDs, i18nc("@title:tab", "User IDs")); smimeAddressList = new QListWidget{parent}; // Breeze draws no frame for scroll areas that are the only widget in a layout...unless we force it smimeAddressList->setProperty("_breeze_force_frame", true); smimeAddressList->setAccessibleName(i18n("Related addresses")); smimeAddressList->setEditTriggers(QAbstractItemView::NoEditTriggers); smimeAddressList->setSelectionMode(QAbstractItemView::SingleSelection); tabWidget->addTab(smimeAddressList, i18nc("@title:tab", "Related Addresses")); subKeysWidget = new SubKeysWidget(parent); tabWidget->addTab(subKeysWidget, i18nc("@title:tab", "Subkeys")); webOfTrustWidget = new WebOfTrustWidget(parent); tabWidget->addTab(webOfTrustWidget, i18nc("@title:tab", "Certifications")); trustChainWidget = new TrustChainWidget(parent); tabWidget->addTab(trustChainWidget, i18nc("@title:tab", "Trust Chain Details")); certificateDumpWidget = new CertificateDumpWidget(parent); tabWidget->addTab(certificateDumpWidget, i18nc("@title:tab", "Certificate Dump")); #if GPGME_VERSION_NUMBER >= 0x011800 // 1.24.0 revokersWidget = new RevokersWidget(parent); const auto index = tabWidget->addTab(revokersWidget, i18nc("@title:tab", "Revokers")); tabWidget->setTabVisible(index, false); #endif } } ui; }; CertificateDetailsWidget::Private::Private(CertificateDetailsWidget *qq) : q{qq} { ui.setupUi(q); connect(ui.changeExpirationAction, &QAction::triggered, q, [this]() { changeExpiration(); }); connect(ui.showIssuerCertificateAction, &QAction::triggered, q, [this]() { showIssuerCertificate(); }); if (ui.copyFingerprintAction) { connect(ui.copyFingerprintAction, &QAction::triggered, q, [this]() { copyFingerprintToClipboard(); }); } connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { keysMayHaveChanged(); }); connect(ui.userIDs, &UserIdsWidget::updateKey, q, [this]() { updateKey(); }); } void CertificateDetailsWidget::Private::setupCommonProperties() { const bool isOpenPGP = key.protocol() == GpgME::OpenPGP; const bool isSMIME = key.protocol() == GpgME::CMS; const bool isOwnKey = key.hasSecret(); 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() // update availability of buttons ui.changeExpirationAction->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())); ui.statusField->setValue(Formatting::complianceStringShort(key)); QString storage; const auto &subkey = key.subkey(0); if (!key.hasSecret()) { storage = i18nc("not applicable", "n/a"); } else if (key.subkey(0).isCardKey()) { if (const char *serialNo = subkey.cardSerialNumber()) { storage = i18nc("As in 'this secret key is stored on smart card <serial number>'", "smart card %1", QString::fromUtf8(serialNo)); } else { storage = i18nc("As in 'this secret key is stored on a smart card'", "smart card"); } } else if (!subkey.isSecret()) { storage = i18nc("key is 'offline key', i.e. secret key is not stored on this computer", "offline"); } else if (KeyCache::instance()->cardsForSubkey(subkey).size() > 0) { storage = i18ncp("As in 'this key is stored on this computer and on smart card(s)'", "On this computer and on a smart card", "On this computer and on %1 smart cards", KeyCache::instance()->cardsForSubkey(subkey).size()); } else { storage = i18nc("As in 'this secret key is stored on this computer'", "on this computer"); } ui.privateKeyInfoField->setValue(storage); if (DeVSCompliance::isCompliant()) { ui.complianceField->setValue(Kleo::Formatting::complianceStringForKey(key)); } ui.usageField->setVisible(key.protocol() == GpgME::CMS); QStringList usage; if (subkey.canEncrypt()) { usage += i18n("encryption"); } if (subkey.canSign()) { usage += i18n("signing"); } if (subkey.canCertify()) { usage += i18n("certification"); } if (subkey.canAuthenticate()) { usage += i18n("authentication"); } if (subkey.isQualified()) { usage += i18nc("as in: can be used for ...", "qualified signatures"); } ui.usageField->setValue(usage.join(i18nc("Separator between words in a list", ", "))); } 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 QGpgME::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.tabWidget->setTabVisible(1, false); } } 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(); } 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); } } 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<GpgME::UserID::Signature> &signatures) { return std::accumulate(std::begin(signatures), std::end(signatures), std::set<QString>(), [](auto domains, const auto &signature) { if (isGood(signature) && signature.isTrustSignature()) { domains.insert(Formatting::trustSignatureDomain(signature)); } return domains; }); } auto accumulateTrustDomains(const std::vector<GpgME::UserID> &userIds) { return std::accumulate(std::begin(userIds), std::end(userIds), std::set<QString>(), [](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::setTabVisible(QWidget *tab, bool visible) { ui.tabWidget->setTabVisible(ui.tabWidget->indexOf(tab), visible); } void CertificateDetailsWidget::Private::setupPGPProperties() { setTabVisible(ui.userIDs, true); setTabVisible(ui.smimeAddressList, false); setTabVisible(ui.subKeysWidget, true); setTabVisible(ui.webOfTrustWidget, true); setTabVisible(ui.trustChainWidget, false); setTabVisible(ui.certificateDumpWidget, false); ui.userIDs->setKey(key); ui.subKeysWidget->setKey(key); ui.webOfTrustWidget->setKey(key); #if GPGME_VERSION_NUMBER >= 0x011800 // 1.24.0 ui.revokersWidget->setKey(key); setTabVisible(ui.revokersWidget, key.numRevocationKeys() > 0); #endif const auto trustDomains = accumulateTrustDomains(key.userIDs()); ui.trustedIntroducerField->setVisible(!trustDomains.empty()); ui.trustedIntroducerField->setValue(QStringList(std::begin(trustDomains), std::end(trustDomains)).join(u", ")); ui.primaryUserIdField->setValue(Formatting::prettyUserID(key.userID(0))); ui.primaryUserIdField->setVisible(true); } static QString formatDNToolTip(const QGpgME::DN &dn) { QString html = QStringLiteral("<table border=\"0\" cell-spacing=15>"); const auto appendRow = [&html, dn](const QString &lbl, const QString &attr) { const QString val = dn[attr]; if (!val.isEmpty()) { html += QStringLiteral( "<tr><th style=\"text-align: left; white-space: nowrap\">%1:</th>" "<td style=\"white-space: nowrap\">%2</td>" "</tr>") .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("</table>"); return html; } void CertificateDetailsWidget::Private::setupSMIMEProperties() { setTabVisible(ui.userIDs, false); setTabVisible(ui.smimeAddressList, true); setTabVisible(ui.subKeysWidget, false); setTabVisible(ui.webOfTrustWidget, false); setTabVisible(ui.trustChainWidget, true); setTabVisible(ui.certificateDumpWidget, true); ui.trustChainWidget->setKey(key); const auto ownerId = key.userID(0); const QGpgME::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 QGpgME::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()); ui.primaryUserIdField->setVisible(false); ui.certificateDumpWidget->setKey(key); setUpSMIMEAdressList(); } 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<Private>(this)} { } CertificateDetailsWidget::~CertificateDetailsWidget() = default; void CertificateDetailsWidget::Private::keyListDone(const GpgME::KeyListResult &, const std::vector<GpgME::Key> &keys, const QString &, const GpgME::Error &) { setUpdateInProgress(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->setUpdateInProgress(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); ctx->addKeyListMode(GpgME::WithKeygrip); if (key.hasSecret()) { ctx->addKeyListMode(GpgME::WithSecret); } // Windows QGpgME new style connect problem makes this necessary. connect(job, SIGNAL(result(GpgME::KeyListResult, std::vector<GpgME::Key>, QString, GpgME::Error)), this, SLOT(keyListDone(GpgME::KeyListResult, std::vector<GpgME::Key>, QString, GpgME::Error))); job->start(QStringList() << QLatin1StringView(key.primaryFingerprint())); } GpgME::Key CertificateDetailsWidget::key() const { return d->key; } void CertificateDetailsWidget::Private::setUpdateInProgress(bool updateInProgress) { this->updateInProgress = updateInProgress; ui.userIDs->setUpdateInProgress(updateInProgress); } #include "moc_certificatedetailswidget.cpp" diff --git a/src/kleopatraapplication.cpp b/src/kleopatraapplication.cpp index 85067b270..589274c71 100644 --- a/src/kleopatraapplication.cpp +++ b/src/kleopatraapplication.cpp @@ -1,923 +1,923 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-kleopatra.h> #include "kleopatraapplication.h" #include "kleopatra_options.h" #include "mainwindow.h" #include "settings.h" #include "systrayicon.h" #include <conf/configuredialog.h> #include <conf/groupsconfigdialog.h> #include <dialogs/smartcardwindow.h> #include <smartcard/readerstatus.h> #include <Libkleo/GnuPG> #include <utils/kdpipeiodevice.h> #include <utils/log.h> #include <utils/userinfo.h> #include <gpgme++/key.h> #include <Libkleo/Classify> -#include <Libkleo/Dn> +#include <Libkleo/DnAttributes> #include <Libkleo/FileSystemWatcher> #include <Libkleo/KeyCache> #include <Libkleo/KeyFilterManager> #include <Libkleo/KeyGroupConfig> #include <Libkleo/SystemInfo> #include <uiserver/uiserver.h> #include "commands/checksumcreatefilescommand.h" #include "commands/checksumverifyfilescommand.h" #include "commands/decryptverifyfilescommand.h" #include "commands/detailscommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/lookupcertificatescommand.h" #include "commands/newcertificatesigningrequestcommand.h" #include "commands/newopenpgpcertificatecommand.h" #include "commands/signencryptfilescommand.h" #include "dialogs/updatenotification.h" #ifdef Q_OS_WIN #include <utils/winapi-helpers.h> #endif #include "kleopatra_debug.h" #include <KLocalizedString> #include <KMessageBox> #include <KWindowSystem> #if __has_include(<KWaylandExtras>) #include <KWaylandExtras> #define HAVE_WAYLAND #endif #include <QDesktopServices> #include <QDir> #include <QFile> #include <QFocusFrame> #include <QProxyStyle> #if QT_CONFIG(graphicseffect) #include <QGraphicsEffect> #endif #include <QPointer> #include <QSettings> #include <QStyleOption> #include <QStylePainter> #include <KSharedConfig> #ifdef Q_OS_WIN #include <windows.h> #endif using namespace Kleo; using namespace Kleo::Commands; using namespace Qt::Literals::StringLiterals; static void add_resources() { const QStringList iconDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, u"libkleopatra/pics"_s, QStandardPaths::LocateDirectory); // qCDebug(KLEOPATRA_LOG) << "Adding icon search paths:" << iconDirs; QIcon::setFallbackSearchPaths(QIcon::fallbackSearchPaths() << iconDirs); } static QList<QByteArray> default_logging_options() { QList<QByteArray> result; result.push_back("io"); return result; } namespace { class FocusFrame : public QFocusFrame { Q_OBJECT public: using QFocusFrame::QFocusFrame; protected: void paintEvent(QPaintEvent *event) override; }; static QRect effectiveWidgetRect(const QWidget *w) { // based on QWidgetPrivate::effectiveRectFor #if QT_CONFIG(graphicseffect) if (auto graphicsEffect = w->graphicsEffect(); graphicsEffect && graphicsEffect->isEnabled()) return graphicsEffect->boundingRectFor(w->rect()).toAlignedRect(); #endif // QT_CONFIG(graphicseffect) return w->rect(); } static QRect clipRect(const QWidget *w) { // based on QWidgetPrivate::clipRect if (!w->isVisible()) { return QRect(); } QRect r = effectiveWidgetRect(w); int ox = 0; int oy = 0; while (w && w->isVisible() && !w->isWindow() && w->parentWidget()) { ox -= w->x(); oy -= w->y(); w = w->parentWidget(); r &= QRect(ox, oy, w->width(), w->height()); } return r; } void FocusFrame::paintEvent(QPaintEvent *) { if (!widget()) { return; } QStylePainter p(this); QStyleOptionFocusRect option; initStyleOption(&option); const int vmargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &option); const int hmargin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, &option); const QRect rect = clipRect(widget()).adjusted(0, 0, hmargin * 2, vmargin * 2); p.setClipRect(rect); p.drawPrimitive(QStyle::PE_FrameFocusRect, option); } } class KleopatraApplication::Private { friend class ::KleopatraApplication; KleopatraApplication *const q; public: explicit Private(KleopatraApplication *qq) : q(qq) , ignoreNewInstance(true) , firstNewInstance(true) #ifndef QT_NO_SYSTEMTRAYICON , sysTray(nullptr) #endif { } ~Private() { #ifndef QT_NO_SYSTEMTRAYICON delete sysTray; #endif } void setUpSysTrayIcon() { #ifndef QT_NO_SYSTEMTRAYICON Q_ASSERT(readerStatus); sysTray = new SysTrayIcon(); sysTray->setFirstCardWithNullPin(readerStatus->firstCardWithNullPin()); connect(readerStatus.get(), &SmartCard::ReaderStatus::firstCardWithNullPinChanged, sysTray, &SysTrayIcon::setFirstCardWithNullPin); #endif } private: void connectConfigureDialog() { if (configureDialog) { if (q->mainWindow()) { connect(configureDialog, SIGNAL(configCommitted()), q->mainWindow(), SLOT(slotConfigCommitted())); } connect(configureDialog, &ConfigureDialog::configCommitted, q, &KleopatraApplication::configurationChanged); } } void disconnectConfigureDialog() { if (configureDialog) { if (q->mainWindow()) { disconnect(configureDialog, SIGNAL(configCommitted()), q->mainWindow(), SLOT(slotConfigCommitted())); } disconnect(configureDialog, &ConfigureDialog::configCommitted, q, &KleopatraApplication::configurationChanged); } } public: bool ignoreNewInstance; bool firstNewInstance; QPointer<FocusFrame> focusFrame; QPointer<ConfigureDialog> configureDialog; QPointer<GroupsConfigDialog> groupsConfigDialog; QPointer<MainWindow> mainWindow; QPointer<SmartCardWindow> smartCardWindow; std::unique_ptr<SmartCard::ReaderStatus> readerStatus; #ifndef QT_NO_SYSTEMTRAYICON SysTrayIcon *sysTray; #endif std::shared_ptr<KeyGroupConfig> groupConfig; std::shared_ptr<KeyCache> keyCache; std::shared_ptr<Log> log; std::shared_ptr<FileSystemWatcher> watcher; std::shared_ptr<QSettings> distroSettings; public: void setupKeyCache() { keyCache = KeyCache::mutableInstance(); keyCache->setRefreshInterval(Settings{}.refreshInterval()); watcher.reset(new FileSystemWatcher); watcher->whitelistFiles(gnupgFileWhitelist()); watcher->addPaths(gnupgFolderWhitelist()); watcher->setDelay(1000); keyCache->addFileSystemWatcher(watcher); keyCache->setGroupConfig(groupConfig); keyCache->setGroupsEnabled(Settings().groupsEnabled()); // always enable remarks (aka tags); in particular, this triggers a // relisting of the keys with signatures and signature notations // after the initial (fast) key listing keyCache->enableRemarks(true); } void setUpFilterManager() { if (!Settings{}.cmsEnabled()) { KeyFilterManager::instance()->alwaysFilterByProtocol(GpgME::OpenPGP); } } void setupLogging() { log = Log::mutableInstance(); const QByteArray envOptions = qgetenv("KLEOPATRA_LOGOPTIONS"); const bool logAll = envOptions.trimmed() == "all"; const QList<QByteArray> options = envOptions.isEmpty() ? default_logging_options() : envOptions.split(','); const QByteArray dirNative = qgetenv("KLEOPATRA_LOGDIR"); if (dirNative.isEmpty()) { return; } const QString dir = QFile::decodeName(dirNative); const QString logFileName = QDir(dir).absoluteFilePath(QStringLiteral("kleopatra.log.%1").arg(QCoreApplication::applicationPid())); std::unique_ptr<QFile> logFile(new QFile(logFileName)); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Append)) { qCDebug(KLEOPATRA_LOG) << "Could not open file for logging: " << logFileName << "\nLogging disabled"; return; } log->setOutputDirectory(dir); if (logAll || options.contains("io")) { log->setIOLoggingEnabled(true); } qInstallMessageHandler(Log::messageHandler); if (logAll || options.contains("pipeio")) { KDPipeIODevice::setDebugLevel(KDPipeIODevice::Debug); } UiServer::setLogStream(log->logFile()); } void updateFocusFrame(QWidget *focusWidget) { if (focusWidget && focusWidget->inherits("QLabel") && focusWidget->window()->testAttribute(Qt::WA_KeyboardFocusChange)) { if (!focusFrame) { focusFrame = new FocusFrame{focusWidget}; } focusFrame->setWidget(focusWidget); } else if (focusFrame) { focusFrame->setWidget(nullptr); } } MainWindow *getOrCreateMainWindow() { auto mw = q->mainWindow(); if (!mw) { mw = new MainWindow; mw->setAttribute(Qt::WA_DeleteOnClose); q->setMainWindow(mw); } return mw; } void exportFocusWindow() { #ifdef HAVE_WAYLAND KWaylandExtras::self()->exportWindow(QGuiApplication::focusWindow()); #endif } }; class KleopatraProxyStyle : public QProxyStyle { public: int styleHint(StyleHint hint, const QStyleOption *option = nullptr, const QWidget *widget = nullptr, QStyleHintReturn *returnData = nullptr) const override { // disable parent<->child navigation in tree views with left/right arrow keys // because this interferes with column by column navigation that is required // for accessibility if (hint == QStyle::SH_ItemView_ArrowKeysNavigateIntoChildren) return 0; return QProxyStyle::styleHint(hint, option, widget, returnData); } }; KleopatraApplication::KleopatraApplication(int &argc, char *argv[]) : QApplication(argc, argv) , d(new Private(this)) { setStyle(new KleopatraProxyStyle); #ifdef Q_OS_WIN if (SystemInfo::isHighContrastModeActive()) { // use colors specified by Windows if high-contrast mode is active QPalette highContrastPalette = palette(); const QColor linkColor = win_getSysColor(COLOR_HOTLIGHT); highContrastPalette.setColor(QPalette::All, QPalette::Link, linkColor); highContrastPalette.setColor(QPalette::All, QPalette::LinkVisited, linkColor); const QColor disabledColor = win_getSysColor(COLOR_GRAYTEXT); highContrastPalette.setColor(QPalette::Disabled, QPalette::WindowText, disabledColor); highContrastPalette.setColor(QPalette::Disabled, QPalette::Text, disabledColor); highContrastPalette.setColor(QPalette::Disabled, QPalette::ButtonText, disabledColor); highContrastPalette.setColor(QPalette::All, QPalette::PlaceholderText, disabledColor); setPalette(highContrastPalette); } #endif connect(this, &QApplication::focusChanged, this, [this](QWidget *, QWidget *now) { d->updateFocusFrame(now); }); } void KleopatraApplication::init() { const QString groupConfigPath = Kleo::gnupgHomeDirectory() + QStringLiteral("/kleopatra/kleopatragroupsrc"); d->groupConfig = std::make_shared<KeyGroupConfig>(groupConfigPath); const auto blockedUrlSchemes = Settings{}.blockedUrlSchemes(); for (const auto &scheme : blockedUrlSchemes) { QDesktopServices::setUrlHandler(scheme, this, "blockUrl"); } add_resources(); - DN::setAttributeOrder(Settings{}.attributeOrder()); + DNAttributes::setOrder(Settings{}.attributeOrder()); /* Start the gpg-agent early, this is done explicitly * because on an empty keyring our keylistings wont start * the agent. In that case any assuan-connect calls to * the agent will fail. The requested start via the * connection is additionally done in case the gpg-agent * is killed while Kleopatra is running. */ startGpgAgent(); d->readerStatus.reset(new SmartCard::ReaderStatus); connect(d->readerStatus.get(), &SmartCard::ReaderStatus::startOfGpgAgentRequested, this, &KleopatraApplication::startGpgAgent); d->setupKeyCache(); d->setUpSysTrayIcon(); d->setUpFilterManager(); d->setupLogging(); #ifndef QT_NO_SYSTEMTRAYICON if (d->sysTray) { d->sysTray->show(); } #endif #ifdef HAVE_WAYLAND connect(KWaylandExtras::self(), &KWaylandExtras::windowExported, this, [](const auto, const auto &token) { qputenv("PINENTRY_GEOM_HINT", QUrl::toPercentEncoding(token)); }); connect(qApp, &QGuiApplication::focusWindowChanged, this, [this](auto w) { if (!w) { return; } d->exportFocusWindow(); }); QMetaObject::invokeMethod( this, [this]() { d->exportFocusWindow(); }, Qt::QueuedConnection); #endif if (!Kleo::userIsElevated()) { // For users running Kleo with elevated permissions on Windows we // always quit the application when the last window is closed. setQuitOnLastWindowClosed(false); } // Sync config when we are about to quit connect(this, &QApplication::aboutToQuit, this, []() { KSharedConfig::openConfig()->sync(); }); } KleopatraApplication::~KleopatraApplication() { delete d->groupsConfigDialog; delete d->smartCardWindow; delete d->mainWindow; } namespace { using Func = void (KleopatraApplication::*)(const QStringList &, GpgME::Protocol); } void KleopatraApplication::slotActivateRequested(const QStringList &arguments, const QString &workingDirectory) { QCommandLineParser parser; kleopatra_options(&parser); QString err; if (!arguments.isEmpty() && !parser.parse(arguments)) { err = parser.errorText(); } else if (arguments.isEmpty()) { // KDBusServices omits the application name if no other // arguments are provided. In that case the parser prints // a warning. parser.parse(QStringList() << QCoreApplication::applicationFilePath()); } if (err.isEmpty()) { err = newInstance(parser, workingDirectory); } if (!err.isEmpty()) { KMessageBox::error(nullptr, err.toHtmlEscaped(), i18nc("@title:window", "Failed to execute command")); Q_EMIT setExitValue(1); return; } Q_EMIT setExitValue(0); } QString KleopatraApplication::newInstance(const QCommandLineParser &parser, const QString &workingDirectory) { if (d->ignoreNewInstance) { qCDebug(KLEOPATRA_LOG) << "New instance ignored because of ignoreNewInstance"; return QString(); } QStringList files; const QDir cwd = QDir(workingDirectory); bool queryMode = parser.isSet(QStringLiteral("query")) || parser.isSet(QStringLiteral("search")); // Query and Search treat positional arguments differently, see below. if (!queryMode) { const auto positionalArguments = parser.positionalArguments(); for (const QString &file : positionalArguments) { // We do not check that file exists here. Better handle // these errors in the UI. if (QFileInfo(file).isAbsolute()) { files << QDir::fromNativeSeparators(file); } else { files << cwd.absoluteFilePath(file); } } } GpgME::Protocol protocol = GpgME::UnknownProtocol; if (parser.isSet(QStringLiteral("openpgp"))) { qCDebug(KLEOPATRA_LOG) << "found OpenPGP"; protocol = GpgME::OpenPGP; } if (parser.isSet(QStringLiteral("cms"))) { qCDebug(KLEOPATRA_LOG) << "found CMS"; if (protocol == GpgME::OpenPGP) { return i18n("Ambiguous protocol: --openpgp and --cms"); } protocol = GpgME::CMS; } // Check for Parent Window id WId parentId = 0; if (parser.isSet(QStringLiteral("parent-windowid"))) { #ifdef Q_OS_WIN // WId is not a portable type as it is a pointer type on Windows. // casting it from an integer is ok though as the values are guaranteed to // be compatible in the documentation. parentId = static_cast<WId>((parser.value(QStringLiteral("parent-windowid")).toUInt())); #else parentId = parser.value(QStringLiteral("parent-windowid")).toUInt(); #endif } // Handle openpgp4fpr URI scheme QString needle; if (queryMode) { needle = parser.positionalArguments().join(QLatin1Char(' ')); } if (needle.startsWith(QLatin1StringView("openpgp4fpr:"))) { needle.remove(0, 12); } // Check for --search command. if (parser.isSet(QStringLiteral("search"))) { // This is an extra command instead of a combination with the // similar query to avoid changing the older query commands behavior // and query's "show details if a certificate exist or search on a // keyserver" logic is hard to explain and use consistently. if (needle.isEmpty()) { return i18n("No search string specified for --search"); } auto const cmd = new LookupCertificatesCommand(needle, nullptr); cmd->setParentWId(parentId); cmd->start(); return QString(); } // Check for --query command if (parser.isSet(QStringLiteral("query"))) { if (needle.isEmpty()) { return i18n("No fingerprint argument specified for --query"); } auto cmd = Command::commandForQuery(needle); cmd->setParentWId(parentId); cmd->start(); return QString(); } // Check for --gen-key command if (parser.isSet(QStringLiteral("gen-key"))) { if (protocol == GpgME::CMS) { const Kleo::Settings settings{}; if (settings.cmsEnabled() && settings.cmsCertificateCreationAllowed()) { auto cmd = new NewCertificateSigningRequestCommand; cmd->setParentWId(parentId); cmd->start(); } else { return i18n("You are not allowed to create S/MIME certificate signing requests."); } } else { auto cmd = new NewOpenPGPCertificateCommand; cmd->setParentWId(parentId); cmd->start(); } return QString(); } // Check for --config command if (parser.isSet(QStringLiteral("config"))) { openConfigDialogWithForeignParent(parentId); return QString(); } struct FuncInfo { QString optionName; Func func; }; // While most of these options can be handled by the content autodetection // below it might be useful to override the autodetection if the input is in // doubt and you e.g. only want to import .asc files or fail and not decrypt them // if they are actually encrypted data. static const std::vector<FuncInfo> funcMap{ {QStringLiteral("import-certificate"), &KleopatraApplication::importCertificatesFromFile}, {QStringLiteral("encrypt"), &KleopatraApplication::encryptFiles}, {QStringLiteral("sign"), &KleopatraApplication::signFiles}, {QStringLiteral("encrypt-sign"), &KleopatraApplication::signEncryptFiles}, {QStringLiteral("sign-encrypt"), &KleopatraApplication::signEncryptFiles}, {QStringLiteral("decrypt"), &KleopatraApplication::decryptFiles}, {QStringLiteral("verify"), &KleopatraApplication::verifyFiles}, {QStringLiteral("decrypt-verify"), &KleopatraApplication::decryptVerifyFiles}, {QStringLiteral("checksum"), &KleopatraApplication::checksumFiles}, }; QString found; Func foundFunc = nullptr; for (const auto &[opt, fn] : funcMap) { if (parser.isSet(opt) && found.isEmpty()) { found = opt; foundFunc = fn; } else if (parser.isSet(opt)) { return i18n(R"(Ambiguous commands "%1" and "%2")", found, opt); } } QStringList errors; if (!found.isEmpty()) { if (files.empty()) { return i18n("No files specified for \"%1\" command", found); } qCDebug(KLEOPATRA_LOG) << "found" << found; (this->*foundFunc)(files, protocol); } else { if (files.empty()) { if (!(d->firstNewInstance && isSessionRestored())) { qCDebug(KLEOPATRA_LOG) << "openOrRaiseMainWindow"; openOrRaiseMainWindow(); } } else { for (const QString &fileName : std::as_const(files)) { QFileInfo fi(fileName); if (!fi.isReadable()) { errors << i18n("Cannot read \"%1\"", fileName); } } handleFiles(files, parentId); } } d->firstNewInstance = false; #ifdef Q_OS_WIN // On Windows we might be started from the // explorer in any working directory. E.g. // a double click on a file. To avoid preventing // the folder from deletion we set the // working directory to the users homedir. QDir::setCurrent(QDir::homePath()); #endif return errors.join(QLatin1Char('\n')); } void KleopatraApplication::handleFiles(const QStringList &files, WId parentId) { auto mw = d->getOrCreateMainWindow(); const QList<Command *> allCmds = Command::commandsForFiles(files, mw->keyListController()); for (Command *cmd : allCmds) { if (parentId) { cmd->setParentWId(parentId); } else { cmd->setParentWidget(mw); } if (dynamic_cast<ImportCertificateFromFileCommand *>(cmd)) { openOrRaiseMainWindow(); } cmd->start(); } } const MainWindow *KleopatraApplication::mainWindow() const { return d->mainWindow; } MainWindow *KleopatraApplication::mainWindow() { return d->mainWindow; } void KleopatraApplication::setMainWindow(MainWindow *mainWindow) { if (mainWindow == d->mainWindow) { return; } d->disconnectConfigureDialog(); d->mainWindow = mainWindow; #ifndef QT_NO_SYSTEMTRAYICON if (d->sysTray) { d->sysTray->setMainWindow(mainWindow); } #endif d->connectConfigureDialog(); } static void open_or_raise(QWidget *w) { #ifdef Q_OS_WIN if (w->isMinimized()) { qCDebug(KLEOPATRA_LOG) << __func__ << "unminimizing and raising window"; w->raise(); } else if (w->isVisible()) { qCDebug(KLEOPATRA_LOG) << __func__ << "raising window"; w->raise(); #else if (w->isVisible()) { qCDebug(KLEOPATRA_LOG) << __func__ << "activating window"; KWindowSystem::updateStartupId(w->windowHandle()); KWindowSystem::activateWindow(w->windowHandle()); #endif } else { qCDebug(KLEOPATRA_LOG) << __func__ << "showing window"; w->show(); } } void KleopatraApplication::toggleMainWindowVisibility() { if (mainWindow()) { mainWindow()->setVisible(!mainWindow()->isVisible()); } else { openOrRaiseMainWindow(); } } void KleopatraApplication::restoreMainWindow() { qCDebug(KLEOPATRA_LOG) << "restoring main window"; // Sanity checks if (!isSessionRestored()) { qCDebug(KLEOPATRA_LOG) << "Not in session restore"; return; } if (mainWindow()) { qCDebug(KLEOPATRA_LOG) << "Already have main window"; return; } auto mw = d->getOrCreateMainWindow(); if (KMainWindow::canBeRestored(1)) { // restore to hidden state, Mainwindow::readProperties() will // restore saved visibility. mw->restore(1, false); } } void KleopatraApplication::openOrRaiseMainWindow() { auto mw = d->getOrCreateMainWindow(); open_or_raise(mw); UpdateNotification::checkUpdate(mw); } void KleopatraApplication::openOrRaiseSmartCardWindow() { if (!d->smartCardWindow) { d->smartCardWindow = new SmartCardWindow; d->smartCardWindow->setAttribute(Qt::WA_DeleteOnClose); } open_or_raise(d->smartCardWindow); } void KleopatraApplication::openConfigDialogWithForeignParent(WId parentWId) { if (!d->configureDialog) { d->configureDialog = new ConfigureDialog; d->configureDialog->setAttribute(Qt::WA_DeleteOnClose); d->connectConfigureDialog(); } // This is similar to what the commands do. if (parentWId) { if (QWidget *pw = QWidget::find(parentWId)) { d->configureDialog->setParent(pw, d->configureDialog->windowFlags()); } else { d->configureDialog->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(d->configureDialog->windowHandle(), parentWId); } } open_or_raise(d->configureDialog); // If we have a parent we want to raise over it. if (parentWId) { d->configureDialog->raise(); } } void KleopatraApplication::openOrRaiseConfigDialog() { openConfigDialogWithForeignParent(0); } void KleopatraApplication::openOrRaiseGroupsConfigDialog(QWidget *parent) { if (!d->groupsConfigDialog) { d->groupsConfigDialog = new GroupsConfigDialog{parent}; d->groupsConfigDialog->setAttribute(Qt::WA_DeleteOnClose); } else { // reparent the dialog to ensure it's shown on top of the (modal) parent d->groupsConfigDialog->setParent(parent, Qt::Dialog); } open_or_raise(d->groupsConfigDialog); } #ifndef QT_NO_SYSTEMTRAYICON void KleopatraApplication::startMonitoringSmartCard() { Q_ASSERT(d->readerStatus); d->readerStatus->startMonitoring(); } #endif // QT_NO_SYSTEMTRAYICON void KleopatraApplication::importCertificatesFromFile(const QStringList &files, GpgME::Protocol /*proto*/) { openOrRaiseMainWindow(); if (!files.empty()) { mainWindow()->importCertificatesFromFile(files); } } void KleopatraApplication::encryptFiles(const QStringList &files, GpgME::Protocol proto) { auto const cmd = new SignEncryptFilesCommand(files, nullptr); cmd->setEncryptionPolicy(Force); cmd->setSigningPolicy(Allow); if (proto != GpgME::UnknownProtocol) { cmd->setProtocol(proto); } cmd->start(); } void KleopatraApplication::signFiles(const QStringList &files, GpgME::Protocol proto) { auto const cmd = new SignEncryptFilesCommand(files, nullptr); cmd->setSigningPolicy(Force); cmd->setEncryptionPolicy(Deny); if (proto != GpgME::UnknownProtocol) { cmd->setProtocol(proto); } cmd->start(); } void KleopatraApplication::signEncryptFiles(const QStringList &files, GpgME::Protocol proto) { auto const cmd = new SignEncryptFilesCommand(files, nullptr); if (proto != GpgME::UnknownProtocol) { cmd->setProtocol(proto); } cmd->start(); } void KleopatraApplication::decryptFiles(const QStringList &files, GpgME::Protocol /*proto*/) { auto const cmd = new DecryptVerifyFilesCommand(files, nullptr); cmd->setOperation(Decrypt); cmd->start(); } void KleopatraApplication::verifyFiles(const QStringList &files, GpgME::Protocol /*proto*/) { auto const cmd = new DecryptVerifyFilesCommand(files, nullptr); cmd->setOperation(Verify); cmd->start(); } void KleopatraApplication::decryptVerifyFiles(const QStringList &files, GpgME::Protocol /*proto*/) { auto const cmd = new DecryptVerifyFilesCommand(files, nullptr); cmd->start(); } void KleopatraApplication::checksumFiles(const QStringList &files, GpgME::Protocol /*proto*/) { QStringList verifyFiles, createFiles; for (const QString &file : files) { if (isChecksumFile(file)) { verifyFiles << file; } else { createFiles << file; } } if (!verifyFiles.isEmpty()) { auto const cmd = new ChecksumVerifyFilesCommand(verifyFiles, nullptr); cmd->start(); } if (!createFiles.isEmpty()) { auto const cmd = new ChecksumCreateFilesCommand(createFiles, nullptr); cmd->start(); } } void KleopatraApplication::setIgnoreNewInstance(bool ignore) { d->ignoreNewInstance = ignore; } bool KleopatraApplication::ignoreNewInstance() const { return d->ignoreNewInstance; } void KleopatraApplication::blockUrl(const QUrl &url) { qCDebug(KLEOPATRA_LOG) << "Blocking URL" << url; KMessageBox::error(mainWindow(), i18n("Opening an external link is administratively prohibited."), i18nc("@title:window", "Prohibited")); } void KleopatraApplication::startGpgAgent() { Kleo::launchGpgAgent(); } void KleopatraApplication::setDistributionSettings(const std::shared_ptr<QSettings> &settings) { d->distroSettings = settings; } std::shared_ptr<QSettings> KleopatraApplication::distributionSettings() const { return d->distroSettings; } #include "kleopatraapplication.moc" #include "moc_kleopatraapplication.cpp" diff --git a/src/newcertificatewizard/enterdetailspage.cpp b/src/newcertificatewizard/enterdetailspage.cpp index 023386b5d..8e3ef2c55 100644 --- a/src/newcertificatewizard/enterdetailspage.cpp +++ b/src/newcertificatewizard/enterdetailspage.cpp @@ -1,531 +1,531 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016, 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-kleopatra.h> #include "enterdetailspage_p.h" #include "advancedsettingsdialog_p.h" #include "utils/userinfo.h" #include <settings.h> #include <Libkleo/Compat> -#include <Libkleo/Dn> +#include <Libkleo/DnAttributes> #include <Libkleo/Formatting> #include <Libkleo/OidMap> #include <Libkleo/Stl_Util> #include <Libkleo/Validation> #include <KAdjustingScrollArea> #include <KLocalizedString> #include <QGpgME/CryptoConfig> #include <QGpgME/DN> #include <QGpgME/Protocol> #include <QCheckBox> #include <QHBoxLayout> #include <QLabel> #include <QLineEdit> #include <QMetaProperty> #include <QPushButton> #include <QSpacerItem> #include <QVBoxLayout> #include <QValidator> #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::NewCertificateUi; using namespace GpgME; static void set_tab_order(const QList<QWidget *> &wl) { kdtools::for_each_adjacent_pair(wl.begin(), wl.end(), [](QWidget *w1, QWidget *w2) { QWidget::setTabOrder(w1, w2); }); } static QString pgpLabel(const QString &attr) { if (attr == QLatin1StringView("NAME")) { return i18n("Name"); } if (attr == QLatin1StringView("EMAIL")) { return i18n("EMail"); } return QString(); } static QString attributeLabel(const QString &attr, bool pgp) { if (attr.isEmpty()) { return QString(); } - const QString label = pgp ? pgpLabel(attr) : Kleo::DN::attributeNameToLabel(attr); + const QString label = pgp ? pgpLabel(attr) : Kleo::DNAttributes::nameToLabel(attr); if (!label.isEmpty()) if (pgp) { return label; } else return i18nc("Format string for the labels in the \"Your Personal Data\" page", "%1 (%2)", label, attr); else { return attr; } } static QString attributeFromKey(QString key) { return key.remove(QLatin1Char('!')); } struct EnterDetailsPage::UI { QGridLayout *gridLayout = nullptr; QLabel *nameLB = nullptr; QLineEdit *nameLE = nullptr; QLabel *nameRequiredLB = nullptr; QLabel *emailLB = nullptr; QLineEdit *emailLE = nullptr; QLabel *emailRequiredLB = nullptr; QCheckBox *withPassCB = nullptr; QLineEdit *resultLE = nullptr; QLabel *errorLB = nullptr; QPushButton *advancedPB = nullptr; UI(QWizardPage *parent) { parent->setTitle(i18nc("@title", "Enter Details")); auto mainLayout = new QVBoxLayout{parent}; const auto margins = mainLayout->contentsMargins(); mainLayout->setContentsMargins(margins.left(), 0, margins.right(), 0); auto scrollArea = new KAdjustingScrollArea{parent}; scrollArea->setFocusPolicy(Qt::NoFocus); scrollArea->setFrameStyle(QFrame::NoFrame); scrollArea->setBackgroundRole(parent->backgroundRole()); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setSizeAdjustPolicy(QScrollArea::AdjustToContents); auto widget = new QWidget; scrollArea->setWidget(widget); auto scrollAreaLayout = new QVBoxLayout(widget); scrollAreaLayout->setContentsMargins(0, margins.top(), 0, margins.bottom()); gridLayout = new QGridLayout; int row = 0; nameLB = new QLabel{i18n("Real name:"), parent}; nameLE = new QLineEdit{parent}; nameRequiredLB = new QLabel{i18n("(required)"), parent}; gridLayout->addWidget(nameLB, row, 0, 1, 1); gridLayout->addWidget(nameLE, row, 1, 1, 1); gridLayout->addWidget(nameRequiredLB, row, 2, 1, 1); row++; emailLB = new QLabel{i18n("EMail address:"), parent}; emailLE = new QLineEdit{parent}; emailRequiredLB = new QLabel{i18n("(required)"), parent}; gridLayout->addWidget(emailLB, row, 0, 1, 1); gridLayout->addWidget(emailLE, row, 1, 1, 1); gridLayout->addWidget(emailRequiredLB, row, 2, 1, 1); row++; withPassCB = new QCheckBox{i18n("Protect the generated key with a passphrase."), parent}; withPassCB->setToolTip( i18nc("@info:tooltip", "Encrypts the secret key with an unrecoverable passphrase. You will be asked for the passphrase during key generation.")); gridLayout->addWidget(withPassCB, row, 1, 1, 2); scrollAreaLayout->addLayout(gridLayout); auto verticalSpacer = new QSpacerItem{20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding}; scrollAreaLayout->addItem(verticalSpacer); resultLE = new QLineEdit{parent}; resultLE->setFrame(false); resultLE->setAlignment(Qt::AlignCenter); resultLE->setReadOnly(true); scrollAreaLayout->addWidget(resultLE); auto horizontalLayout = new QHBoxLayout; errorLB = new QLabel{parent}; QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); sizePolicy.setHeightForWidth(errorLB->sizePolicy().hasHeightForWidth()); errorLB->setSizePolicy(sizePolicy); QPalette palette; QBrush brush(QColor(255, 0, 0, 255)); brush.setStyle(Qt::SolidPattern); palette.setBrush(QPalette::Active, QPalette::WindowText, brush); palette.setBrush(QPalette::Inactive, QPalette::WindowText, brush); QBrush brush1(QColor(114, 114, 114, 255)); brush1.setStyle(Qt::SolidPattern); palette.setBrush(QPalette::Disabled, QPalette::WindowText, brush1); errorLB->setPalette(palette); errorLB->setTextFormat(Qt::RichText); horizontalLayout->addWidget(errorLB); advancedPB = new QPushButton{i18n("Advanced Settings..."), parent}; advancedPB->setAutoDefault(false); horizontalLayout->addWidget(advancedPB); scrollAreaLayout->addLayout(horizontalLayout); mainLayout->addWidget(scrollArea); } }; EnterDetailsPage::EnterDetailsPage(QWidget *p) : WizardPage{p} , ui{new UI{this}} , dialog{new AdvancedSettingsDialog{this}} { setObjectName(QLatin1StringView("Kleo__NewCertificateUi__EnterDetailsPage")); Settings settings; if (settings.hideAdvanced()) { setSubTitle(i18n("Please enter your personal details below.")); } else { setSubTitle(i18n("Please enter your personal details below. If you want more control over the parameters, click on the Advanced Settings button.")); } ui->advancedPB->setVisible(!settings.hideAdvanced()); ui->resultLE->setFocusPolicy(Qt::NoFocus); // set errorLB to have a fixed height of two lines: ui->errorLB->setText(QStringLiteral("2<br>1")); ui->errorLB->setFixedHeight(ui->errorLB->minimumSizeHint().height()); ui->errorLB->clear(); connect(ui->advancedPB, &QPushButton::clicked, this, &EnterDetailsPage::slotAdvancedSettingsClicked); connect(ui->resultLE, &QLineEdit::textChanged, this, &QWizardPage::completeChanged); // The email doesn't necessarily show up in ui->resultLE: connect(ui->emailLE, &QLineEdit::textChanged, this, &QWizardPage::completeChanged); registerDialogPropertiesAsFields(); registerField(QStringLiteral("dn"), ui->resultLE); registerField(QStringLiteral("name"), ui->nameLE); registerField(QStringLiteral("email"), ui->emailLE); registerField(QStringLiteral("protectedKey"), ui->withPassCB); setCommitPage(true); setButtonText(QWizard::CommitButton, i18nc("@action", "Create")); const auto conf = QGpgME::cryptoConfig(); if (!conf) { qCWarning(KLEOPATRA_LOG) << "Failed to obtain cryptoConfig."; return; } const auto entry = getCryptoConfigEntry(conf, "gpg-agent", "enforce-passphrase-constraints"); if (entry && entry->boolValue()) { qCDebug(KLEOPATRA_LOG) << "Disabling passphrace cb because of agent config."; ui->withPassCB->setEnabled(false); ui->withPassCB->setChecked(true); } else { const KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("CertificateCreationWizard")); ui->withPassCB->setChecked(config.readEntry("WithPassphrase", false)); ui->withPassCB->setEnabled(!config.isEntryImmutable("WithPassphrase")); } } EnterDetailsPage::~EnterDetailsPage() = default; void EnterDetailsPage::initializePage() { updateForm(); ui->withPassCB->setVisible(false); } void EnterDetailsPage::cleanupPage() { saveValues(); } void EnterDetailsPage::registerDialogPropertiesAsFields() { const QMetaObject *const mo = dialog->metaObject(); for (unsigned int i = mo->propertyOffset(), end = i + mo->propertyCount(); i != end; ++i) { const QMetaProperty mp = mo->property(i); if (mp.isValid()) { registerField(QLatin1StringView(mp.name()), dialog, mp.name(), SIGNAL(accepted())); } } } void EnterDetailsPage::saveValues() { for (const Line &line : std::as_const(lineList)) { savedValues[attributeFromKey(line.attr)] = line.edit->text().trimmed(); } } void EnterDetailsPage::clearForm() { qDeleteAll(dynamicWidgets); dynamicWidgets.clear(); lineList.clear(); ui->nameLE->hide(); ui->nameLE->clear(); ui->nameLB->hide(); ui->nameRequiredLB->hide(); ui->emailLE->hide(); ui->emailLE->clear(); ui->emailLB->hide(); ui->emailRequiredLB->hide(); } static int row_index_of(QWidget *w, QGridLayout *l) { const int idx = l->indexOf(w); int r, c, rs, cs; l->getItemPosition(idx, &r, &c, &rs, &cs); return r; } static QLineEdit * adjust_row(QGridLayout *l, int row, const QString &label, const QString &preset, const std::shared_ptr<QValidator> &validator, bool readonly, bool required) { Q_ASSERT(l); Q_ASSERT(row >= 0); Q_ASSERT(row < l->rowCount()); auto lb = qobject_cast<QLabel *>(l->itemAtPosition(row, 0)->widget()); Q_ASSERT(lb); auto le = qobject_cast<QLineEdit *>(l->itemAtPosition(row, 1)->widget()); Q_ASSERT(le); lb->setBuddy(le); // For better accessibility auto reqLB = qobject_cast<QLabel *>(l->itemAtPosition(row, 2)->widget()); Q_ASSERT(reqLB); lb->setText(i18nc("interpunctation for labels", "%1:", label)); le->setText(preset); reqLB->setText(required ? i18n("(required)") : i18n("(optional)")); if (validator) { le->setValidator(validator.get()); } le->setReadOnly(readonly && le->hasAcceptableInput()); lb->show(); le->show(); reqLB->show(); return le; } static int add_row(QGridLayout *l, QList<QWidget *> *wl) { Q_ASSERT(l); Q_ASSERT(wl); const int row = l->rowCount(); QWidget *w1, *w2, *w3; l->addWidget(w1 = new QLabel(l->parentWidget()), row, 0); l->addWidget(w2 = new QLineEdit(l->parentWidget()), row, 1); l->addWidget(w3 = new QLabel(l->parentWidget()), row, 2); wl->push_back(w1); wl->push_back(w2); wl->push_back(w3); return row; } void EnterDetailsPage::updateForm() { clearForm(); const auto settings = Kleo::Settings{}; const KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("CertificateCreationWizard")); QStringList attrOrder = config.readEntry("DNAttributeOrder", QStringList()); if (attrOrder.empty()) { attrOrder << QStringLiteral("CN!") << QStringLiteral("L") << QStringLiteral("OU") << QStringLiteral("O") << QStringLiteral("C") << QStringLiteral("EMAIL!"); } QList<QWidget *> widgets; widgets.push_back(ui->nameLE); widgets.push_back(ui->emailLE); QMap<int, Line> lines; for (const QString &rawKey : std::as_const(attrOrder)) { const QString key = rawKey.trimmed().toUpper(); const QString attr = attributeFromKey(key); if (attr.isEmpty()) { continue; } const QString preset = savedValues.value(attr, config.readEntry(attr, QString())); const bool required = key.endsWith(QLatin1Char('!')); const bool readonly = config.isEntryImmutable(attr); const QString label = config.readEntry(attr + QLatin1StringView("_label"), attributeLabel(attr, false)); const QString regex = config.readEntry(attr + QLatin1StringView("_regex")); const QString placeholder = config.readEntry(attr + QLatin1StringView{"_placeholder"}); int row; bool known = true; std::shared_ptr<QValidator> validator; if (attr == QLatin1StringView("EMAIL")) { row = row_index_of(ui->emailLE, ui->gridLayout); validator = regex.isEmpty() ? Validation::email() : Validation::email(regex); } else if (attr == QLatin1StringView("NAME") || attr == QLatin1StringView("CN")) { if (attr == QLatin1StringView("NAME")) { continue; } row = row_index_of(ui->nameLE, ui->gridLayout); } else { known = false; row = add_row(ui->gridLayout, &dynamicWidgets); } if (!validator && !regex.isEmpty()) { validator = std::make_shared<QRegularExpressionValidator>(QRegularExpression{regex}); } QLineEdit *le = adjust_row(ui->gridLayout, row, label, preset, validator, readonly, required); le->setPlaceholderText(placeholder); const Line line = {key, label, regex, le, validator}; lines[row] = line; if (!known) { widgets.push_back(le); } // don't connect twice: disconnect(le, &QLineEdit::textChanged, this, &EnterDetailsPage::slotUpdateResultLabel); connect(le, &QLineEdit::textChanged, this, &EnterDetailsPage::slotUpdateResultLabel); } // create lineList in visual order, so requirementsAreMet() // complains from top to bottom: lineList.reserve(lines.count()); std::copy(lines.cbegin(), lines.cend(), std::back_inserter(lineList)); widgets.push_back(ui->withPassCB); widgets.push_back(ui->advancedPB); if (ui->nameLE->text().isEmpty() && settings.prefillCN()) { ui->nameLE->setText(userFullName()); } if (ui->emailLE->text().isEmpty() && settings.prefillEmail()) { ui->emailLE->setText(userEmailAddress()); } slotUpdateResultLabel(); set_tab_order(widgets); } QString EnterDetailsPage::cmsDN() const { QGpgME::DN dn; for (QList<Line>::const_iterator it = lineList.begin(), end = lineList.end(); it != end; ++it) { const QString text = it->edit->text().trimmed(); if (text.isEmpty()) { continue; } QString attr = attributeFromKey(it->attr); if (attr == QLatin1StringView("EMAIL")) { continue; } if (const char *const oid = oidForAttributeName(attr)) { attr = QString::fromUtf8(oid); } dn.append(QGpgME::DN::Attribute(attr, text)); } return dn.dn(); } QString EnterDetailsPage::pgpUserID() const { return Formatting::prettyNameAndEMail(OpenPGP, QString(), ui->nameLE->text().trimmed(), ui->emailLE->text().trimmed(), QString()); } static bool has_intermediate_input(const QLineEdit *le) { QString text = le->text(); int pos = le->cursorPosition(); const QValidator *const v = le->validator(); return v && v->validate(text, pos) == QValidator::Intermediate; } static bool requirementsAreMet(const QList<EnterDetailsPage::Line> &list, QString &error) { bool allEmpty = true; for (const auto &line : list) { const QLineEdit *le = line.edit; if (!le) { continue; } const QString key = line.attr; qCDebug(KLEOPATRA_LOG) << "requirementsAreMet(): checking" << key << "against" << le->text() << ":"; if (le->text().trimmed().isEmpty()) { if (key.endsWith(QLatin1Char('!'))) { if (line.regex.isEmpty()) { error = xi18nc("@info", "<interface>%1</interface> is required, but empty.", line.label); } else error = xi18nc("@info", "<interface>%1</interface> is required, but empty.<nl/>" "Local Admin rule: <icode>%2</icode>", line.label, line.regex); return false; } } else if (has_intermediate_input(le)) { if (line.regex.isEmpty()) { error = xi18nc("@info", "<interface>%1</interface> is incomplete.", line.label); } else error = xi18nc("@info", "<interface>%1</interface> is incomplete.<nl/>" "Local Admin rule: <icode>%2</icode>", line.label, line.regex); return false; } else if (!le->hasAcceptableInput()) { if (line.regex.isEmpty()) { error = xi18nc("@info", "<interface>%1</interface> is invalid.", line.label); } else error = xi18nc("@info", "<interface>%1</interface> is invalid.<nl/>" "Local Admin rule: <icode>%2</icode>", line.label, line.regex); return false; } else { allEmpty = false; } } // Ensure that at least one value is acceptable return !allEmpty; } bool EnterDetailsPage::isComplete() const { QString error; const bool ok = requirementsAreMet(lineList, error); ui->errorLB->setText(error); return ok; } void EnterDetailsPage::slotAdvancedSettingsClicked() { dialog->exec(); } void EnterDetailsPage::slotUpdateResultLabel() { ui->resultLE->setText(cmsDN()); } #include "moc_enterdetailspage_p.cpp"