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"