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

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     newcertificatewizard/enterdetailspage.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
0006     SPDX-FileCopyrightText: 2016, 2017 Bundesamt für Sicherheit in der Informationstechnik
0007     SPDX-FileContributor: Intevation GmbH
0008     SPDX-FileCopyrightText: 2022 g10 Code GmbH
0009     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0010 
0011     SPDX-License-Identifier: GPL-2.0-or-later
0012 */
0013 
0014 #include <config-kleopatra.h>
0015 
0016 #include "enterdetailspage_p.h"
0017 
0018 #include "advancedsettingsdialog_p.h"
0019 
0020 #include "utils/scrollarea.h"
0021 #include "utils/userinfo.h"
0022 #include "utils/validation.h"
0023 
0024 #include <settings.h>
0025 
0026 #include <Libkleo/Compat>
0027 #include <Libkleo/Dn>
0028 #include <Libkleo/Formatting>
0029 #include <Libkleo/OidMap>
0030 #include <Libkleo/Stl_Util>
0031 
0032 #include <KLocalizedString>
0033 
0034 #include <QGpgME/CryptoConfig>
0035 #include <QGpgME/Protocol>
0036 
0037 #include <QCheckBox>
0038 #include <QHBoxLayout>
0039 #include <QLabel>
0040 #include <QLineEdit>
0041 #include <QMetaProperty>
0042 #include <QPushButton>
0043 #include <QSpacerItem>
0044 #include <QVBoxLayout>
0045 #include <QValidator>
0046 
0047 #include "kleopatra_debug.h"
0048 
0049 using namespace Kleo;
0050 using namespace Kleo::NewCertificateUi;
0051 using namespace GpgME;
0052 
0053 static void set_tab_order(const QList<QWidget *> &wl)
0054 {
0055     kdtools::for_each_adjacent_pair(wl.begin(), wl.end(), [](QWidget *w1, QWidget *w2) {
0056         QWidget::setTabOrder(w1, w2);
0057     });
0058 }
0059 
0060 static QString pgpLabel(const QString &attr)
0061 {
0062     if (attr == QLatin1StringView("NAME")) {
0063         return i18n("Name");
0064     }
0065     if (attr == QLatin1StringView("EMAIL")) {
0066         return i18n("EMail");
0067     }
0068     return QString();
0069 }
0070 
0071 static QString attributeLabel(const QString &attr, bool pgp)
0072 {
0073     if (attr.isEmpty()) {
0074         return QString();
0075     }
0076     const QString label = pgp ? pgpLabel(attr) : Kleo::DN::attributeNameToLabel(attr);
0077     if (!label.isEmpty())
0078         if (pgp) {
0079             return label;
0080         } else
0081             return i18nc("Format string for the labels in the \"Your Personal Data\" page", "%1 (%2)", label, attr);
0082     else {
0083         return attr;
0084     }
0085 }
0086 
0087 static QString attributeFromKey(QString key)
0088 {
0089     return key.remove(QLatin1Char('!'));
0090 }
0091 
0092 struct EnterDetailsPage::UI {
0093     QGridLayout *gridLayout = nullptr;
0094     QLabel *nameLB = nullptr;
0095     QLineEdit *nameLE = nullptr;
0096     QLabel *nameRequiredLB = nullptr;
0097     QLabel *emailLB = nullptr;
0098     QLineEdit *emailLE = nullptr;
0099     QLabel *emailRequiredLB = nullptr;
0100     QCheckBox *withPassCB = nullptr;
0101     QLineEdit *resultLE = nullptr;
0102     QLabel *errorLB = nullptr;
0103     QPushButton *advancedPB = nullptr;
0104 
0105     UI(QWizardPage *parent)
0106     {
0107         parent->setTitle(i18nc("@title", "Enter Details"));
0108 
0109         auto mainLayout = new QVBoxLayout{parent};
0110         const auto margins = mainLayout->contentsMargins();
0111         mainLayout->setContentsMargins(margins.left(), 0, margins.right(), 0);
0112 
0113         auto scrollArea = new ScrollArea{parent};
0114         scrollArea->setFocusPolicy(Qt::NoFocus);
0115         scrollArea->setFrameStyle(QFrame::NoFrame);
0116         scrollArea->setBackgroundRole(parent->backgroundRole());
0117         scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0118         scrollArea->setSizeAdjustPolicy(QScrollArea::AdjustToContents);
0119         auto scrollAreaLayout = qobject_cast<QBoxLayout *>(scrollArea->widget()->layout());
0120         scrollAreaLayout->setContentsMargins(0, margins.top(), 0, margins.bottom());
0121 
0122         gridLayout = new QGridLayout;
0123         int row = 0;
0124 
0125         nameLB = new QLabel{i18n("Real name:"), parent};
0126         nameLE = new QLineEdit{parent};
0127         nameRequiredLB = new QLabel{i18n("(required)"), parent};
0128         gridLayout->addWidget(nameLB, row, 0, 1, 1);
0129         gridLayout->addWidget(nameLE, row, 1, 1, 1);
0130         gridLayout->addWidget(nameRequiredLB, row, 2, 1, 1);
0131 
0132         row++;
0133         emailLB = new QLabel{i18n("EMail address:"), parent};
0134         emailLE = new QLineEdit{parent};
0135         emailRequiredLB = new QLabel{i18n("(required)"), parent};
0136 
0137         gridLayout->addWidget(emailLB, row, 0, 1, 1);
0138         gridLayout->addWidget(emailLE, row, 1, 1, 1);
0139         gridLayout->addWidget(emailRequiredLB, row, 2, 1, 1);
0140 
0141         row++;
0142         withPassCB = new QCheckBox{i18n("Protect the generated key with a passphrase."), parent};
0143         withPassCB->setToolTip(i18n("Encrypts the secret key with an unrecoverable passphrase. You will be asked for the passphrase during key generation."));
0144         gridLayout->addWidget(withPassCB, row, 1, 1, 2);
0145 
0146         scrollAreaLayout->addLayout(gridLayout);
0147 
0148         auto verticalSpacer = new QSpacerItem{20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding};
0149 
0150         scrollAreaLayout->addItem(verticalSpacer);
0151 
0152         resultLE = new QLineEdit{parent};
0153         resultLE->setFrame(false);
0154         resultLE->setAlignment(Qt::AlignCenter);
0155         resultLE->setReadOnly(true);
0156 
0157         scrollAreaLayout->addWidget(resultLE);
0158 
0159         auto horizontalLayout = new QHBoxLayout;
0160         errorLB = new QLabel{parent};
0161         QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
0162         sizePolicy.setHorizontalStretch(0);
0163         sizePolicy.setVerticalStretch(0);
0164         sizePolicy.setHeightForWidth(errorLB->sizePolicy().hasHeightForWidth());
0165         errorLB->setSizePolicy(sizePolicy);
0166         QPalette palette;
0167         QBrush brush(QColor(255, 0, 0, 255));
0168         brush.setStyle(Qt::SolidPattern);
0169         palette.setBrush(QPalette::Active, QPalette::WindowText, brush);
0170         palette.setBrush(QPalette::Inactive, QPalette::WindowText, brush);
0171         QBrush brush1(QColor(114, 114, 114, 255));
0172         brush1.setStyle(Qt::SolidPattern);
0173         palette.setBrush(QPalette::Disabled, QPalette::WindowText, brush1);
0174         errorLB->setPalette(palette);
0175         errorLB->setTextFormat(Qt::RichText);
0176 
0177         horizontalLayout->addWidget(errorLB);
0178 
0179         advancedPB = new QPushButton{i18n("Advanced Settings..."), parent};
0180         advancedPB->setAutoDefault(false);
0181 
0182         horizontalLayout->addWidget(advancedPB);
0183 
0184         scrollAreaLayout->addLayout(horizontalLayout);
0185 
0186         mainLayout->addWidget(scrollArea);
0187     }
0188 };
0189 
0190 EnterDetailsPage::EnterDetailsPage(QWidget *p)
0191     : WizardPage{p}
0192     , ui{new UI{this}}
0193     , dialog{new AdvancedSettingsDialog{this}}
0194 {
0195     setObjectName(QLatin1StringView("Kleo__NewCertificateUi__EnterDetailsPage"));
0196 
0197     Settings settings;
0198     if (settings.hideAdvanced()) {
0199         setSubTitle(i18n("Please enter your personal details below."));
0200     } else {
0201         setSubTitle(i18n("Please enter your personal details below. If you want more control over the parameters, click on the Advanced Settings button."));
0202     }
0203     ui->advancedPB->setVisible(!settings.hideAdvanced());
0204     ui->resultLE->setFocusPolicy(Qt::NoFocus);
0205 
0206     // set errorLB to have a fixed height of two lines:
0207     ui->errorLB->setText(QStringLiteral("2<br>1"));
0208     ui->errorLB->setFixedHeight(ui->errorLB->minimumSizeHint().height());
0209     ui->errorLB->clear();
0210 
0211     connect(ui->advancedPB, &QPushButton::clicked, this, &EnterDetailsPage::slotAdvancedSettingsClicked);
0212     connect(ui->resultLE, &QLineEdit::textChanged, this, &QWizardPage::completeChanged);
0213     // The email doesn't necessarily show up in ui->resultLE:
0214     connect(ui->emailLE, &QLineEdit::textChanged, this, &QWizardPage::completeChanged);
0215     registerDialogPropertiesAsFields();
0216     registerField(QStringLiteral("dn"), ui->resultLE);
0217     registerField(QStringLiteral("name"), ui->nameLE);
0218     registerField(QStringLiteral("email"), ui->emailLE);
0219     registerField(QStringLiteral("protectedKey"), ui->withPassCB);
0220     setCommitPage(true);
0221     setButtonText(QWizard::CommitButton, i18nc("@action", "Create"));
0222 
0223     const auto conf = QGpgME::cryptoConfig();
0224     if (!conf) {
0225         qCWarning(KLEOPATRA_LOG) << "Failed to obtain cryptoConfig.";
0226         return;
0227     }
0228     const auto entry = getCryptoConfigEntry(conf, "gpg-agent", "enforce-passphrase-constraints");
0229     if (entry && entry->boolValue()) {
0230         qCDebug(KLEOPATRA_LOG) << "Disabling passphrace cb because of agent config.";
0231         ui->withPassCB->setEnabled(false);
0232         ui->withPassCB->setChecked(true);
0233     } else {
0234         const KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("CertificateCreationWizard"));
0235         ui->withPassCB->setChecked(config.readEntry("WithPassphrase", false));
0236         ui->withPassCB->setEnabled(!config.isEntryImmutable("WithPassphrase"));
0237     }
0238 }
0239 
0240 EnterDetailsPage::~EnterDetailsPage() = default;
0241 
0242 void EnterDetailsPage::initializePage()
0243 {
0244     updateForm();
0245     ui->withPassCB->setVisible(pgp());
0246     dialog->setProtocol(pgp() ? OpenPGP : CMS);
0247 }
0248 
0249 void EnterDetailsPage::cleanupPage()
0250 {
0251     saveValues();
0252 }
0253 
0254 void EnterDetailsPage::registerDialogPropertiesAsFields()
0255 {
0256     const QMetaObject *const mo = dialog->metaObject();
0257     for (unsigned int i = mo->propertyOffset(), end = i + mo->propertyCount(); i != end; ++i) {
0258         const QMetaProperty mp = mo->property(i);
0259         if (mp.isValid()) {
0260             registerField(QLatin1StringView(mp.name()), dialog, mp.name(), SIGNAL(accepted()));
0261         }
0262     }
0263 }
0264 
0265 void EnterDetailsPage::saveValues()
0266 {
0267     for (const Line &line : std::as_const(lineList)) {
0268         savedValues[attributeFromKey(line.attr)] = line.edit->text().trimmed();
0269     }
0270 }
0271 
0272 void EnterDetailsPage::clearForm()
0273 {
0274     qDeleteAll(dynamicWidgets);
0275     dynamicWidgets.clear();
0276     lineList.clear();
0277 
0278     ui->nameLE->hide();
0279     ui->nameLE->clear();
0280     ui->nameLB->hide();
0281     ui->nameRequiredLB->hide();
0282 
0283     ui->emailLE->hide();
0284     ui->emailLE->clear();
0285     ui->emailLB->hide();
0286     ui->emailRequiredLB->hide();
0287 }
0288 
0289 static int row_index_of(QWidget *w, QGridLayout *l)
0290 {
0291     const int idx = l->indexOf(w);
0292     int r, c, rs, cs;
0293     l->getItemPosition(idx, &r, &c, &rs, &cs);
0294     return r;
0295 }
0296 
0297 static QLineEdit *
0298 adjust_row(QGridLayout *l, int row, const QString &label, const QString &preset, const std::shared_ptr<QValidator> &validator, bool readonly, bool required)
0299 {
0300     Q_ASSERT(l);
0301     Q_ASSERT(row >= 0);
0302     Q_ASSERT(row < l->rowCount());
0303 
0304     auto lb = qobject_cast<QLabel *>(l->itemAtPosition(row, 0)->widget());
0305     Q_ASSERT(lb);
0306     auto le = qobject_cast<QLineEdit *>(l->itemAtPosition(row, 1)->widget());
0307     Q_ASSERT(le);
0308     lb->setBuddy(le); // For better accessibility
0309     auto reqLB = qobject_cast<QLabel *>(l->itemAtPosition(row, 2)->widget());
0310     Q_ASSERT(reqLB);
0311 
0312     lb->setText(i18nc("interpunctation for labels", "%1:", label));
0313     le->setText(preset);
0314     reqLB->setText(required ? i18n("(required)") : i18n("(optional)"));
0315     if (validator) {
0316         le->setValidator(validator.get());
0317     }
0318 
0319     le->setReadOnly(readonly && le->hasAcceptableInput());
0320 
0321     lb->show();
0322     le->show();
0323     reqLB->show();
0324 
0325     return le;
0326 }
0327 
0328 static int add_row(QGridLayout *l, QList<QWidget *> *wl)
0329 {
0330     Q_ASSERT(l);
0331     Q_ASSERT(wl);
0332     const int row = l->rowCount();
0333     QWidget *w1, *w2, *w3;
0334     l->addWidget(w1 = new QLabel(l->parentWidget()), row, 0);
0335     l->addWidget(w2 = new QLineEdit(l->parentWidget()), row, 1);
0336     l->addWidget(w3 = new QLabel(l->parentWidget()), row, 2);
0337     wl->push_back(w1);
0338     wl->push_back(w2);
0339     wl->push_back(w3);
0340     return row;
0341 }
0342 
0343 void EnterDetailsPage::updateForm()
0344 {
0345     clearForm();
0346 
0347     const auto settings = Kleo::Settings{};
0348     const KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("CertificateCreationWizard"));
0349 
0350     QStringList attrOrder = config.readEntry(pgp() ? "OpenPGPAttributeOrder" : "DNAttributeOrder", QStringList());
0351     if (attrOrder.empty()) {
0352         if (pgp()) {
0353             attrOrder << QStringLiteral("NAME") << QStringLiteral("EMAIL");
0354         } else {
0355             attrOrder << QStringLiteral("CN!") << QStringLiteral("L") << QStringLiteral("OU") << QStringLiteral("O") << QStringLiteral("C")
0356                       << QStringLiteral("EMAIL!");
0357         }
0358     }
0359 
0360     QList<QWidget *> widgets;
0361     widgets.push_back(ui->nameLE);
0362     widgets.push_back(ui->emailLE);
0363 
0364     QMap<int, Line> lines;
0365 
0366     for (const QString &rawKey : std::as_const(attrOrder)) {
0367         const QString key = rawKey.trimmed().toUpper();
0368         const QString attr = attributeFromKey(key);
0369         if (attr.isEmpty()) {
0370             continue;
0371         }
0372         const QString preset = savedValues.value(attr, config.readEntry(attr, QString()));
0373         const bool required = key.endsWith(QLatin1Char('!'));
0374         const bool readonly = config.isEntryImmutable(attr);
0375         const QString label = config.readEntry(attr + QLatin1StringView("_label"), attributeLabel(attr, pgp()));
0376         const QString regex = config.readEntry(attr + QLatin1StringView("_regex"));
0377         const QString placeholder = config.readEntry(attr + QLatin1StringView{"_placeholder"});
0378 
0379         int row;
0380         bool known = true;
0381         std::shared_ptr<QValidator> validator;
0382         if (attr == QLatin1StringView("EMAIL")) {
0383             row = row_index_of(ui->emailLE, ui->gridLayout);
0384             validator = regex.isEmpty() ? Validation::email() : Validation::email(regex);
0385         } else if (attr == QLatin1StringView("NAME") || attr == QLatin1String("CN")) {
0386             if ((pgp() && attr == QLatin1StringView("CN")) || (!pgp() && attr == QLatin1String("NAME"))) {
0387                 continue;
0388             }
0389             if (pgp()) {
0390                 validator = regex.isEmpty() ? Validation::pgpName() : Validation::pgpName(regex);
0391             }
0392             row = row_index_of(ui->nameLE, ui->gridLayout);
0393         } else {
0394             known = false;
0395             row = add_row(ui->gridLayout, &dynamicWidgets);
0396         }
0397         if (!validator && !regex.isEmpty()) {
0398             validator = std::make_shared<QRegularExpressionValidator>(QRegularExpression{regex});
0399         }
0400 
0401         QLineEdit *le = adjust_row(ui->gridLayout, row, label, preset, validator, readonly, required);
0402         le->setPlaceholderText(placeholder);
0403 
0404         const Line line = {key, label, regex, le, validator};
0405         lines[row] = line;
0406 
0407         if (!known) {
0408             widgets.push_back(le);
0409         }
0410 
0411         // don't connect twice:
0412         disconnect(le, &QLineEdit::textChanged, this, &EnterDetailsPage::slotUpdateResultLabel);
0413         connect(le, &QLineEdit::textChanged, this, &EnterDetailsPage::slotUpdateResultLabel);
0414     }
0415 
0416     // create lineList in visual order, so requirementsAreMet()
0417     // complains from top to bottom:
0418     lineList.reserve(lines.count());
0419     std::copy(lines.cbegin(), lines.cend(), std::back_inserter(lineList));
0420 
0421     widgets.push_back(ui->withPassCB);
0422     widgets.push_back(ui->advancedPB);
0423 
0424     const bool prefillName = (pgp() && settings.prefillName()) || (!pgp() && settings.prefillCN());
0425     if (ui->nameLE->text().isEmpty() && prefillName) {
0426         ui->nameLE->setText(userFullName());
0427     }
0428     if (ui->emailLE->text().isEmpty() && settings.prefillEmail()) {
0429         ui->emailLE->setText(userEmailAddress());
0430     }
0431 
0432     slotUpdateResultLabel();
0433 
0434     set_tab_order(widgets);
0435 }
0436 
0437 QString EnterDetailsPage::cmsDN() const
0438 {
0439     DN dn;
0440     for (QList<Line>::const_iterator it = lineList.begin(), end = lineList.end(); it != end; ++it) {
0441         const QString text = it->edit->text().trimmed();
0442         if (text.isEmpty()) {
0443             continue;
0444         }
0445         QString attr = attributeFromKey(it->attr);
0446         if (attr == QLatin1StringView("EMAIL")) {
0447             continue;
0448         }
0449         if (const char *const oid = oidForAttributeName(attr)) {
0450             attr = QString::fromUtf8(oid);
0451         }
0452         dn.append(DN::Attribute(attr, text));
0453     }
0454     return dn.dn();
0455 }
0456 
0457 QString EnterDetailsPage::pgpUserID() const
0458 {
0459     return Formatting::prettyNameAndEMail(OpenPGP, QString(), ui->nameLE->text().trimmed(), ui->emailLE->text().trimmed(), QString());
0460 }
0461 
0462 static bool has_intermediate_input(const QLineEdit *le)
0463 {
0464     QString text = le->text();
0465     int pos = le->cursorPosition();
0466     const QValidator *const v = le->validator();
0467     return v && v->validate(text, pos) == QValidator::Intermediate;
0468 }
0469 
0470 static bool requirementsAreMet(const QList<EnterDetailsPage::Line> &list, QString &error)
0471 {
0472     bool allEmpty = true;
0473     for (const auto &line : list) {
0474         const QLineEdit *le = line.edit;
0475         if (!le) {
0476             continue;
0477         }
0478         const QString key = line.attr;
0479         qCDebug(KLEOPATRA_LOG) << "requirementsAreMet(): checking" << key << "against" << le->text() << ":";
0480         if (le->text().trimmed().isEmpty()) {
0481             if (key.endsWith(QLatin1Char('!'))) {
0482                 if (line.regex.isEmpty()) {
0483                     error = xi18nc("@info", "<interface>%1</interface> is required, but empty.", line.label);
0484                 } else
0485                     error = xi18nc("@info",
0486                                    "<interface>%1</interface> is required, but empty.<nl/>"
0487                                    "Local Admin rule: <icode>%2</icode>",
0488                                    line.label,
0489                                    line.regex);
0490                 return false;
0491             }
0492         } else if (has_intermediate_input(le)) {
0493             if (line.regex.isEmpty()) {
0494                 error = xi18nc("@info", "<interface>%1</interface> is incomplete.", line.label);
0495             } else
0496                 error = xi18nc("@info",
0497                                "<interface>%1</interface> is incomplete.<nl/>"
0498                                "Local Admin rule: <icode>%2</icode>",
0499                                line.label,
0500                                line.regex);
0501             return false;
0502         } else if (!le->hasAcceptableInput()) {
0503             if (line.regex.isEmpty()) {
0504                 error = xi18nc("@info", "<interface>%1</interface> is invalid.", line.label);
0505             } else
0506                 error = xi18nc("@info",
0507                                "<interface>%1</interface> is invalid.<nl/>"
0508                                "Local Admin rule: <icode>%2</icode>",
0509                                line.label,
0510                                line.regex);
0511             return false;
0512         } else {
0513             allEmpty = false;
0514         }
0515     }
0516     // Ensure that at least one value is acceptable
0517     return !allEmpty;
0518 }
0519 
0520 bool EnterDetailsPage::isComplete() const
0521 {
0522     QString error;
0523     const bool ok = requirementsAreMet(lineList, error);
0524     ui->errorLB->setText(error);
0525     return ok;
0526 }
0527 
0528 void EnterDetailsPage::slotAdvancedSettingsClicked()
0529 {
0530     dialog->exec();
0531 }
0532 
0533 void EnterDetailsPage::slotUpdateResultLabel()
0534 {
0535     ui->resultLE->setText(pgp() ? pgpUserID() : cmsDN());
0536 }
0537 
0538 #include "moc_enterdetailspage_p.cpp"