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"