File indexing completed on 2024-12-22 04:56:57

0001 /*
0002     SPDX-FileCopyrightText: 2009 Grégory Oestreicher <greg@kamago.net>
0003       Based on an original work for the IMAP resource which is :
0004     SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>
0005     SPDX-FileCopyrightText: 2008 Omat Holding B.V. <info@omat.nl>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "settings.h"
0011 
0012 #include "davresource_debug.h"
0013 #include "settingsadaptor.h"
0014 #include "utils.h"
0015 
0016 #include <KAuthorized>
0017 #include <KLocalizedString>
0018 
0019 #include <KDAV/ProtocolInfo>
0020 
0021 #include <KPasswordLineEdit>
0022 #include <QByteArray>
0023 #include <QCoreApplication>
0024 #include <QDBusConnection>
0025 #include <QDataStream>
0026 #include <QDialog>
0027 #include <QDialogButtonBox>
0028 #include <QFile>
0029 #include <QFileInfo>
0030 #include <QHBoxLayout>
0031 #include <QLabel>
0032 #include <QPointer>
0033 #include <QPushButton>
0034 #include <QRegularExpression>
0035 #include <QUrl>
0036 #include <QVBoxLayout>
0037 
0038 #include <qt6keychain/keychain.h>
0039 using namespace QKeychain;
0040 
0041 class SettingsHelper
0042 {
0043 public:
0044     SettingsHelper() = default;
0045 
0046     ~SettingsHelper()
0047     {
0048         delete q;
0049     }
0050 
0051     Settings *q = nullptr;
0052 };
0053 
0054 Q_GLOBAL_STATIC(SettingsHelper, s_globalSettings)
0055 
0056 Settings::UrlConfiguration::UrlConfiguration() = default;
0057 
0058 Settings::UrlConfiguration::UrlConfiguration(const QString &serialized)
0059 {
0060     const QStringList splitString = serialized.split(QLatin1Char('|'));
0061 
0062     if (splitString.size() == 3) {
0063         mUrl = splitString.at(2);
0064         mProtocol = KDAV::ProtocolInfo::protocolByName(splitString.at(1));
0065         mUser = splitString.at(0);
0066     }
0067 }
0068 
0069 QString Settings::UrlConfiguration::serialize()
0070 {
0071     QString serialized = mUser;
0072     serialized.append(QLatin1Char('|')).append(KDAV::ProtocolInfo::protocolName(KDAV::Protocol(mProtocol)));
0073     serialized.append(QLatin1Char('|')).append(mUrl);
0074     return serialized;
0075 }
0076 
0077 Settings *Settings::self()
0078 {
0079     if (!s_globalSettings->q) {
0080         new Settings;
0081         s_globalSettings->q->load();
0082     }
0083 
0084     return s_globalSettings->q;
0085 }
0086 
0087 Settings::Settings()
0088     : SettingsBase()
0089     , mWinId(0)
0090 {
0091     Q_ASSERT(!s_globalSettings->q);
0092     s_globalSettings->q = this;
0093 
0094     new SettingsAdaptor(this);
0095     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
0096                                                  this,
0097                                                  QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents);
0098 
0099     if (settingsVersion() == 1) {
0100         updateToV2();
0101     } else if (settingsVersion() == 2) {
0102         updateToV3();
0103     }
0104 }
0105 
0106 Settings::~Settings()
0107 {
0108     QMapIterator<QString, UrlConfiguration *> it(mUrls);
0109     while (it.hasNext()) {
0110         it.next();
0111         delete it.value();
0112     }
0113 }
0114 
0115 void Settings::setWinId(WId winId)
0116 {
0117     mWinId = winId;
0118 }
0119 
0120 void Settings::cleanup()
0121 {
0122     const QString entry = mResourceIdentifier + QLatin1Char(',') + QStringLiteral("$default$");
0123     auto deleteJob = new DeletePasswordJob(QStringLiteral("Passwords"));
0124     deleteJob->setKey(entry);
0125     deleteJob->start();
0126     QFile cacheFile(mCollectionsUrlsMappingCache);
0127     cacheFile.remove();
0128 }
0129 
0130 void Settings::setResourceIdentifier(const QString &identifier)
0131 {
0132     mResourceIdentifier = identifier;
0133 }
0134 
0135 void Settings::setDefaultPassword(const QString &password)
0136 {
0137     savePassword(mResourceIdentifier, QStringLiteral("$default$"), password);
0138 }
0139 
0140 QString Settings::defaultPassword()
0141 {
0142     return loadPassword(mResourceIdentifier, QStringLiteral("$default$"));
0143 }
0144 
0145 KDAV::DavUrl::List Settings::configuredDavUrls()
0146 {
0147     if (mUrls.isEmpty()) {
0148         buildUrlsList();
0149     }
0150     KDAV::DavUrl::List davUrls;
0151     davUrls.reserve(mUrls.count());
0152     QMap<QString, UrlConfiguration *>::const_iterator it = mUrls.cbegin();
0153     const QMap<QString, UrlConfiguration *>::const_iterator itEnd = mUrls.cend();
0154     for (; it != itEnd; ++it) {
0155         const QStringList split = it.key().split(QLatin1Char(','));
0156         davUrls << configuredDavUrl(KDAV::ProtocolInfo::protocolByName(split.at(1)), split.at(0));
0157     }
0158 
0159     return davUrls;
0160 }
0161 
0162 KDAV::DavUrl Settings::configuredDavUrl(KDAV::Protocol proto, const QString &searchUrl, const QString &finalUrl)
0163 {
0164     if (mUrls.isEmpty()) {
0165         buildUrlsList();
0166     }
0167 
0168     QUrl fullUrl;
0169 
0170     if (!finalUrl.isEmpty()) {
0171         fullUrl = QUrl::fromUserInput(finalUrl);
0172         if (finalUrl.startsWith(QLatin1Char('/'))) {
0173             QUrl searchQUrl(searchUrl);
0174             fullUrl.setHost(searchQUrl.host());
0175             fullUrl.setScheme(searchQUrl.scheme());
0176             fullUrl.setPort(searchQUrl.port());
0177         }
0178     } else {
0179         fullUrl = QUrl::fromUserInput(searchUrl);
0180     }
0181 
0182     const QString user = username(proto, searchUrl);
0183     fullUrl.setUserName(user);
0184     fullUrl.setPassword(password(proto, searchUrl));
0185 
0186     return KDAV::DavUrl(fullUrl, proto);
0187 }
0188 
0189 KDAV::DavUrl Settings::davUrlFromCollectionUrl(const QString &collectionUrl, const QString &finalUrl)
0190 {
0191     if (mCollectionsUrlsMapping.isEmpty()) {
0192         loadMappings();
0193     }
0194 
0195     KDAV::DavUrl davUrl;
0196     const QString targetUrl = finalUrl.isEmpty() ? collectionUrl : finalUrl;
0197 
0198     if (mCollectionsUrlsMapping.contains(collectionUrl)) {
0199         const QStringList split = mCollectionsUrlsMapping.value(collectionUrl).split(QLatin1Char(','));
0200         if (split.size() == 2) {
0201             davUrl = configuredDavUrl(KDAV::ProtocolInfo::protocolByName(split.at(1)), split.at(0), targetUrl);
0202         }
0203     }
0204 
0205     return davUrl;
0206 }
0207 
0208 void Settings::addCollectionUrlMapping(KDAV::Protocol proto, const QString &collectionUrl, const QString &configuredUrl)
0209 {
0210     if (mCollectionsUrlsMapping.isEmpty()) {
0211         loadMappings();
0212     }
0213 
0214     const QString value = configuredUrl + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(proto);
0215     mCollectionsUrlsMapping.insert(collectionUrl, value);
0216 
0217     // Update the cache now
0218     // QMap<QString, QString> tmp( mCollectionsUrlsMapping );
0219     QFileInfo cacheFileInfo = QFileInfo(mCollectionsUrlsMappingCache);
0220     if (!cacheFileInfo.dir().exists()) {
0221         QDir::root().mkpath(cacheFileInfo.dir().absolutePath());
0222     }
0223 
0224     QFile cacheFile(mCollectionsUrlsMappingCache);
0225     if (cacheFile.open(QIODevice::WriteOnly)) {
0226         QDataStream cache(&cacheFile);
0227         cache.setVersion(QDataStream::Qt_4_7);
0228         cache << mCollectionsUrlsMapping;
0229         cacheFile.close();
0230     }
0231 }
0232 
0233 QStringList Settings::mappedCollections(KDAV::Protocol proto, const QString &configuredUrl)
0234 {
0235     if (mCollectionsUrlsMapping.isEmpty()) {
0236         loadMappings();
0237     }
0238 
0239     const QString value = configuredUrl + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(proto);
0240     return mCollectionsUrlsMapping.keys(value);
0241 }
0242 
0243 void Settings::reloadConfig()
0244 {
0245     buildUrlsList();
0246     updateRemoteUrls();
0247     loadMappings();
0248 }
0249 
0250 void Settings::newUrlConfiguration(Settings::UrlConfiguration *urlConfig)
0251 {
0252     const QString key = urlConfig->mUrl + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(KDAV::Protocol(urlConfig->mProtocol));
0253 
0254     if (mUrls.contains(key)) {
0255         removeUrlConfiguration(KDAV::Protocol(urlConfig->mProtocol), urlConfig->mUrl);
0256     }
0257 
0258     mUrls[key] = urlConfig;
0259     if (urlConfig->mUser != QLatin1StringView("$default$")) {
0260         savePassword(key, urlConfig->mUser, urlConfig->mPassword);
0261     }
0262     updateRemoteUrls();
0263 }
0264 
0265 void Settings::removeUrlConfiguration(KDAV::Protocol proto, const QString &url)
0266 {
0267     const QString key = url + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(proto);
0268 
0269     if (!mUrls.contains(key)) {
0270         return;
0271     }
0272 
0273     delete mUrls[key];
0274     mUrls.remove(key);
0275     updateRemoteUrls();
0276 }
0277 
0278 Settings::UrlConfiguration *Settings::urlConfiguration(KDAV::Protocol proto, const QString &url)
0279 {
0280     const QString key = url + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(proto);
0281 
0282     UrlConfiguration *ret = nullptr;
0283     if (mUrls.contains(key)) {
0284         ret = mUrls[key];
0285     }
0286 
0287     return ret;
0288 }
0289 
0290 // KDAV::Protocol Settings::protocol( const QString &url ) const
0291 // {
0292 //   if ( mUrls.contains( url ) )
0293 //     return KDAV::Protocol( mUrls[ url ]->mProtocol );
0294 //   else
0295 //     returnKDAV::CalDav;
0296 // }
0297 
0298 QString Settings::username(KDAV::Protocol proto, const QString &url) const
0299 {
0300     const QString key = url + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(proto);
0301 
0302     if (mUrls.contains(key)) {
0303         if (mUrls[key]->mUser == QLatin1StringView("$default$")) {
0304             return defaultUsername();
0305         } else {
0306             return mUrls[key]->mUser;
0307         }
0308     } else {
0309         return {};
0310     }
0311 }
0312 
0313 QString Settings::password(KDAV::Protocol proto, const QString &url)
0314 {
0315     const QString key = url + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(proto);
0316 
0317     if (mUrls.contains(key)) {
0318         if (mUrls[key]->mUser == QLatin1StringView("$default$")) {
0319             return defaultPassword();
0320         } else {
0321             return mUrls[key]->mPassword;
0322         }
0323     } else {
0324         return {};
0325     }
0326 }
0327 
0328 QDateTime Settings::getSyncRangeStart() const
0329 {
0330     QDateTime start = QDateTime::currentDateTimeUtc();
0331     start.setTime(QTime());
0332     const int delta = -syncRangeStartNumber().toUInt();
0333 
0334     if (syncRangeStartType() == QLatin1Char('D')) {
0335         start = start.addDays(delta);
0336     } else if (syncRangeStartType() == QLatin1Char('M')) {
0337         start = start.addMonths(delta);
0338     } else if (syncRangeStartType() == QLatin1Char('Y')) {
0339         start = start.addYears(delta);
0340     } else {
0341         start = QDateTime();
0342     }
0343 
0344     return start;
0345 }
0346 
0347 void Settings::buildUrlsList()
0348 {
0349     const auto remoteUrlsLst = remoteUrls();
0350     for (const QString &serializedUrl : remoteUrlsLst) {
0351         auto urlConfig = new UrlConfiguration(serializedUrl);
0352         const QString key = urlConfig->mUrl + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(KDAV::Protocol(urlConfig->mProtocol));
0353         const QString pass = loadPassword(key, urlConfig->mUser);
0354         if (!pass.isNull()) {
0355             urlConfig->mPassword = pass;
0356             mUrls[key] = urlConfig;
0357         } else {
0358             delete urlConfig;
0359         }
0360     }
0361 }
0362 
0363 void Settings::loadMappings()
0364 {
0365     const QString collectionsMappingCacheBase = QStringLiteral("akonadi-davgroupware/%1_c2u.dat").arg(QCoreApplication::applicationName());
0366     mCollectionsUrlsMappingCache = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + collectionsMappingCacheBase;
0367     QFile collectionsMappingsCache(mCollectionsUrlsMappingCache);
0368 
0369     if (collectionsMappingsCache.exists()) {
0370         if (collectionsMappingsCache.open(QIODevice::ReadOnly)) {
0371             QDataStream cache(&collectionsMappingsCache);
0372             cache >> mCollectionsUrlsMapping;
0373             collectionsMappingsCache.close();
0374         }
0375     } else if (!collectionsUrlsMappings().isEmpty()) {
0376         QByteArray rawMappings = QByteArray::fromBase64(collectionsUrlsMappings().toLatin1());
0377         QDataStream stream(&rawMappings, QIODevice::ReadOnly);
0378         stream >> mCollectionsUrlsMapping;
0379         setCollectionsUrlsMappings(QString());
0380     }
0381 }
0382 
0383 void Settings::updateRemoteUrls()
0384 {
0385     QStringList newUrls;
0386     newUrls.reserve(mUrls.count());
0387 
0388     QMapIterator<QString, UrlConfiguration *> it(mUrls);
0389     while (it.hasNext()) {
0390         it.next();
0391         newUrls << it.value()->serialize();
0392     }
0393 
0394     setRemoteUrls(newUrls);
0395 }
0396 
0397 void Settings::savePassword(const QString &key, const QString &user, const QString &password)
0398 {
0399     const QString entry = key + QLatin1Char(',') + user;
0400     mPasswordsCache[entry] = password;
0401 
0402     auto writeJob = new WritePasswordJob(QStringLiteral("Passwords"), this);
0403     connect(writeJob, &QKeychain::Job::finished, this, [](QKeychain::Job *baseJob) {
0404         if (baseJob->error()) {
0405             qCWarning(DAVRESOURCE_LOG) << "Error writing password using QKeychain:" << baseJob->errorString();
0406         }
0407     });
0408     writeJob->setKey(entry);
0409     writeJob->setTextData(password);
0410     writeJob->start();
0411 }
0412 
0413 QString Settings::loadPassword(const QString &key, const QString &user)
0414 {
0415     QString entry;
0416     QString pass;
0417 
0418     if (user == QLatin1StringView("$default$")) {
0419         entry = mResourceIdentifier + QLatin1Char(',') + user;
0420     } else {
0421         entry = key + QLatin1Char(',') + user;
0422     }
0423 
0424     if (mPasswordsCache.contains(entry)) {
0425         return mPasswordsCache[entry];
0426     }
0427 
0428     QKeychain::ReadPasswordJob job(QStringLiteral("Passwords"));
0429     job.setAutoDelete(false);
0430     job.setKey(entry);
0431     QEventLoop loop; // Ideally we should have an async API
0432     QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
0433     job.start();
0434     loop.exec();
0435 
0436     if (job.error() == QKeychain::Error::EntryNotFound) {
0437         pass = promptForPassword(user);
0438         if (!pass.isEmpty()) {
0439             if (user == QLatin1StringView("$default$")) {
0440                 savePassword(mResourceIdentifier, user, pass);
0441             } else {
0442                 savePassword(key, user, pass);
0443             }
0444         }
0445     } else if (job.error() != QKeychain::Error::NoError) {
0446         // Other type of errors
0447         pass = promptForPassword(user);
0448     } else {
0449         pass = QString::fromLatin1(job.binaryData());
0450     }
0451 
0452     if (!pass.isNull()) {
0453         mPasswordsCache[entry] = pass;
0454     }
0455 
0456     return pass;
0457 }
0458 
0459 QString Settings::promptForPassword(const QString &user)
0460 {
0461     QPointer<QDialog> dlg = new QDialog();
0462     QString password;
0463 
0464     auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg);
0465     auto mainLayout = new QVBoxLayout(dlg);
0466     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0467     okButton->setDefault(true);
0468     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0469     connect(buttonBox, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept);
0470     connect(buttonBox, &QDialogButtonBox::rejected, dlg.data(), &QDialog::reject);
0471 
0472     auto mainWidget = new QWidget(dlg);
0473     mainLayout->addWidget(mainWidget);
0474     mainLayout->addWidget(buttonBox);
0475     auto vLayout = new QVBoxLayout();
0476     mainWidget->setLayout(vLayout);
0477     auto label = new QLabel(i18n("A password is required for user %1", (user == QLatin1StringView("$default$") ? defaultUsername() : user)), mainWidget);
0478     vLayout->addWidget(label);
0479     auto hLayout = new QHBoxLayout();
0480     label = new QLabel(i18n("Password: "), mainWidget);
0481     hLayout->addWidget(label);
0482     auto lineEdit = new KPasswordLineEdit();
0483     lineEdit->setRevealPasswordAvailable(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")));
0484     hLayout->addWidget(lineEdit);
0485     vLayout->addLayout(hLayout);
0486     lineEdit->setFocus();
0487 
0488     const int result = dlg->exec();
0489 
0490     if (result == QDialog::Accepted && !dlg.isNull()) {
0491         password = lineEdit->password();
0492     }
0493 
0494     delete dlg;
0495     return password;
0496 }
0497 
0498 void Settings::updateToV2()
0499 {
0500     // Take the first URL that was configured to get the username that
0501     // has the most chances being the default
0502 
0503     QStringList urls = remoteUrls();
0504     if (urls.isEmpty()) {
0505         return;
0506     }
0507 
0508     const QString urlConfigStr = urls.at(0);
0509     UrlConfiguration urlConfig(urlConfigStr);
0510     const QRegularExpression regexp(QStringLiteral("^") + urlConfig.mUser);
0511 
0512     QMutableStringListIterator it(urls);
0513     while (it.hasNext()) {
0514         it.next();
0515         it.value().replace(regexp, QStringLiteral("$default$"));
0516     }
0517 
0518     setDefaultUsername(urlConfig.mUser);
0519     QString key = urlConfig.mUrl + QLatin1Char(',') + KDAV::ProtocolInfo::protocolName(KDAV::Protocol(urlConfig.mProtocol));
0520     QString pass = loadPassword(key, urlConfig.mUser);
0521     if (!pass.isNull()) {
0522         setDefaultPassword(pass);
0523     }
0524     setRemoteUrls(urls);
0525     setSettingsVersion(2);
0526     save();
0527 }
0528 
0529 void Settings::updateToV3()
0530 {
0531     QStringList updatedUrls;
0532 
0533     const auto remoteUrlsLst = remoteUrls();
0534     for (const QString &url : remoteUrlsLst) {
0535         QStringList splitUrl = url.split(QLatin1Char('|'));
0536 
0537         if (splitUrl.size() == 3) {
0538             const KDAV::Protocol protocol = Utils::protocolByTranslatedName(splitUrl.at(1));
0539             splitUrl[1] = KDAV::ProtocolInfo::protocolName(protocol);
0540             updatedUrls << splitUrl.join(QLatin1Char('|'));
0541         }
0542     }
0543 
0544     setRemoteUrls(updatedUrls);
0545     setSettingsVersion(3);
0546     save();
0547 }
0548 
0549 #include "moc_settings.cpp"