diff --git a/src/dialogs/expirydialog.cpp b/src/dialogs/expirydialog.cpp index a4acfc542..a56fcf303 100644 --- a/src/dialogs/expirydialog.cpp +++ b/src/dialogs/expirydialog.cpp @@ -1,378 +1,390 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/expirydialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "expirydialog.h" #include "utils/expiration.h" #include "utils/gui-helper.h" #include "utils/qt-cxx20-compat.h" #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Dialogs; namespace { enum Period { Days, Weeks, Months, Years, NumPeriods }; static QDate date_by_amount_and_unit(int inAmount, int inUnit) { const QDate current = QDate::currentDate(); switch (inUnit) { case Days: return current.addDays(inAmount); case Weeks: return current.addDays(7 * inAmount); case Months: return current.addMonths(inAmount); case Years: return current.addYears(inAmount); default: Q_ASSERT(!"Should not reach here"); } return QDate(); } static QString accessibleValidityDuration(int amount, Period unit) { switch (unit) { case Days: return i18np("Valid for %1 day", "Valid for %1 days", amount); case Weeks: return i18np("Valid for %1 week", "Valid for %1 weeks", amount); case Months: return i18np("Valid for %1 month", "Valid for %1 months", amount); case Years: return i18np("Valid for %1 year", "Valid for %1 years", amount); default: Q_ASSERT(!"invalid unit"); } return {}; } // these calculations should be precise enough for the forseeable future... static const double DAYS_IN_GREGORIAN_YEAR = 365.2425; static int monthsBetween(const QDate &d1, const QDate &d2) { const int days = d1.daysTo(d2); return qRound(days / DAYS_IN_GREGORIAN_YEAR * 12); } static int yearsBetween(const QDate &d1, const QDate &d2) { const int days = d1.daysTo(d2); return qRound(days / DAYS_IN_GREGORIAN_YEAR); } } class ExpiryDialog::Private { friend class ::Kleo::Dialogs::ExpiryDialog; ExpiryDialog *const q; public: explicit Private(Mode mode, ExpiryDialog *qq) : q{qq} , mode{mode} , inUnit{Days} , ui{mode, q} { ui.neverRB->setEnabled(unlimitedValidityAllowed()); ui.inRB->setEnabled(!fixedExpirationDate()); ui.onRB->setEnabled(!fixedExpirationDate()); #if QT_DEPRECATED_SINCE(5, 14) connect(ui.inSB, qOverload(&QSpinBox::valueChanged), q, [this] () { slotInAmountChanged(); }); #else connect(ui.inSB, &QSpinBox::valueChanged, q, [this] () { slotInAmountChanged(); }); #endif connect(ui.inCB, qOverload(&QComboBox::currentIndexChanged), q, [this] () { slotInUnitChanged(); }); connect(ui.onCB, &KDateComboBox::dateChanged, q, [this] () { slotOnDateChanged(); }); Q_ASSERT(ui.inCB->currentIndex() == inUnit); } private: void slotInAmountChanged(); void slotInUnitChanged(); void slotOnDateChanged(); private: bool unlimitedValidityAllowed() const; bool fixedExpirationDate() const; QDate inDate() const; int inAmountByDate(const QDate &date) const; void setInitialFocus(); private: ExpiryDialog::Mode mode; int inUnit; bool initialFocusWasSet = false; struct UI { QRadioButton *neverRB; QRadioButton *inRB; QSpinBox *inSB; QComboBox *inCB; QRadioButton *onRB; KDateComboBox *onCB; QCheckBox *updateSubkeysCheckBox; explicit UI(Mode mode, Dialogs::ExpiryDialog *qq) { auto mainLayout = new QVBoxLayout{qq}; auto mainWidget = new QWidget{qq}; auto vboxLayout = new QVBoxLayout{mainWidget}; vboxLayout->setContentsMargins(0, 0, 0, 0); { auto label = new QLabel{qq}; label->setText(mode == Mode::UpdateIndividualSubkey ? i18n("Please select until when the subkey should be valid:") : i18n("Please select until when the certificate should be valid:")); vboxLayout->addWidget(label); } neverRB = new QRadioButton(i18n("Unlimited validity"), mainWidget); neverRB->setChecked(false); vboxLayout->addWidget(neverRB); { auto hboxLayout = new QHBoxLayout; inRB = new QRadioButton{i18n("Valid for:"), mainWidget}; inRB->setChecked(false); hboxLayout->addWidget(inRB); inSB = new QSpinBox{mainWidget}; inSB->setEnabled(false); inSB->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); inSB->setMinimum(1); hboxLayout->addWidget(inSB); inCB = new QComboBox{mainWidget}; inCB->addItem(i18n("Days")); inCB->addItem(i18n("Weeks")); inCB->addItem(i18n("Months")); inCB->addItem(i18n("Years")); Q_ASSERT(inCB->count() == NumPeriods); inCB->setEnabled(false); hboxLayout->addWidget(inCB); hboxLayout->addStretch(1); vboxLayout->addLayout(hboxLayout); } { auto hboxLayout = new QHBoxLayout; onRB = new QRadioButton{i18n("Valid until:"), mainWidget}; onRB->setChecked(true); hboxLayout->addWidget(onRB); onCB = new KDateComboBox{mainWidget}; setUpExpirationDateComboBox(onCB); hboxLayout->addWidget(onCB); hboxLayout->addStretch(1); vboxLayout->addLayout(hboxLayout); } { updateSubkeysCheckBox = new QCheckBox{i18n("Also update the validity period of the subkeys"), qq}; #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY updateSubkeysCheckBox->setVisible(mode == Mode::UpdateCertificateWithSubkeys); #else updateSubkeysCheckBox->setVisible(false); #endif vboxLayout->addWidget(updateSubkeysCheckBox); } vboxLayout->addStretch(1); mainLayout->addWidget(mainWidget); auto buttonBox = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel, qq}; auto okButton = buttonBox->button(QDialogButtonBox::Ok); KGuiItem::assign(okButton, KStandardGuiItem::ok()); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); qq->connect(buttonBox, &QDialogButtonBox::accepted, qq, &QDialog::accept); qq->connect(buttonBox, &QDialogButtonBox::rejected, qq, &QDialog::reject); mainLayout->addWidget(buttonBox); connect(onRB, &QRadioButton::toggled, onCB, &QWidget::setEnabled); connect(inRB, &QRadioButton::toggled, inCB, &QWidget::setEnabled); connect(inRB, &QRadioButton::toggled, inSB, &QWidget::setEnabled); } } ui; }; void ExpiryDialog::Private::slotInUnitChanged() { const int oldInAmount = ui.inSB->value(); const QDate targetDate = date_by_amount_and_unit(oldInAmount, inUnit); inUnit = ui.inCB->currentIndex(); if (targetDate.isValid()) { ui.inSB->setValue(inAmountByDate(targetDate)); } else { slotInAmountChanged(); } } void ExpiryDialog::Private::slotInAmountChanged() { if (ui.inRB->isChecked()) { ui.onCB->setDate(inDate()); } ui.inRB->setAccessibleName(accessibleValidityDuration(ui.inSB->value(), static_cast(ui.inCB->currentIndex()))); } void ExpiryDialog::Private::slotOnDateChanged() { if (!ui.inRB->isChecked()) { ui.inSB->setValue(inAmountByDate(ui.onCB->date())); } ui.onRB->setAccessibleName(i18nc("Valid until DATE", "Valid until %1", Formatting::accessibleDate(ui.onCB->date()))); } bool Kleo::Dialogs::ExpiryDialog::Private::unlimitedValidityAllowed() const { return !ui.onCB->maximumDate().isValid(); } bool Kleo::Dialogs::ExpiryDialog::Private::fixedExpirationDate() const { return ui.onCB->minimumDate() == ui.onCB->maximumDate(); } QDate ExpiryDialog::Private::inDate() const { return date_by_amount_and_unit(ui.inSB->value(), ui.inCB->currentIndex()); } int ExpiryDialog::Private::inAmountByDate(const QDate &selected) const { const QDate current = QDate::currentDate(); switch (ui.inCB->currentIndex()) { case Days: return current.daysTo(selected); case Weeks: return qRound(current.daysTo(selected) / 7.0); case Months: return monthsBetween(current, selected); case Years: return yearsBetween(current, selected); }; Q_ASSERT(!"Should not reach here"); return -1; } void ExpiryDialog::Private::setInitialFocus() { if (initialFocusWasSet) { return; } // give focus to the checked radio button (void) focusFirstCheckedButton({ui.neverRB, ui.inRB, ui.onRB}); initialFocusWasSet = true; } ExpiryDialog::ExpiryDialog(Mode mode, QWidget *p) : QDialog{p} , d{new Private{mode, this}} { setWindowTitle(i18nc("@title:window", "Change Validity Period")); } ExpiryDialog::~ExpiryDialog() = default; void ExpiryDialog::setDateOfExpiry(const QDate &date) { const QDate current = QDate::currentDate(); if (date.isValid()) { d->ui.onRB->setChecked(true); if (date <= current) { d->ui.onCB->setDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); } else { d->ui.onCB->setDate(date); } } else { if (d->unlimitedValidityAllowed()) { d->ui.neverRB->setChecked(true); } else { d->ui.onRB->setChecked(true); } d->ui.onCB->setDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); } } QDate ExpiryDialog::dateOfExpiry() const { return d->ui.inRB->isChecked() ? d->inDate() : d->ui.onRB->isChecked() ? d->ui.onCB->date() : QDate{}; } void ExpiryDialog::setUpdateExpirationOfAllSubkeys(bool update) { d->ui.updateSubkeysCheckBox->setChecked(update); } bool ExpiryDialog::updateExpirationOfAllSubkeys() const { return d->ui.updateSubkeysCheckBox->isChecked(); } +void ExpiryDialog::accept() +{ + const auto date = dateOfExpiry(); + if (!Kleo::isValidExpirationDate(date)) { + KMessageBox::error(this, i18nc("@info", "Error: %1", Kleo::validityPeriodHint())); + return; + } + + QDialog::accept(); +} + void ExpiryDialog::showEvent(QShowEvent *event) { d->setInitialFocus(); QDialog::showEvent(event); } #include "moc_expirydialog.cpp" diff --git a/src/dialogs/expirydialog.h b/src/dialogs/expirydialog.h index c6222de94..5c8a64dab 100644 --- a/src/dialogs/expirydialog.h +++ b/src/dialogs/expirydialog.h @@ -1,56 +1,58 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/expirydialog.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include class QDate; class QShowEvent; namespace Kleo { namespace Dialogs { class ExpiryDialog : public QDialog { Q_OBJECT public: enum class Mode { UpdateCertificateWithSubkeys, UpdateCertificateWithoutSubkeys, UpdateIndividualSubkey, }; explicit ExpiryDialog(Mode mode, QWidget *parent = nullptr); ~ExpiryDialog() override; void setDateOfExpiry(const QDate &date); QDate dateOfExpiry() const; void setUpdateExpirationOfAllSubkeys(bool update); bool updateExpirationOfAllSubkeys() const; + void accept() override; + protected: void showEvent(QShowEvent *event) override; private: class Private; std::unique_ptr d; }; } } diff --git a/src/utils/expiration.cpp b/src/utils/expiration.cpp index 1af97885f..d74648f07 100644 --- a/src/utils/expiration.cpp +++ b/src/utils/expiration.cpp @@ -1,111 +1,128 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/expiration.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "expiration.h" #include #include #include QDate Kleo::maximumAllowedDate() { static const QDate maxAllowedDate{2106, 2, 5}; return maxAllowedDate; } QDate Kleo::minimumExpirationDate() { return expirationDateRange().minimum; } QDate Kleo::maximumExpirationDate() { return expirationDateRange().maximum; } Kleo::DateRange Kleo::expirationDateRange() { Kleo::DateRange range; const auto settings = Kleo::Settings{}; const auto today = QDate::currentDate(); const auto minimumExpiry = std::max(1, settings.validityPeriodInDaysMin()); range.minimum = std::min(today.addDays(minimumExpiry), maximumAllowedDate()); const auto maximumExpiry = settings.validityPeriodInDaysMax(); if (maximumExpiry >= 0) { range.maximum = std::min(std::max(today.addDays(maximumExpiry), range.minimum), maximumAllowedDate()); } return range; } QDate Kleo::defaultExpirationDate(Kleo::ExpirationOnUnlimitedValidity onUnlimitedValidity) { QDate expirationDate; const auto settings = Kleo::Settings{}; const auto defaultExpirationInDays = settings.validityPeriodInDays(); if (defaultExpirationInDays > 0) { expirationDate = QDate::currentDate().addDays(defaultExpirationInDays); } else if (defaultExpirationInDays < 0 || onUnlimitedValidity == ExpirationOnUnlimitedValidity::InternalDefaultExpiration) { expirationDate = QDate::currentDate().addYears(3); } const auto allowedRange = expirationDateRange(); expirationDate = std::max(expirationDate, allowedRange.minimum); if (allowedRange.maximum.isValid()) { expirationDate = std::min(expirationDate, allowedRange.maximum); } return expirationDate; } +bool Kleo::isValidExpirationDate(const QDate &date) +{ + const auto allowedRange = expirationDateRange(); + if (date.isValid()) { + return (date >= allowedRange.minimum // + && (!allowedRange.maximum.isValid() || date <= allowedRange.maximum)); + } else { + return !allowedRange.maximum.isValid(); + } +} + static QString dateToString(const QDate &date, QWidget *widget) { // workaround for QLocale using "yy" way too often for years // stolen from KDateComboBox auto locale = widget ? widget->locale() : QLocale{}; const auto dateFormat = (locale.dateFormat(QLocale::ShortFormat) // .replace(QLatin1String{"yy"}, QLatin1String{"yyyy"}) .replace(QLatin1String{"yyyyyyyy"}, QLatin1String{"yyyy"})); return locale.toString(date, dateFormat); } static QString validityPeriodHint(const Kleo::DateRange &dateRange, QWidget *widget) { // the minimum date is always valid if (dateRange.maximum.isValid()) { if (dateRange.maximum == dateRange.minimum) { return i18nc("@info", "The validity period cannot be changed."); } else { - return i18nc("@info ... between and .", "The validity period must end between %1 and %2.", + return i18nc("@info ... between and .", "Enter a date between %1 and %2.", dateToString(dateRange.minimum, widget), dateToString(dateRange.maximum, widget)); } } else { - return i18nc("@info ... after .", "The validity period must end after %1.", dateToString(dateRange.minimum, widget)); + return i18nc("@info ... between and .", "Enter a date between %1 and %2.", + dateToString(dateRange.minimum, widget), dateToString(Kleo::maximumAllowedDate(), widget)); } } +QString Kleo::validityPeriodHint() +{ + return ::validityPeriodHint(expirationDateRange(), nullptr); +} + void Kleo::setUpExpirationDateComboBox(KDateComboBox *dateCB) { const auto dateRange = expirationDateRange(); dateCB->setMinimumDate(dateRange.minimum); dateCB->setMaximumDate(dateRange.maximum); if (dateRange.minimum == dateRange.maximum) { // validity period is a fixed number of days dateCB->setEnabled(false); } dateCB->setToolTip(validityPeriodHint(dateRange, dateCB)); } diff --git a/src/utils/expiration.h b/src/utils/expiration.h index 4d4798293..14b74a97e 100644 --- a/src/utils/expiration.h +++ b/src/utils/expiration.h @@ -1,83 +1,96 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/expiration.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include class KDateComboBox; namespace Kleo { struct DateRange { QDate minimum; QDate maximum; }; /** * Returns a date a bit before the technically possible latest expiration * date (~2106-02-07) that is safe to use as latest expiration date. */ QDate maximumAllowedDate(); /** * Returns the earliest allowed expiration date. * * This is either tomorrow or the configured number of days after today * (whichever is later). * * \sa Settings::validityPeriodInDaysMin */ QDate minimumExpirationDate(); /** * Returns the latest allowed expiration date. * * If unlimited validity is allowed, then an invalid date is returned. * Otherwise, either the configured number of days after today or * the maximum allowed date, whichever is earlier, is returned. * Additionally, the returned date is never earlier than the minimum * expiration date. * * \sa Settings::validityPeriodInDaysMax */ QDate maximumExpirationDate(); /** * Returns the allowed range for the expiration date. * * \sa minimumExpirationDate, maximumExpirationDate */ DateRange expirationDateRange(); enum class ExpirationOnUnlimitedValidity { NoExpiration, InternalDefaultExpiration, }; /** * Returns a useful value for the default expiration date based on the current * date and the configured default validity. If the configured validity is * unlimited, then the return value depends on \p onUnlimitedValidity. * * The returned value is always in the allowed range for the expiration date. * * \sa expirationDateRange */ QDate defaultExpirationDate(ExpirationOnUnlimitedValidity onUnlimitedValidity); + /** + * Returns true, if \p date is a valid expiration date. + */ + bool isValidExpirationDate(const QDate &date); + + /** + * Returns a hint which dates are valid expiration dates for the date + * combo box \p dateCB. + * The hint can be used as tool tip or as error message when the user + * entered an invalid date. + */ + QString validityPeriodHint(); + /** * Configures the date combo box \p dateCB for choosing an expiration date. * * Sets the allowed date range and a tooltip. And disables the combo box * if a fixed validity period is configured. */ void setUpExpirationDateComboBox(KDateComboBox *dateCB); }