File indexing completed on 2024-06-23 05:13:47

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     conf/dirservconfigpage.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2004, 2008 Klarälvdalens Datakonsult AB
0006 
0007     SPDX-FileCopyrightText: 2022 g10 Code GmbH
0008     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0009 
0010     SPDX-License-Identifier: GPL-2.0-or-later
0011 */
0012 
0013 #include <config-kleopatra.h>
0014 
0015 #include "dirservconfigpage.h"
0016 
0017 #include "labelledwidget.h"
0018 
0019 #include <settings.h>
0020 
0021 #include <Libkleo/Compat>
0022 #include <Libkleo/DirectoryServicesWidget>
0023 #include <Libkleo/GnuPG>
0024 #include <Libkleo/KeyserverConfig>
0025 
0026 #include <QGpgME/CryptoConfig>
0027 #include <QGpgME/Protocol>
0028 
0029 #include "kleopatra_debug.h"
0030 #include <KConfig>
0031 #include <KLocalizedString>
0032 #include <KMessageBox>
0033 #include <QSpinBox>
0034 
0035 #include <QCheckBox>
0036 #include <QGroupBox>
0037 #include <QLabel>
0038 #include <QLayout>
0039 #include <QLineEdit>
0040 #include <QTimeEdit>
0041 #include <QVBoxLayout>
0042 
0043 #include <gpgme++/engineinfo.h>
0044 #include <gpgme.h>
0045 
0046 using namespace Kleo;
0047 using namespace QGpgME;
0048 
0049 // Option for configuring X.509 servers (available via gpgconf since GnuPG 2.3.5 and 2.2.34)
0050 static const char s_x509services_componentName[] = "dirmngr";
0051 static const char s_x509services_entryName[] = "ldapserver";
0052 
0053 // Legacy option for configuring X.509 servers (deprecated with GnuPG 2.2.28 and 2.3.2)
0054 static const char s_x509services_legacy_componentName[] = "gpgsm";
0055 static const char s_x509services_legacy_entryName[] = "keyserver";
0056 
0057 static const char s_pgpservice_componentName[] = "dirmngr";
0058 static const char s_pgpservice_entryName[] = "keyserver";
0059 
0060 // legacy config entry used until GnuPG 2.2
0061 static const char s_pgpservice_legacy_componentName[] = "gpg";
0062 static const char s_pgpservice_legacy_entryName[] = "keyserver";
0063 
0064 static const char s_timeout_componentName[] = "dirmngr";
0065 static const char s_timeout_entryName[] = "ldaptimeout";
0066 
0067 static const char s_maxitems_componentName[] = "dirmngr";
0068 static const char s_maxitems_entryName[] = "max-replies";
0069 
0070 class DirectoryServicesConfigurationPage::Private
0071 {
0072     DirectoryServicesConfigurationPage *q = nullptr;
0073 
0074 public:
0075     Private(DirectoryServicesConfigurationPage *q);
0076 
0077     void load();
0078     void save();
0079     void defaults();
0080 
0081 private:
0082     enum EntryMultiplicity {
0083         SingleValue,
0084         ListValue,
0085     };
0086     enum ShowError {
0087         DoNotShowError,
0088         DoShowError,
0089     };
0090 
0091     void setX509ServerEntry(const std::vector<KeyserverConfig> &servers);
0092     void load(const Kleo::Settings &settings);
0093 
0094     QGpgME::CryptoConfigEntry *configEntry(const char *componentName,
0095                                            const char *entryName,
0096                                            QGpgME::CryptoConfigEntry::ArgType argType,
0097                                            EntryMultiplicity multiplicity,
0098                                            ShowError showError);
0099 
0100     Kleo::LabelledWidget<QLineEdit> mOpenPGPKeyserverEdit;
0101     Kleo::DirectoryServicesWidget *mDirectoryServices = nullptr;
0102     Kleo::LabelledWidget<QTimeEdit> mTimeout;
0103     Kleo::LabelledWidget<QSpinBox> mMaxItems;
0104     QCheckBox *mFetchMissingSignerKeysCB = nullptr;
0105     QCheckBox *mQueryWKDsForAllUserIDsCB = nullptr;
0106 
0107     QGpgME::CryptoConfigEntry *mOpenPGPServiceEntry = nullptr;
0108     QGpgME::CryptoConfigEntry *mTimeoutConfigEntry = nullptr;
0109     QGpgME::CryptoConfigEntry *mMaxItemsConfigEntry = nullptr;
0110 
0111     QGpgME::CryptoConfig *mConfig = nullptr;
0112 };
0113 
0114 DirectoryServicesConfigurationPage::Private::Private(DirectoryServicesConfigurationPage *q)
0115 {
0116     mConfig = QGpgME::cryptoConfig();
0117     auto glay = new QGridLayout(q->widget());
0118 
0119     // OpenPGP keyserver
0120     int row = 0;
0121     {
0122         auto l = new QHBoxLayout{};
0123         l->setContentsMargins(0, 0, 0, 0);
0124 
0125         mOpenPGPKeyserverEdit.createWidgets(q->widget());
0126         mOpenPGPKeyserverEdit.label()->setText(i18n("OpenPGP keyserver:"));
0127         if (engineIsVersion(2, 4, 4) //
0128             || (engineIsVersion(2, 2, 42) && !engineIsVersion(2, 3, 0))) {
0129             mOpenPGPKeyserverEdit.widget()->setToolTip( //
0130                 xi18nc("@info:tooltip",
0131                        "Enter the address of the keyserver to use when searching for OpenPGP certificates and "
0132                        "when uploading OpenPGP certificates. If you do not enter an address then an internal "
0133                        "default will be used. To disable the use of an OpenPGP keyserver enter the special value <emphasis>none</emphasis>."));
0134         }
0135         l->addWidget(mOpenPGPKeyserverEdit.label());
0136         l->addWidget(mOpenPGPKeyserverEdit.widget());
0137 
0138         glay->addLayout(l, row, 0, 1, 3);
0139         connect(mOpenPGPKeyserverEdit.widget(), &QLineEdit::textEdited, q, &DirectoryServicesConfigurationPage::markAsChanged);
0140     }
0141 
0142     // X.509 servers
0143     if (Settings{}.cmsEnabled()) {
0144         ++row;
0145         auto groupBox = new QGroupBox{i18n("X.509 Directory Services"), q->widget()};
0146         groupBox->setFlat(true);
0147         auto groupBoxLayout = new QVBoxLayout{groupBox};
0148         groupBoxLayout->setContentsMargins({});
0149 
0150         if (gpgme_check_version("1.16.0")) {
0151             mDirectoryServices = new Kleo::DirectoryServicesWidget(q->widget());
0152             if (QLayout *l = mDirectoryServices->layout()) {
0153                 l->setContentsMargins(0, 0, 0, 0);
0154             }
0155             groupBoxLayout->addWidget(mDirectoryServices);
0156             connect(mDirectoryServices, &DirectoryServicesWidget::changed, q, &DirectoryServicesConfigurationPage::markAsChanged);
0157         } else {
0158             // QGpgME does not properly support keyserver flags for X.509 keyservers (added in GnuPG 2.2.28);
0159             // disable the configuration to prevent the configuration from being corrupted
0160             groupBoxLayout->addWidget(new QLabel{i18n("Configuration of directory services is not possible "
0161                                                       "because the used gpgme libraries are too old."),
0162                                                  q->widget()});
0163         }
0164 
0165         glay->addWidget(groupBox, row, 0, 1, 3);
0166     }
0167 
0168     // LDAP timeout
0169     ++row;
0170     mTimeout.createWidgets(q->widget());
0171     mTimeout.label()->setText(i18n("LDAP &timeout (minutes:seconds):"));
0172     mTimeout.widget()->setDisplayFormat(QStringLiteral("mm:ss"));
0173     connect(mTimeout.widget(), &QTimeEdit::timeChanged, q, &DirectoryServicesConfigurationPage::markAsChanged);
0174     glay->addWidget(mTimeout.label(), row, 0);
0175     glay->addWidget(mTimeout.widget(), row, 1);
0176 
0177     // Max number of items returned by queries
0178     ++row;
0179     mMaxItems.createWidgets(q->widget());
0180     mMaxItems.label()->setText(i18n("&Maximum number of items returned by query:"));
0181     mMaxItems.widget()->setMinimum(0);
0182     connect(mMaxItems.widget(), &QSpinBox::valueChanged, q, &DirectoryServicesConfigurationPage::markAsChanged);
0183     glay->addWidget(mMaxItems.label(), row, 0);
0184     glay->addWidget(mMaxItems.widget(), row, 1);
0185 
0186     ++row;
0187     mFetchMissingSignerKeysCB = new QCheckBox{q->widget()};
0188     mFetchMissingSignerKeysCB->setText(i18nc("@option:check", "Retrieve missing certification keys when importing new keys"));
0189     mFetchMissingSignerKeysCB->setToolTip(xi18nc("@info:tooltip",
0190                                                  "If enabled, then Kleopatra will automatically try to retrieve the keys "
0191                                                  "that were used to certify the user IDs of newly imported OpenPGP keys."));
0192     connect(mFetchMissingSignerKeysCB, &QCheckBox::toggled, q, &DirectoryServicesConfigurationPage::markAsChanged);
0193     glay->addWidget(mFetchMissingSignerKeysCB, row, 0, 1, 3);
0194 
0195     ++row;
0196     mQueryWKDsForAllUserIDsCB = new QCheckBox{q->widget()};
0197     mQueryWKDsForAllUserIDsCB->setText(i18nc("@option:check", "Query certificate directories of providers for all user IDs"));
0198     mQueryWKDsForAllUserIDsCB->setToolTip(xi18nc("@info:tooltip",
0199                                                  "By default, Kleopatra only queries the certificate directories of providers (WKD) "
0200                                                  "for user IDs that were originally retrieved from a WKD when you update an OpenPGP "
0201                                                  "certificate. If this option is enabled, then Kleopatra will query WKDs for all user IDs."));
0202     connect(mQueryWKDsForAllUserIDsCB, &QCheckBox::toggled, q, &DirectoryServicesConfigurationPage::markAsChanged);
0203     glay->addWidget(mQueryWKDsForAllUserIDsCB, row, 0, 1, 3);
0204 
0205     glay->setRowStretch(++row, 1);
0206     glay->setColumnStretch(2, 1);
0207 }
0208 
0209 static auto readKeyserverConfigs(const CryptoConfigEntry *configEntry)
0210 {
0211     std::vector<KeyserverConfig> servers;
0212     if (configEntry) {
0213         const auto urls = configEntry->urlValueList();
0214         servers.reserve(urls.size());
0215         std::transform(std::begin(urls), std::end(urls), std::back_inserter(servers), &KeyserverConfig::fromUrl);
0216     }
0217     return servers;
0218 }
0219 
0220 void DirectoryServicesConfigurationPage::Private::load(const Kleo::Settings &settings)
0221 {
0222     if (mDirectoryServices) {
0223         mDirectoryServices->clear();
0224 
0225         // gpgsm uses the deprecated keyserver option in gpgsm.conf additionally to the ldapserver option in dirmngr.conf;
0226         // we (try to) read servers from both entries, but always write to the newest existing entry
0227         const auto *const newEntry =
0228             configEntry(s_x509services_componentName, s_x509services_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError);
0229         const auto *const legacyEntry =
0230             configEntry(s_x509services_legacy_componentName, s_x509services_legacy_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError);
0231         auto entry = newEntry ? newEntry : legacyEntry;
0232         if (entry) {
0233             const auto additionalServers = readKeyserverConfigs(legacyEntry);
0234             auto servers = readKeyserverConfigs(newEntry);
0235             std::copy(std::begin(additionalServers), std::end(additionalServers), std::back_inserter(servers));
0236             mDirectoryServices->setKeyservers(servers);
0237             mDirectoryServices->setReadOnly(entry->isReadOnly());
0238         } else {
0239             qCWarning(KLEOPATRA_LOG) << "Unknown or wrong typed config entries" << s_x509services_componentName << "/" << s_x509services_entryName << "and"
0240                                      << s_x509services_legacy_componentName << "/" << s_x509services_legacy_entryName;
0241 
0242             mDirectoryServices->setDisabled(true);
0243         }
0244     }
0245 
0246     {
0247         // gpg prefers the deprecated keyserver option in gpg.conf over the keyserver option in dirmngr.conf;
0248         // therefore, we use the deprecated keyserver option if it is set or if the new option doesn't exist (gpg < 2.1.9)
0249         auto const newEntry = configEntry(s_pgpservice_componentName, s_pgpservice_entryName, CryptoConfigEntry::ArgType_String, SingleValue, DoNotShowError);
0250         auto const legacyEntry =
0251             configEntry(s_pgpservice_legacy_componentName, s_pgpservice_legacy_entryName, CryptoConfigEntry::ArgType_String, SingleValue, DoNotShowError);
0252         mOpenPGPServiceEntry = ((legacyEntry && legacyEntry->isSet()) || !newEntry) ? legacyEntry : newEntry;
0253 
0254         if (!mOpenPGPServiceEntry) {
0255             qCWarning(KLEOPATRA_LOG) << "Unknown or wrong typed config entries" << s_pgpservice_componentName << "/" << s_pgpservice_entryName << "and"
0256                                      << s_pgpservice_legacy_componentName << "/" << s_pgpservice_legacy_entryName;
0257         } else if (mOpenPGPServiceEntry == legacyEntry) {
0258             qCDebug(KLEOPATRA_LOG) << "Using config entry" << s_pgpservice_legacy_componentName << "/" << s_pgpservice_legacy_entryName;
0259         } else {
0260             qCDebug(KLEOPATRA_LOG) << "Using config entry" << s_pgpservice_componentName << "/" << s_pgpservice_entryName;
0261         }
0262 
0263         mOpenPGPKeyserverEdit.widget()->setText(mOpenPGPServiceEntry && mOpenPGPServiceEntry->isSet() ? mOpenPGPServiceEntry->stringValue() : QString());
0264         mOpenPGPKeyserverEdit.setEnabled(mOpenPGPServiceEntry && !mOpenPGPServiceEntry->isReadOnly());
0265         if (newEntry && !newEntry->defaultValue().isNull()) {
0266             mOpenPGPKeyserverEdit.widget()->setPlaceholderText(newEntry->defaultValue().toString());
0267         } else {
0268             if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.16") {
0269                 mOpenPGPKeyserverEdit.widget()->setPlaceholderText(QStringLiteral("hkp://keys.gnupg.net"));
0270             } else {
0271                 mOpenPGPKeyserverEdit.widget()->setPlaceholderText(QStringLiteral("hkps://hkps.pool.sks-keyservers.net"));
0272             }
0273         }
0274     }
0275 
0276     // read LDAP timeout
0277     // first try to read the config entry as int (GnuPG 2.3)
0278     mTimeoutConfigEntry = configEntry(s_timeout_componentName, s_timeout_entryName, CryptoConfigEntry::ArgType_Int, SingleValue, DoNotShowError);
0279     if (!mTimeoutConfigEntry) {
0280         // if this fails, then try to read the config entry as unsigned int (GnuPG <= 2.2)
0281         mTimeoutConfigEntry = configEntry(s_timeout_componentName, s_timeout_entryName, CryptoConfigEntry::ArgType_UInt, SingleValue, DoShowError);
0282     }
0283     if (mTimeoutConfigEntry) {
0284         const int ldapTimeout = mTimeoutConfigEntry->argType() == CryptoConfigEntry::ArgType_Int ? mTimeoutConfigEntry->intValue()
0285                                                                                                  : static_cast<int>(mTimeoutConfigEntry->uintValue());
0286         const QTime time = QTime(0, 0, 0, 0).addSecs(ldapTimeout);
0287         // qCDebug(KLEOPATRA_LOG) <<"timeout:" << mTimeoutConfigEntry->uintValue() <<"  ->" << time;
0288         mTimeout.widget()->setTime(time);
0289     }
0290     mTimeout.setEnabled(mTimeoutConfigEntry && !mTimeoutConfigEntry->isReadOnly());
0291 
0292     // read max-replies config entry
0293     // first try to read the config entry as int (GnuPG 2.3)
0294     mMaxItemsConfigEntry = configEntry(s_maxitems_componentName, s_maxitems_entryName, CryptoConfigEntry::ArgType_Int, SingleValue, DoNotShowError);
0295     if (!mMaxItemsConfigEntry) {
0296         // if this fails, then try to read the config entry as unsigned int (GnuPG <= 2.2)
0297         mMaxItemsConfigEntry = configEntry(s_maxitems_componentName, s_maxitems_entryName, CryptoConfigEntry::ArgType_UInt, SingleValue, DoShowError);
0298     }
0299     if (mMaxItemsConfigEntry) {
0300         const int value = mMaxItemsConfigEntry->argType() == CryptoConfigEntry::ArgType_Int ? mMaxItemsConfigEntry->intValue()
0301                                                                                             : static_cast<int>(mMaxItemsConfigEntry->uintValue());
0302         mMaxItems.widget()->blockSignals(true); // KNumInput emits valueChanged from setValue!
0303         mMaxItems.widget()->setValue(value);
0304         mMaxItems.widget()->blockSignals(false);
0305     }
0306     mMaxItems.setEnabled(mMaxItemsConfigEntry && !mMaxItemsConfigEntry->isReadOnly());
0307 
0308     mFetchMissingSignerKeysCB->setChecked(settings.retrieveSignerKeysAfterImport());
0309     mFetchMissingSignerKeysCB->setEnabled(!settings.isImmutable(QStringLiteral("RetrieveSignerKeysAfterImport")));
0310     mQueryWKDsForAllUserIDsCB->setChecked(settings.queryWKDsForAllUserIDs());
0311     mQueryWKDsForAllUserIDsCB->setEnabled(!settings.isImmutable(QStringLiteral("QueryWKDsForAllUserIDs")));
0312 }
0313 
0314 void DirectoryServicesConfigurationPage::Private::load()
0315 {
0316     load(Settings{});
0317 }
0318 
0319 namespace
0320 {
0321 void updateIntegerConfigEntry(QGpgME::CryptoConfigEntry *configEntry, int value)
0322 {
0323     if (!configEntry) {
0324         return;
0325     }
0326     if (configEntry->argType() == CryptoConfigEntry::ArgType_Int) {
0327         if (configEntry->intValue() != value) {
0328             configEntry->setIntValue(value);
0329         }
0330     } else {
0331         const auto newValue = static_cast<unsigned>(value);
0332         if (configEntry->uintValue() != newValue) {
0333             configEntry->setUIntValue(newValue);
0334         }
0335     }
0336 }
0337 }
0338 
0339 void DirectoryServicesConfigurationPage::Private::setX509ServerEntry(const std::vector<KeyserverConfig> &servers)
0340 {
0341     const auto newEntry = configEntry(s_x509services_componentName, s_x509services_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError);
0342     const auto legacyEntry =
0343         configEntry(s_x509services_legacy_componentName, s_x509services_legacy_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError);
0344 
0345     if ((newEntry && newEntry->isReadOnly()) || (legacyEntry && legacyEntry->isReadOnly())) {
0346         // do not change the config entries if either config entry is read-only
0347         return;
0348     }
0349     QList<QUrl> urls;
0350     urls.reserve(servers.size());
0351     std::transform(std::begin(servers), std::end(servers), std::back_inserter(urls), std::mem_fn(&KeyserverConfig::toUrl));
0352     if (newEntry) {
0353         // write all servers to the new config entry
0354         newEntry->setURLValueList(urls);
0355         // and clear the legacy config entry
0356         if (legacyEntry) {
0357             legacyEntry->setURLValueList({});
0358         }
0359     } else if (legacyEntry) {
0360         // write all servers to the legacy config entry if the new entry is not available
0361         legacyEntry->setURLValueList(urls);
0362     } else {
0363         qCWarning(KLEOPATRA_LOG) << "Could not store the X.509 servers. Unknown or wrong typed config entries" << s_x509services_componentName << "/"
0364                                  << s_x509services_entryName << "and" << s_x509services_legacy_componentName << "/" << s_x509services_legacy_entryName;
0365     }
0366 }
0367 
0368 void DirectoryServicesConfigurationPage::Private::save()
0369 {
0370     if (mDirectoryServices && mDirectoryServices->isEnabled()) {
0371         setX509ServerEntry(mDirectoryServices->keyservers());
0372     }
0373 
0374     if (mOpenPGPServiceEntry) {
0375         const auto keyserver = mOpenPGPKeyserverEdit.widget()->text().trimmed();
0376         if (keyserver.isEmpty()) {
0377             mOpenPGPServiceEntry->resetToDefault();
0378         } else if (keyserver == QLatin1StringView{"none"}) {
0379             mOpenPGPServiceEntry->setStringValue(keyserver);
0380         } else {
0381             const auto keyserverUrl = keyserver.contains(QLatin1StringView{"://"}) ? keyserver : (QLatin1String{"hkps://"} + keyserver);
0382             mOpenPGPServiceEntry->setStringValue(keyserverUrl);
0383         }
0384     }
0385 
0386     const QTime time{mTimeout.widget()->time()};
0387     updateIntegerConfigEntry(mTimeoutConfigEntry, time.minute() * 60 + time.second());
0388 
0389     updateIntegerConfigEntry(mMaxItemsConfigEntry, mMaxItems.widget()->value());
0390 
0391     mConfig->sync(true);
0392 
0393     Settings settings;
0394     settings.setRetrieveSignerKeysAfterImport(mFetchMissingSignerKeysCB->isChecked());
0395     settings.setQueryWKDsForAllUserIDs(mQueryWKDsForAllUserIDsCB->isChecked());
0396     settings.save();
0397 }
0398 
0399 void DirectoryServicesConfigurationPage::Private::defaults()
0400 {
0401     // these guys don't have a default, to clear them:
0402     if (mDirectoryServices && mDirectoryServices->isEnabled()) {
0403         setX509ServerEntry({});
0404     }
0405     if (mOpenPGPServiceEntry && !mOpenPGPServiceEntry->isReadOnly()) {
0406         mOpenPGPServiceEntry->setStringValue(QString());
0407     }
0408     // these presumably have a default, use that one:
0409     if (mTimeoutConfigEntry && !mTimeoutConfigEntry->isReadOnly()) {
0410         mTimeoutConfigEntry->resetToDefault();
0411     }
0412     if (mMaxItemsConfigEntry && !mMaxItemsConfigEntry->isReadOnly()) {
0413         mMaxItemsConfigEntry->resetToDefault();
0414     }
0415 
0416     Settings settings;
0417     settings.setRetrieveSignerKeysAfterImport(settings.findItem(QStringLiteral("RetrieveSignerKeysAfterImport"))->getDefault().toBool());
0418     settings.setQueryWKDsForAllUserIDs(settings.findItem(QStringLiteral("QueryWKDsForAllUserIDs"))->getDefault().toBool());
0419 
0420     load(settings);
0421 }
0422 
0423 // Find config entry for ldap servers. Implements runtime checks on the configuration option.
0424 CryptoConfigEntry *DirectoryServicesConfigurationPage::Private::configEntry(const char *componentName,
0425                                                                             const char *entryName,
0426                                                                             CryptoConfigEntry::ArgType argType,
0427                                                                             EntryMultiplicity multiplicity,
0428                                                                             ShowError showError)
0429 {
0430     CryptoConfigEntry *const entry = Kleo::getCryptoConfigEntry(mConfig, componentName, entryName);
0431     if (!entry) {
0432         if (showError == DoShowError) {
0433             KMessageBox::error(
0434                 q->widget(),
0435                 i18n("Backend error: gpgconf does not seem to know the entry for %1/%2", QLatin1StringView(componentName), QLatin1String(entryName)));
0436         }
0437         return nullptr;
0438     }
0439     if (entry->argType() != argType || entry->isList() != bool(multiplicity)) {
0440         if (showError == DoShowError) {
0441             KMessageBox::error(q->widget(),
0442                                i18n("Backend error: gpgconf has wrong type for %1/%2: %3 %4",
0443                                     QLatin1StringView(componentName),
0444                                     QLatin1StringView(entryName),
0445                                     entry->argType(),
0446                                     entry->isList()));
0447         }
0448         return nullptr;
0449     }
0450     return entry;
0451 }
0452 
0453 DirectoryServicesConfigurationPage::DirectoryServicesConfigurationPage(QObject *parent, const KPluginMetaData &data)
0454     : KCModule(parent, data)
0455     , d{new Private{this}}
0456 {
0457 }
0458 
0459 DirectoryServicesConfigurationPage::~DirectoryServicesConfigurationPage() = default;
0460 
0461 void DirectoryServicesConfigurationPage::load()
0462 {
0463     d->load();
0464 }
0465 
0466 void DirectoryServicesConfigurationPage::save()
0467 {
0468     d->save();
0469 }
0470 
0471 void DirectoryServicesConfigurationPage::defaults()
0472 {
0473     d->defaults();
0474 }
0475 
0476 #include "moc_dirservconfigpage.cpp"