File indexing completed on 2024-04-21 03:55:40

0001 /*
0002     This file is part of the KDE Password Server
0003     SPDX-FileCopyrightText: 2002 Waldo Bastian <bastian@kde.org>
0004     SPDX-FileCopyrightText: 2005 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2012 Dawit Alemayehu <adawit@kde.org>
0006     SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-only
0009 */
0010 
0011 // KDE Password Server
0012 
0013 #include "kpasswdserver.h"
0014 
0015 #include "kpasswdserveradaptor.h"
0016 
0017 #include <KLocalizedString>
0018 #include <KMessageDialog>
0019 #include <KPasswordDialog>
0020 #include <kwindowsystem.h>
0021 
0022 #ifdef HAVE_KF6WALLET
0023 #include <KWallet>
0024 #endif
0025 
0026 #include <QPushButton>
0027 #include <QTimer>
0028 #include <ctime>
0029 
0030 #include "../gui/config-kiogui.h"
0031 
0032 #if HAVE_X11
0033 #include <KUserTimestamp>
0034 #include <KX11Extras>
0035 #endif
0036 
0037 Q_LOGGING_CATEGORY(category, "kf.kio.kpasswdserver", QtInfoMsg)
0038 
0039 static const char s_domain[] = "domain";
0040 static const char s_anonymous[] = "anonymous";
0041 static const char s_bypassCacheAndKwallet[] = "bypass-cache-and-kwallet";
0042 static const char s_skipCachingOnQuery[] = "skip-caching-on-query";
0043 static const char s_hideUsernameInput[] = "hide-username-line";
0044 static const char s_usernameContextHelp[] = "username-context-help";
0045 
0046 static qlonglong getRequestId()
0047 {
0048     static qlonglong nextRequestId = 0;
0049     return nextRequestId++;
0050 }
0051 
0052 bool KPasswdServer::AuthInfoContainer::Sorter::operator()(const AuthInfoContainer &n1, const AuthInfoContainer &n2) const
0053 {
0054     const int l1 = n1.directory.length();
0055     const int l2 = n2.directory.length();
0056     return l1 < l2;
0057 }
0058 
0059 KPasswdServer::KPasswdServer(QObject *parent, const QList<QVariant> &)
0060     : KDEDModule(parent)
0061 {
0062     KIO::AuthInfo::registerMetaTypes();
0063 
0064     m_seqNr = 0;
0065     m_wallet = nullptr;
0066     m_walletDisabled = false;
0067 
0068     KPasswdServerAdaptor *adaptor = new KPasswdServerAdaptor(this);
0069     // connect signals to the adaptor
0070     connect(this, &KPasswdServer::checkAuthInfoAsyncResult, adaptor, &KPasswdServerAdaptor::checkAuthInfoAsyncResult);
0071     connect(this, &KPasswdServer::queryAuthInfoAsyncResult, adaptor, &KPasswdServerAdaptor::queryAuthInfoAsyncResult);
0072 
0073     connect(this, &KDEDModule::windowUnregistered, this, &KPasswdServer::removeAuthForWindowId);
0074 
0075 #if HAVE_X11
0076     connect(KX11Extras::self(), &KX11Extras::windowRemoved, this, &KPasswdServer::windowRemoved);
0077 #endif
0078 }
0079 
0080 KPasswdServer::~KPasswdServer()
0081 {
0082     // TODO: what about clients waiting for requests? will they just
0083     //       notice kpasswdserver is gone from the dbus?
0084     qDeleteAll(m_authPending);
0085     qDeleteAll(m_authWait);
0086     qDeleteAll(m_authDict);
0087     qDeleteAll(m_authInProgress);
0088     qDeleteAll(m_authRetryInProgress);
0089 
0090 #ifdef HAVE_KF6WALLET
0091     delete m_wallet;
0092 #endif
0093 }
0094 
0095 #ifdef HAVE_KF6WALLET
0096 
0097 // Helper - returns the wallet key to use for read/store/checking for existence.
0098 static QString makeWalletKey(const QString &key, const QString &realm)
0099 {
0100     return realm.isEmpty() ? key : key + QLatin1Char('-') + realm;
0101 }
0102 
0103 // Helper for storeInWallet/readFromWallet
0104 static QString makeMapKey(const char *key, int entryNumber)
0105 {
0106     QString str = QLatin1String(key);
0107     if (entryNumber > 1) {
0108         str += QLatin1Char('-') + QString::number(entryNumber);
0109     }
0110     return str;
0111 }
0112 
0113 static bool storeInWallet(KWallet::Wallet *wallet, const QString &key, const KIO::AuthInfo &info)
0114 {
0115     if (!wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
0116         if (!wallet->createFolder(KWallet::Wallet::PasswordFolder())) {
0117             return false;
0118         }
0119     }
0120     wallet->setFolder(KWallet::Wallet::PasswordFolder());
0121     // Before saving, check if there's already an entry with this login.
0122     // If so, replace it (with the new password). Otherwise, add a new entry.
0123     typedef QMap<QString, QString> Map;
0124     int entryNumber = 1;
0125     Map map;
0126     QString walletKey = makeWalletKey(key, info.realmValue);
0127     qCDebug(category) << "walletKey =" << walletKey << "  reading existing map";
0128     if (wallet->readMap(walletKey, map) == 0) {
0129         Map::ConstIterator end = map.constEnd();
0130         Map::ConstIterator it = map.constFind(QStringLiteral("login"));
0131         while (it != end) {
0132             if (it.value() == info.username) {
0133                 break; // OK, overwrite this entry
0134             }
0135             it = map.constFind(QStringLiteral("login-") + QString::number(++entryNumber));
0136         }
0137         // If no entry was found, create a new entry - entryNumber is set already.
0138     }
0139     const QString loginKey = makeMapKey("login", entryNumber);
0140     const QString passwordKey = makeMapKey("password", entryNumber);
0141     qCDebug(category) << "writing to " << loginKey << "," << passwordKey;
0142     // note the overwrite=true by default
0143     map.insert(loginKey, info.username);
0144     map.insert(passwordKey, info.password);
0145     wallet->writeMap(walletKey, map);
0146     return true;
0147 }
0148 
0149 static bool readFromWallet(KWallet::Wallet *wallet,
0150                            const QString &key,
0151                            const QString &realm,
0152                            QString &username,
0153                            QString &password,
0154                            bool userReadOnly,
0155                            QMap<QString, QString> &knownLogins)
0156 {
0157     // qCDebug(category) << "key =" << key << " username =" << username << " password =" /*<< password*/
0158     //                   << " userReadOnly =" << userReadOnly << " realm =" << realm;
0159     if (wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
0160         wallet->setFolder(KWallet::Wallet::PasswordFolder());
0161 
0162         QMap<QString, QString> map;
0163         if (wallet->readMap(makeWalletKey(key, realm), map) == 0) {
0164             typedef QMap<QString, QString> Map;
0165             int entryNumber = 1;
0166             Map::ConstIterator end = map.constEnd();
0167             Map::ConstIterator it = map.constFind(QStringLiteral("login"));
0168             while (it != end) {
0169                 // qCDebug(category) << "found " << it.key() << "=" << it.value();
0170                 Map::ConstIterator pwdIter = map.constFind(makeMapKey("password", entryNumber));
0171                 if (pwdIter != end) {
0172                     if (it.value() == username) {
0173                         password = pwdIter.value();
0174                     }
0175                     knownLogins.insert(it.value(), pwdIter.value());
0176                 }
0177 
0178                 it = map.constFind(QStringLiteral("login-") + QString::number(++entryNumber));
0179             }
0180             // qCDebug(category) << knownLogins.count() << " known logins";
0181 
0182             if (!userReadOnly && !knownLogins.isEmpty() && username.isEmpty()) {
0183                 // Pick one, any one...
0184                 username = knownLogins.begin().key();
0185                 password = knownLogins.begin().value();
0186                 // qCDebug(category) << "picked the first one:" << username;
0187             }
0188 
0189             return true;
0190         }
0191     }
0192     return false;
0193 }
0194 
0195 #endif
0196 
0197 bool KPasswdServer::hasPendingQuery(const QString &key, const KIO::AuthInfo &info)
0198 {
0199     const QString path2(info.url.path().left(info.url.path().indexOf(QLatin1Char('/')) + 1));
0200     for (const Request *request : std::as_const(m_authPending)) {
0201         if (request->key != key) {
0202             continue;
0203         }
0204 
0205         if (info.verifyPath) {
0206             const QString path1(request->info.url.path().left(info.url.path().indexOf(QLatin1Char('/')) + 1));
0207             if (!path2.startsWith(path1)) {
0208                 continue;
0209             }
0210         }
0211 
0212         return true;
0213     }
0214 
0215     return false;
0216 }
0217 
0218 // deprecated method, not used anymore. TODO KF6: REMOVE
0219 QByteArray KPasswdServer::checkAuthInfo(const QByteArray &data, qlonglong windowId, qlonglong usertime)
0220 {
0221     KIO::AuthInfo info;
0222     QDataStream stream(data);
0223     stream >> info;
0224 #if HAVE_X11
0225     if (usertime != 0) {
0226         KUserTimestamp::updateUserTimestamp(usertime);
0227     }
0228 #endif
0229 
0230     // if the check depends on a pending query, delay it
0231     // until that query is finished.
0232     const QString key(createCacheKey(info));
0233     if (hasPendingQuery(key, info)) {
0234         setDelayedReply(true);
0235         Request *pendingCheck = new Request;
0236         pendingCheck->isAsync = false;
0237         if (calledFromDBus()) {
0238             pendingCheck->transaction = message();
0239         }
0240         pendingCheck->key = key;
0241         pendingCheck->info = info;
0242         m_authWait.append(pendingCheck);
0243         return data; // return value will be ignored
0244     }
0245 
0246     // qCDebug(category) << "key =" << key << "user =" << info.username << "windowId =" << windowId;
0247     const AuthInfoContainer *result = findAuthInfoItem(key, info);
0248     if (!result || result->isCanceled) {
0249 #ifdef HAVE_KF6WALLET
0250         if (!result && !m_walletDisabled && (info.username.isEmpty() || info.password.isEmpty())
0251             && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), makeWalletKey(key, info.realmValue))) {
0252             QMap<QString, QString> knownLogins;
0253             if (openWallet(windowId)) {
0254                 if (readFromWallet(m_wallet, key, info.realmValue, info.username, info.password, info.readOnly, knownLogins)) {
0255                     info.setModified(true);
0256                     // fall through
0257                 }
0258             }
0259         } else {
0260             info.setModified(false);
0261         }
0262 #else
0263         info.setModified(false);
0264 #endif
0265     } else {
0266         qCDebug(category) << "Found cached authentication for" << key;
0267         updateAuthExpire(key, result, windowId, false);
0268         copyAuthInfo(result, info);
0269     }
0270 
0271     QByteArray data2;
0272     QDataStream stream2(&data2, QIODevice::WriteOnly);
0273     stream2 << info;
0274     return data2;
0275 }
0276 
0277 qlonglong KPasswdServer::checkAuthInfoAsync(KIO::AuthInfo info, qlonglong windowId, qlonglong usertime)
0278 {
0279 #if HAVE_X11
0280     if (usertime != 0) {
0281         KUserTimestamp::updateUserTimestamp(usertime);
0282     }
0283 #endif
0284 
0285     // send the request id back to the client
0286     qlonglong requestId = getRequestId();
0287     qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId;
0288     if (calledFromDBus()) {
0289         QDBusMessage reply(message().createReply(requestId));
0290         QDBusConnection::sessionBus().send(reply);
0291     }
0292 
0293     // if the check depends on a pending query, delay it
0294     // until that query is finished.
0295     const QString key(createCacheKey(info));
0296     if (hasPendingQuery(key, info)) {
0297         Request *pendingCheck = new Request;
0298         pendingCheck->isAsync = true;
0299         pendingCheck->requestId = requestId;
0300         pendingCheck->key = key;
0301         pendingCheck->info = info;
0302         m_authWait.append(pendingCheck);
0303         return 0; // ignored as we already sent a reply
0304     }
0305 
0306     const AuthInfoContainer *result = findAuthInfoItem(key, info);
0307     if (!result || result->isCanceled) {
0308 #ifdef HAVE_KF6WALLET
0309         if (!result && !m_walletDisabled && (info.username.isEmpty() || info.password.isEmpty())
0310             && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), makeWalletKey(key, info.realmValue))) {
0311             QMap<QString, QString> knownLogins;
0312             if (openWallet(windowId)) {
0313                 if (readFromWallet(m_wallet, key, info.realmValue, info.username, info.password, info.readOnly, knownLogins)) {
0314                     info.setModified(true);
0315                     // fall through
0316                 }
0317             }
0318         } else {
0319             info.setModified(false);
0320         }
0321 #else
0322         info.setModified(false);
0323 #endif
0324     } else {
0325         // qCDebug(category) << "Found cached authentication for" << key;
0326         updateAuthExpire(key, result, windowId, false);
0327         copyAuthInfo(result, info);
0328     }
0329 
0330     Q_EMIT checkAuthInfoAsyncResult(requestId, m_seqNr, info);
0331     return 0; // ignored
0332 }
0333 
0334 // deprecated method, not used anymore. TODO KF6: REMOVE
0335 QByteArray KPasswdServer::queryAuthInfo(const QByteArray &data, const QString &errorMsg, qlonglong windowId, qlonglong seqNr, qlonglong usertime)
0336 {
0337     KIO::AuthInfo info;
0338     QDataStream stream(data);
0339     stream >> info;
0340 
0341     qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId << "seqNr =" << seqNr << ", errorMsg =" << errorMsg;
0342 
0343     if (!info.password.isEmpty()) { // should we really allow the caller to pre-fill the password?
0344         qCDebug(category) << "password was set by caller";
0345     }
0346 #if HAVE_X11
0347     if (usertime != 0) {
0348         KUserTimestamp::updateUserTimestamp(usertime);
0349     }
0350 #endif
0351 
0352     const QString key(createCacheKey(info));
0353     Request *request = new Request;
0354     setDelayedReply(true);
0355     request->isAsync = false;
0356     request->transaction = message();
0357     request->key = key;
0358     request->info = info;
0359     request->windowId = windowId;
0360     request->seqNr = seqNr;
0361     if (errorMsg == QLatin1String("<NoAuthPrompt>")) {
0362         request->errorMsg.clear();
0363         request->prompt = false;
0364     } else {
0365         request->errorMsg = errorMsg;
0366         request->prompt = true;
0367     }
0368     m_authPending.append(request);
0369 
0370     if (m_authPending.count() == 1) {
0371         QTimer::singleShot(0, this, &KPasswdServer::processRequest);
0372     }
0373 
0374     return QByteArray(); // return value is going to be ignored
0375 }
0376 
0377 qlonglong KPasswdServer::queryAuthInfoAsync(const KIO::AuthInfo &info, const QString &errorMsg, qlonglong windowId, qlonglong seqNr, qlonglong usertime)
0378 {
0379     qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId << "seqNr =" << seqNr << ", errorMsg =" << errorMsg;
0380 
0381     if (!info.password.isEmpty()) {
0382         qCDebug(category) << "password was set by caller";
0383     }
0384 #if HAVE_X11
0385     if (usertime != 0) {
0386         KUserTimestamp::updateUserTimestamp(usertime);
0387     }
0388 #endif
0389 
0390     const QString key(createCacheKey(info));
0391     Request *request = new Request;
0392     request->isAsync = true;
0393     request->requestId = getRequestId();
0394     request->key = key;
0395     request->info = info;
0396     request->windowId = windowId;
0397     request->seqNr = seqNr;
0398     if (errorMsg == QLatin1String("<NoAuthPrompt>")) {
0399         request->errorMsg.clear();
0400         request->prompt = false;
0401     } else {
0402         request->errorMsg = errorMsg;
0403         request->prompt = true;
0404     }
0405     m_authPending.append(request);
0406 
0407     if (m_authPending.count() == 1) {
0408         QTimer::singleShot(0, this, &KPasswdServer::processRequest);
0409     }
0410 
0411     return request->requestId;
0412 }
0413 
0414 void KPasswdServer::addAuthInfo(const KIO::AuthInfo &info, qlonglong windowId)
0415 {
0416     qCDebug(category) << "User =" << info.username << ", Realm =" << info.realmValue << ", WindowId =" << windowId;
0417     if (!info.keepPassword) {
0418         qWarning() << "This KIO worker is caching a password in KWallet even though the user didn't ask for it!";
0419     }
0420     const QString key(createCacheKey(info));
0421 
0422     m_seqNr++;
0423 
0424 #ifdef HAVE_KF6WALLET
0425     if (!m_walletDisabled && openWallet(windowId) && storeInWallet(m_wallet, key, info)) {
0426         // Since storing the password in the wallet succeeded, make sure the
0427         // password information is stored in memory only for the duration the
0428         // windows associated with it are still around.
0429         KIO::AuthInfo authToken(info);
0430         authToken.keepPassword = false;
0431         addAuthInfoItem(key, authToken, windowId, m_seqNr, false);
0432         return;
0433     }
0434 #endif
0435 
0436     addAuthInfoItem(key, info, windowId, m_seqNr, false);
0437 }
0438 
0439 // deprecated method, not used anymore. TODO KF6: REMOVE
0440 void KPasswdServer::addAuthInfo(const QByteArray &data, qlonglong windowId)
0441 {
0442     KIO::AuthInfo info;
0443     QDataStream stream(data);
0444     stream >> info;
0445     addAuthInfo(info, windowId);
0446 }
0447 
0448 void KPasswdServer::removeAuthInfo(const QString &host, const QString &protocol, const QString &user)
0449 {
0450     qCDebug(category) << protocol << host << user;
0451 
0452     QHashIterator<QString, AuthInfoContainerList *> dictIterator(m_authDict);
0453     while (dictIterator.hasNext()) {
0454         dictIterator.next();
0455 
0456         const AuthInfoContainerList *authList = dictIterator.value();
0457         if (!authList) {
0458             continue;
0459         }
0460 
0461         for (const AuthInfoContainer &current : *authList) {
0462             qCDebug(category) << "Evaluating: " << current.info.url.scheme() << current.info.url.host() << current.info.username;
0463             if (current.info.url.scheme() == protocol && current.info.url.host() == host && (current.info.username == user || user.isEmpty())) {
0464                 qCDebug(category) << "Removing this entry";
0465                 removeAuthInfoItem(dictIterator.key(), current.info); // warning, this can modify m_authDict!
0466             }
0467         }
0468     }
0469 }
0470 
0471 #ifdef HAVE_KF6WALLET
0472 bool KPasswdServer::openWallet(qlonglong windowId)
0473 {
0474     if (m_wallet && !m_wallet->isOpen()) { // forced closed
0475         delete m_wallet;
0476         m_wallet = nullptr;
0477     }
0478     if (!m_wallet) {
0479         m_wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), static_cast<WId>(windowId));
0480     }
0481     return m_wallet != nullptr;
0482 }
0483 #endif
0484 
0485 void KPasswdServer::processRequest()
0486 {
0487     if (m_authPending.isEmpty()) {
0488         return;
0489     }
0490 
0491     std::unique_ptr<Request> request(m_authPending.takeFirst());
0492 
0493     // Prevent multiple prompts originating from the same window or the same
0494     // key (server address).
0495     const QString windowIdStr = QString::number(request->windowId);
0496     if (m_authPrompted.contains(windowIdStr) || m_authPrompted.contains(request->key)) {
0497         m_authPending.prepend(request.release()); // put it back.
0498         return;
0499     }
0500 
0501     m_authPrompted.append(windowIdStr);
0502     m_authPrompted.append(request->key);
0503 
0504     KIO::AuthInfo &info = request->info;
0505 
0506     // NOTE: If info.username is empty and info.url.userName() is not, set
0507     // info.username to info.url.userName() to ensure proper caching. See
0508     // note passwordDialogDone.
0509     if (info.username.isEmpty() && !info.url.userName().isEmpty()) {
0510         info.username = info.url.userName();
0511     }
0512     const bool bypassCacheAndKWallet = info.getExtraField(QString::fromLatin1(s_bypassCacheAndKwallet)).toBool();
0513 
0514     const AuthInfoContainer *result = findAuthInfoItem(request->key, request->info);
0515     qCDebug(category) << "key=" << request->key << ", user=" << info.username << "seqNr: request=" << request->seqNr
0516                       << ", result=" << (result ? result->seqNr : -1);
0517 
0518     if (!bypassCacheAndKWallet && result && (request->seqNr < result->seqNr)) {
0519         qCDebug(category) << "auto retry!";
0520         if (result->isCanceled) {
0521             info.setModified(false);
0522         } else {
0523             updateAuthExpire(request->key, result, request->windowId, false);
0524             copyAuthInfo(result, info);
0525         }
0526     } else {
0527         m_seqNr++;
0528         if (result && !request->errorMsg.isEmpty()) {
0529             const QString prompt = request->errorMsg.trimmed() + QLatin1Char('\n') + i18n("Do you want to retry?");
0530 
0531             KMessageDialog *dlg = new KMessageDialog(KMessageDialog::WarningContinueCancel, prompt, nullptr);
0532             dlg->setAttribute(Qt::WA_DeleteOnClose);
0533             dlg->setWindowTitle(i18n("Retry Authentication"));
0534             dlg->setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-password")));
0535             dlg->setObjectName(QStringLiteral("warningOKCancel"));
0536             KGuiItem retryButton(i18nc("@action:button filter-continue", "Retry"));
0537 
0538             dlg->setButtons(retryButton);
0539 
0540             connect(dlg, &QDialog::finished, this, [this, dlg](int result) {
0541                 retryDialogDone(result, dlg);
0542             });
0543 
0544             dlg->setAttribute(Qt::WA_NativeWindow, true);
0545             KWindowSystem::setMainWindow(dlg->windowHandle(), request->windowId);
0546 
0547             qCDebug(category) << "Calling open on retry dialog" << dlg;
0548             m_authRetryInProgress.insert(dlg, request.release());
0549             dlg->open();
0550             return;
0551         }
0552 
0553         if (request->prompt) {
0554             showPasswordDialog(request.release());
0555             return;
0556         } else {
0557             if (!bypassCacheAndKWallet && request->prompt) {
0558                 addAuthInfoItem(request->key, info, 0, m_seqNr, true);
0559             }
0560             info.setModified(false);
0561         }
0562     }
0563 
0564     sendResponse(request.get());
0565 }
0566 
0567 QString KPasswdServer::createCacheKey(const KIO::AuthInfo &info)
0568 {
0569     if (!info.url.isValid()) {
0570         // Note that a null key will break findAuthInfoItem later on...
0571         qCWarning(category) << "createCacheKey: invalid URL " << info.url;
0572         return QString();
0573     }
0574 
0575     // Generate the basic key sequence.
0576     QString key = info.url.scheme();
0577     key += QLatin1Char('-');
0578     if (!info.url.userName().isEmpty()) {
0579         key += info.url.userName() + QLatin1Char('@');
0580     }
0581     key += info.url.host();
0582     int port = info.url.port();
0583     if (port) {
0584         key += QLatin1Char(':') + QString::number(port);
0585     }
0586 
0587     return key;
0588 }
0589 
0590 void KPasswdServer::copyAuthInfo(const AuthInfoContainer *i, KIO::AuthInfo &info)
0591 {
0592     info = i->info;
0593     info.setModified(true);
0594 }
0595 
0596 const KPasswdServer::AuthInfoContainer *KPasswdServer::findAuthInfoItem(const QString &key, const KIO::AuthInfo &info)
0597 {
0598     // qCDebug(category) << "key=" << key << ", user=" << info.username;
0599 
0600     AuthInfoContainerList *authList = m_authDict.value(key);
0601     if (authList) {
0602         QString path2 = info.url.path().left(info.url.path().indexOf(QLatin1Char('/')) + 1);
0603         auto it = authList->begin();
0604         while (it != authList->end()) {
0605             const AuthInfoContainer &current = (*it);
0606             if (current.expire == AuthInfoContainer::expTime && static_cast<qulonglong>(time(nullptr)) > current.expireTime) {
0607                 it = authList->erase(it);
0608                 continue;
0609             }
0610 
0611             if (info.verifyPath) {
0612                 const QString path1 = current.directory;
0613                 if (path2.startsWith(path1) && (info.username.isEmpty() || info.username == current.info.username)) {
0614                     return &current;
0615                 }
0616             } else {
0617                 if (current.info.realmValue == info.realmValue && (info.username.isEmpty() || info.username == current.info.username)) {
0618                     return &current; // TODO: Update directory info
0619                 }
0620             }
0621 
0622             ++it;
0623         }
0624     }
0625     return nullptr;
0626 }
0627 
0628 void KPasswdServer::removeAuthInfoItem(const QString &key, const KIO::AuthInfo &info)
0629 {
0630     AuthInfoContainerList *authList = m_authDict.value(key);
0631     if (!authList) {
0632         return;
0633     }
0634 
0635     auto it = authList->begin();
0636     while (it != authList->end()) {
0637         if ((*it).info.realmValue == info.realmValue) {
0638             it = authList->erase(it);
0639         } else {
0640             ++it;
0641         }
0642     }
0643     if (authList->isEmpty()) {
0644         delete m_authDict.take(key);
0645     }
0646 }
0647 
0648 void KPasswdServer::addAuthInfoItem(const QString &key, const KIO::AuthInfo &info, qlonglong windowId, qlonglong seqNr, bool canceled)
0649 {
0650     qCDebug(category) << "key=" << key << "window-id=" << windowId << "username=" << info.username << "realm=" << info.realmValue << "seqNr=" << seqNr
0651                       << "keepPassword?" << info.keepPassword << "canceled?" << canceled;
0652     AuthInfoContainerList *authList = m_authDict.value(key);
0653     if (!authList) {
0654         authList = new AuthInfoContainerList;
0655         m_authDict.insert(key, authList);
0656     }
0657     bool found = false;
0658     AuthInfoContainer authItem;
0659     auto it = authList->begin();
0660     while (it != authList->end()) {
0661         if ((*it).info.realmValue == info.realmValue) {
0662             authItem = (*it);
0663             it = authList->erase(it);
0664             found = true;
0665             break;
0666         } else {
0667             ++it;
0668         }
0669     }
0670 
0671     if (!found) {
0672         qCDebug(category) << "Creating AuthInfoContainer";
0673         authItem.expire = AuthInfoContainer::expTime;
0674     }
0675 
0676     authItem.info = info;
0677     authItem.directory = info.url.path().left(info.url.path().indexOf(QLatin1Char('/')) + 1);
0678     authItem.seqNr = seqNr;
0679     authItem.isCanceled = canceled;
0680 
0681     updateAuthExpire(key, &authItem, windowId, (info.keepPassword && !canceled));
0682 
0683     // Insert into list, keep the list sorted "longest path" first.
0684     authList->append(authItem);
0685     std::sort(authList->begin(), authList->end(), AuthInfoContainer::Sorter());
0686 }
0687 
0688 void KPasswdServer::updateAuthExpire(const QString &key, const AuthInfoContainer *auth, qlonglong windowId, bool keep)
0689 {
0690     AuthInfoContainer *current = const_cast<AuthInfoContainer *>(auth);
0691     Q_ASSERT(current);
0692 
0693     qCDebug(category) << "key=" << key << "expire=" << current->expire << "window-id=" << windowId << "keep=" << keep;
0694 
0695     if (keep && !windowId) {
0696         current->expire = AuthInfoContainer::expNever;
0697     } else if (windowId && (current->expire != AuthInfoContainer::expNever)) {
0698         current->expire = AuthInfoContainer::expWindowClose;
0699         if (!current->windowList.contains(windowId)) {
0700             current->windowList.append(windowId);
0701         }
0702     } else if (current->expire == AuthInfoContainer::expTime) {
0703         current->expireTime = time(nullptr) + 10;
0704     }
0705 
0706     // Update mWindowIdList
0707     if (windowId) {
0708         QStringList &keysChanged = mWindowIdList[windowId]; // find or insert
0709         if (!keysChanged.contains(key)) {
0710             keysChanged.append(key);
0711         }
0712     }
0713 }
0714 
0715 void KPasswdServer::removeAuthForWindowId(qlonglong windowId)
0716 {
0717     const QStringList keysChanged = mWindowIdList.value(windowId);
0718     for (const QString &key : keysChanged) {
0719         AuthInfoContainerList *authList = m_authDict.value(key);
0720         if (!authList) {
0721             continue;
0722         }
0723 
0724         QMutableListIterator<AuthInfoContainer> it(*authList);
0725         while (it.hasNext()) {
0726             AuthInfoContainer &current = it.next();
0727             if (current.expire == AuthInfoContainer::expWindowClose) {
0728                 if (current.windowList.removeAll(windowId) && current.windowList.isEmpty()) {
0729                     it.remove();
0730                 }
0731             }
0732         }
0733     }
0734 }
0735 
0736 void KPasswdServer::showPasswordDialog(KPasswdServer::Request *request)
0737 {
0738     KIO::AuthInfo &info = request->info;
0739     QString username = info.username;
0740     QString password = info.password;
0741     bool hasWalletData = false;
0742     QMap<QString, QString> knownLogins;
0743 
0744 #ifdef HAVE_KF6WALLET
0745     const bool bypassCacheAndKWallet = info.getExtraField(QString::fromLatin1(s_bypassCacheAndKwallet)).toBool();
0746     if (!bypassCacheAndKWallet && (username.isEmpty() || password.isEmpty()) && !m_walletDisabled
0747         && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(),
0748                                              KWallet::Wallet::PasswordFolder(),
0749                                              makeWalletKey(request->key, info.realmValue))) {
0750         // no login+pass provided, check if kwallet has one
0751         if (openWallet(request->windowId)) {
0752             hasWalletData = readFromWallet(m_wallet, request->key, info.realmValue, username, password, info.readOnly, knownLogins);
0753         }
0754     }
0755 #endif
0756 
0757     // assemble dialog-flags
0758     KPasswordDialog::KPasswordDialogFlags dialogFlags;
0759 
0760     if (info.getExtraField(QString::fromLatin1(s_domain)).isValid()) {
0761         dialogFlags |= KPasswordDialog::ShowDomainLine;
0762         if (info.getExtraFieldFlags(QString::fromLatin1(s_domain)) & KIO::AuthInfo::ExtraFieldReadOnly) {
0763             dialogFlags |= KPasswordDialog::DomainReadOnly;
0764         }
0765     }
0766 
0767     if (info.getExtraField(QString::fromLatin1(s_anonymous)).isValid()) {
0768         dialogFlags |= KPasswordDialog::ShowAnonymousLoginCheckBox;
0769     }
0770 
0771     if (!info.getExtraField(QString::fromLatin1(s_hideUsernameInput)).toBool()) {
0772         dialogFlags |= KPasswordDialog::ShowUsernameLine;
0773     }
0774 
0775 #ifdef HAVE_KF6WALLET
0776     // If wallet is not enabled and the caller explicitly requested for it,
0777     // do not show the keep password checkbox.
0778     if (info.keepPassword && KWallet::Wallet::isEnabled()) {
0779         dialogFlags |= KPasswordDialog::ShowKeepPassword;
0780     }
0781 #endif
0782 
0783     // instantiate dialog
0784     qCDebug(category) << "Widget for" << request->windowId << QWidget::find(request->windowId);
0785 
0786     KPasswordDialog *dlg = new KPasswordDialog(nullptr, dialogFlags);
0787     dlg->setAttribute(Qt::WA_DeleteOnClose);
0788 
0789     connect(dlg, &QDialog::finished, this, [this, dlg](int result) {
0790         passwordDialogDone(result, dlg);
0791     });
0792 
0793     dlg->setPrompt(info.prompt);
0794     dlg->setUsername(username);
0795     if (info.caption.isEmpty()) {
0796         dlg->setWindowTitle(i18n("Authentication Dialog"));
0797     } else {
0798         dlg->setWindowTitle(info.caption);
0799     }
0800 
0801     if (!info.comment.isEmpty()) {
0802         dlg->addCommentLine(info.commentLabel, info.comment);
0803     }
0804 
0805     if (!password.isEmpty()) {
0806         dlg->setPassword(password);
0807     }
0808 
0809     if (info.readOnly) {
0810         dlg->setUsernameReadOnly(true);
0811     } else {
0812         dlg->setKnownLogins(knownLogins);
0813     }
0814 
0815     if (hasWalletData) {
0816         dlg->setKeepPassword(true);
0817     }
0818 
0819     if (info.getExtraField(QString::fromLatin1(s_domain)).isValid()) {
0820         dlg->setDomain(info.getExtraField(QString::fromLatin1(s_domain)).toString());
0821     }
0822 
0823     if (info.getExtraField(QString::fromLatin1(s_anonymous)).isValid() && password.isEmpty() && username.isEmpty()) {
0824         dlg->setAnonymousMode(info.getExtraField(QString::fromLatin1(s_anonymous)).toBool());
0825     }
0826 
0827     const QVariant userContextHelp = info.getExtraField(QString::fromLatin1(s_usernameContextHelp));
0828     if (userContextHelp.isValid()) {
0829         dlg->setUsernameContextHelp(userContextHelp.toString());
0830     }
0831 
0832 #ifndef Q_OS_MACOS
0833     dlg->setAttribute(Qt::WA_NativeWindow, true);
0834     KWindowSystem::setMainWindow(dlg->windowHandle(), request->windowId);
0835 #endif
0836 
0837     qCDebug(category) << "Showing password dialog" << dlg << ", window-id=" << request->windowId;
0838     m_authInProgress.insert(dlg, request);
0839     dlg->open();
0840 }
0841 
0842 void KPasswdServer::sendResponse(KPasswdServer::Request *request)
0843 {
0844     Q_ASSERT(request);
0845     if (!request) {
0846         return;
0847     }
0848 
0849     qCDebug(category) << "key=" << request->key;
0850     if (request->isAsync) {
0851         Q_EMIT queryAuthInfoAsyncResult(request->requestId, m_seqNr, request->info);
0852     } else {
0853         QByteArray replyData;
0854         QDataStream stream2(&replyData, QIODevice::WriteOnly);
0855         stream2 << request->info;
0856         QDBusConnection::sessionBus().send(request->transaction.createReply(QVariantList{QVariant(replyData), QVariant(m_seqNr)}));
0857     }
0858 
0859     // Check all requests in the wait queue.
0860     Request *waitRequest;
0861     QMutableListIterator<Request *> it(m_authWait);
0862     while (it.hasNext()) {
0863         waitRequest = it.next();
0864 
0865         if (!hasPendingQuery(waitRequest->key, waitRequest->info)) {
0866             const AuthInfoContainer *result = findAuthInfoItem(waitRequest->key, waitRequest->info);
0867             QByteArray replyData;
0868 
0869             QDataStream stream2(&replyData, QIODevice::WriteOnly);
0870 
0871             KIO::AuthInfo rcinfo;
0872             if (!result || result->isCanceled) {
0873                 waitRequest->info.setModified(false);
0874                 stream2 << waitRequest->info;
0875             } else {
0876                 updateAuthExpire(waitRequest->key, result, waitRequest->windowId, false);
0877                 copyAuthInfo(result, rcinfo);
0878                 stream2 << rcinfo;
0879             }
0880 
0881             if (waitRequest->isAsync) {
0882                 Q_EMIT checkAuthInfoAsyncResult(waitRequest->requestId, m_seqNr, rcinfo);
0883             } else {
0884                 QDBusConnection::sessionBus().send(waitRequest->transaction.createReply(QVariantList{QVariant(replyData), QVariant(m_seqNr)}));
0885             }
0886 
0887             delete waitRequest;
0888             it.remove();
0889         }
0890     }
0891 
0892     // Re-enable password request processing for the current window id again.
0893     m_authPrompted.removeAll(QString::number(request->windowId));
0894     m_authPrompted.removeAll(request->key);
0895 
0896     if (!m_authPending.isEmpty()) {
0897         QTimer::singleShot(0, this, &KPasswdServer::processRequest);
0898     }
0899 }
0900 
0901 void KPasswdServer::passwordDialogDone(int result, KPasswordDialog *sender)
0902 {
0903     std::unique_ptr<Request> request(m_authInProgress.take(sender));
0904     Q_ASSERT(request); // request should never be nullptr.
0905 
0906     if (request) {
0907         KIO::AuthInfo &info = request->info;
0908         const bool bypassCacheAndKWallet = info.getExtraField(QString::fromLatin1(s_bypassCacheAndKwallet)).toBool();
0909 
0910         qCDebug(category) << "dialog result=" << result << ", bypassCacheAndKWallet?" << bypassCacheAndKWallet;
0911         if (sender && result == QDialog::Accepted) {
0912             info.username = sender->username();
0913             info.password = sender->password();
0914             info.keepPassword = sender->keepPassword();
0915 
0916             if (info.getExtraField(QString::fromLatin1(s_domain)).isValid()) {
0917                 info.setExtraField(QString::fromLatin1(s_domain), sender->domain());
0918             }
0919             if (info.getExtraField(QString::fromLatin1(s_anonymous)).isValid()) {
0920                 info.setExtraField(QString::fromLatin1(s_anonymous), sender->anonymousMode());
0921             }
0922 
0923             // When the user checks "keep password", that means:
0924             // * if the wallet is enabled, store it there for long-term, and in kpasswdserver
0925             // only for the duration of the window (#92928)
0926             // * otherwise store in kpasswdserver for the duration of the KDE session.
0927             if (!bypassCacheAndKWallet) {
0928                 /*
0929                   NOTE: The following code changes the key under which the auth
0930                   info is stored in memory if the request url contains a username.
0931                   e.g. "ftp://user@localhost", but the user changes that username
0932                   in the password dialog.
0933 
0934                   Since the key generated to store the credential contains the
0935                   username from the request URL, the key must be updated on such
0936                   changes. Otherwise, the key will not be found on subsequent
0937                   requests and the user will be end up being prompted over and
0938                   over to re-enter the password unnecessarily.
0939                 */
0940                 if (!info.url.userName().isEmpty() && info.username != info.url.userName()) {
0941                     const QString oldKey(request->key);
0942                     removeAuthInfoItem(oldKey, info);
0943                     info.url.setUserName(info.username);
0944                     request->key = createCacheKey(info);
0945                     updateCachedRequestKey(m_authPending, oldKey, request->key);
0946                     updateCachedRequestKey(m_authWait, oldKey, request->key);
0947                 }
0948 
0949 #ifdef HAVE_KF6WALLET
0950                 const bool skipAutoCaching = info.getExtraField(QString::fromLatin1(s_skipCachingOnQuery)).toBool();
0951                 if (!skipAutoCaching && info.keepPassword && openWallet(request->windowId)) {
0952                     if (storeInWallet(m_wallet, request->key, info)) {
0953                         // password is in wallet, don't keep it in memory after window is closed
0954                         info.keepPassword = false;
0955                     }
0956                 }
0957 #endif
0958                 addAuthInfoItem(request->key, info, request->windowId, m_seqNr, false);
0959             }
0960             info.setModified(true);
0961         } else {
0962             if (!bypassCacheAndKWallet && request->prompt) {
0963                 addAuthInfoItem(request->key, info, 0, m_seqNr, true);
0964             }
0965             info.setModified(false);
0966         }
0967 
0968         sendResponse(request.get());
0969     }
0970 }
0971 
0972 void KPasswdServer::retryDialogDone(int result, KMessageDialog *sender)
0973 {
0974     std::unique_ptr<Request> request(m_authRetryInProgress.take(sender));
0975     Q_ASSERT(request);
0976 
0977     if (request) {
0978         if (result == KMessageDialog::PrimaryAction) {
0979             showPasswordDialog(request.release());
0980         } else {
0981             // NOTE: If the user simply cancels the retry dialog, we remove the
0982             // credential stored under this key because the original attempt to
0983             // use it has failed. Otherwise, the failed credential would be cached
0984             // and used subsequently.
0985             //
0986             // TODO: decide whether it should be removed from the wallet too.
0987             KIO::AuthInfo &info = request->info;
0988             removeAuthInfoItem(request->key, request->info);
0989             info.setModified(false);
0990             sendResponse(request.get());
0991         }
0992     }
0993 }
0994 
0995 void KPasswdServer::windowRemoved(WId id)
0996 {
0997     bool foundMatch = false;
0998     if (!m_authInProgress.isEmpty()) {
0999         const qlonglong windowId = static_cast<qlonglong>(id);
1000         QMutableHashIterator<QObject *, Request *> it(m_authInProgress);
1001         while (it.hasNext()) {
1002             it.next();
1003             if (it.value()->windowId == windowId) {
1004                 Request *request = it.value();
1005                 QObject *obj = it.key();
1006                 it.remove();
1007                 m_authPrompted.removeAll(QString::number(request->windowId));
1008                 m_authPrompted.removeAll(request->key);
1009                 delete obj;
1010                 delete request;
1011                 foundMatch = true;
1012             }
1013         }
1014     }
1015 
1016     if (!foundMatch && !m_authRetryInProgress.isEmpty()) {
1017         const qlonglong windowId = static_cast<qlonglong>(id);
1018         QMutableHashIterator<QObject *, Request *> it(m_authRetryInProgress);
1019         while (it.hasNext()) {
1020             it.next();
1021             if (it.value()->windowId == windowId) {
1022                 Request *request = it.value();
1023                 QObject *obj = it.key();
1024                 it.remove();
1025                 delete obj;
1026                 delete request;
1027             }
1028         }
1029     }
1030 }
1031 
1032 void KPasswdServer::updateCachedRequestKey(QList<KPasswdServer::Request *> &list, const QString &oldKey, const QString &newKey)
1033 {
1034     QListIterator<Request *> it(list);
1035     while (it.hasNext()) {
1036         Request *r = it.next();
1037         if (r->key == oldKey) {
1038             r->key = newKey;
1039         }
1040     }
1041 }
1042 
1043 #include "moc_kpasswdserver.cpp"