File indexing completed on 2024-12-01 12:36:28

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