File indexing completed on 2024-05-19 15:15:44

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 2019-2021 Harald Sitter <sitter@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 /*
0010     Recommended reading explaining FTP details and quirks:
0011       https://cr.yp.to/ftp.html  (by D.J. Bernstein)
0012 
0013     RFC:
0014       RFC  959 "File Transfer Protocol (FTP)"
0015       RFC 1635 "How to Use Anonymous FTP"
0016       RFC 2428 "FTP Extensions for IPv6 and NATs" (defines EPRT and EPSV)
0017 */
0018 
0019 #include <config-kioworker-ftp.h>
0020 
0021 #include "ftp.h"
0022 
0023 #ifdef Q_OS_WIN
0024 #include <sys/utime.h>
0025 #else
0026 #include <utime.h>
0027 #endif
0028 
0029 #include <cctype>
0030 #include <cerrno>
0031 #include <cstdlib>
0032 #include <cstring>
0033 
0034 #include <QAuthenticator>
0035 #include <QCoreApplication>
0036 #include <QDir>
0037 #include <QHostAddress>
0038 #include <QMimeDatabase>
0039 #include <QNetworkProxy>
0040 #include <QSslSocket>
0041 #include <QTcpServer>
0042 #include <QTcpSocket>
0043 
0044 #include <KConfigGroup>
0045 #include <KLocalizedString>
0046 #include <QDebug>
0047 #include <ioworker_defaults.h>
0048 #include <kremoteencoding.h>
0049 
0050 #include "kioglobal_p.h"
0051 
0052 #include <QLoggingCategory>
0053 Q_DECLARE_LOGGING_CATEGORY(KIO_FTP)
0054 Q_LOGGING_CATEGORY(KIO_FTP, "kf.kio.workers.ftp", QtWarningMsg)
0055 
0056 #if HAVE_STRTOLL
0057 #define charToLongLong(a) strtoll(a, nullptr, 10)
0058 #else
0059 #define charToLongLong(a) strtol(a, nullptr, 10)
0060 #endif
0061 
0062 static constexpr char s_ftpLogin[] = "anonymous";
0063 static constexpr char s_ftpPasswd[] = "anonymous@";
0064 
0065 static constexpr bool s_enableCanResume = true;
0066 
0067 // Pseudo plugin class to embed meta data
0068 class KIOPluginForMetaData : public QObject
0069 {
0070     Q_OBJECT
0071     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.ftp" FILE "ftp.json")
0072 };
0073 
0074 static QString ftpCleanPath(const QString &path)
0075 {
0076     if (path.endsWith(QLatin1String(";type=A"), Qt::CaseInsensitive) || path.endsWith(QLatin1String(";type=I"), Qt::CaseInsensitive)
0077         || path.endsWith(QLatin1String(";type=D"), Qt::CaseInsensitive)) {
0078         return path.left((path.length() - qstrlen(";type=X")));
0079     }
0080 
0081     return path;
0082 }
0083 
0084 static char ftpModeFromPath(const QString &path, char defaultMode = '\0')
0085 {
0086     const int index = path.lastIndexOf(QLatin1String(";type="));
0087 
0088     if (index > -1 && (index + 6) < path.size()) {
0089         const QChar mode = path.at(index + 6);
0090         // kio_ftp supports only A (ASCII) and I(BINARY) modes.
0091         if (mode == QLatin1Char('A') || mode == QLatin1Char('a') || mode == QLatin1Char('I') || mode == QLatin1Char('i')) {
0092             return mode.toUpper().toLatin1();
0093         }
0094     }
0095 
0096     return defaultMode;
0097 }
0098 
0099 static bool supportedProxyScheme(const QString &scheme)
0100 {
0101     return (scheme == QLatin1String("ftp") || scheme == QLatin1String("socks"));
0102 }
0103 
0104 // JPF: somebody should find a better solution for this or move this to KIO
0105 namespace KIO
0106 {
0107 enum buffersizes {
0108     /**
0109      * largest buffer size that should be used to transfer data between
0110      * KIO workers using the data() function
0111      */
0112     maximumIpcSize = 32 * 1024,
0113     /**
0114      * this is a reasonable value for an initial read() that a KIO worker
0115      * can do to obtain data via a slow network connection.
0116      */
0117     initialIpcSize = 2 * 1024,
0118     /**
0119      * recommended size of a data block passed to findBufferFileType()
0120      */
0121     minimumMimeSize = 1024,
0122 };
0123 
0124 // JPF: this helper was derived from write_all in file.cc (FileProtocol).
0125 static // JPF: in ftp.cc we make it static
0126     /**
0127      * This helper handles some special issues (blocking and interrupted
0128      * system call) when writing to a file handle.
0129      *
0130      * @return 0 on success or an error code on failure (ERR_CANNOT_WRITE,
0131      * ERR_DISK_FULL, ERR_CONNECTION_BROKEN).
0132      */
0133     int
0134     WriteToFile(int fd, const char *buf, size_t len)
0135 {
0136     while (len > 0) {
0137         // JPF: shouldn't there be a KDE_write?
0138         ssize_t written = write(fd, buf, len);
0139         if (written >= 0) {
0140             buf += written;
0141             len -= written;
0142             continue;
0143         }
0144         switch (errno) {
0145         case EINTR:
0146             continue;
0147         case EPIPE:
0148             return ERR_CONNECTION_BROKEN;
0149         case ENOSPC:
0150             return ERR_DISK_FULL;
0151         default:
0152             return ERR_CANNOT_WRITE;
0153         }
0154     }
0155     return 0;
0156 }
0157 }
0158 
0159 const KIO::filesize_t FtpInternal::UnknownSize = (KIO::filesize_t)-1;
0160 
0161 using namespace KIO;
0162 
0163 extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
0164 {
0165     QCoreApplication app(argc, argv);
0166     app.setApplicationName(QStringLiteral("kio_ftp"));
0167 
0168     qCDebug(KIO_FTP) << "Starting";
0169 
0170     if (argc != 4) {
0171         fprintf(stderr, "Usage: kio_ftp protocol domain-socket1 domain-socket2\n");
0172         exit(-1);
0173     }
0174 
0175     Ftp worker(argv[2], argv[3]);
0176     worker.dispatchLoop();
0177 
0178     qCDebug(KIO_FTP) << "Done";
0179     return 0;
0180 }
0181 
0182 //===============================================================================
0183 // FtpInternal
0184 //===============================================================================
0185 
0186 /**
0187  * This closes a data connection opened by ftpOpenDataConnection().
0188  */
0189 void FtpInternal::ftpCloseDataConnection()
0190 {
0191     delete m_data;
0192     m_data = nullptr;
0193     delete m_server;
0194     m_server = nullptr;
0195 }
0196 
0197 /**
0198  * This closes a control connection opened by ftpOpenControlConnection() and reinits the
0199  * related states.  This method gets called from the constructor with m_control = nullptr.
0200  */
0201 void FtpInternal::ftpCloseControlConnection()
0202 {
0203     m_extControl = 0;
0204     delete m_control;
0205     m_control = nullptr;
0206     m_cDataMode = 0;
0207     m_bLoggedOn = false; // logon needs control connection
0208     m_bTextMode = false;
0209     m_bBusy = false;
0210 }
0211 
0212 /**
0213  * Returns the last response from the server (iOffset >= 0)  -or-  reads a new response
0214  * (iOffset < 0). The result is returned (with iOffset chars skipped for iOffset > 0).
0215  */
0216 const char *FtpInternal::ftpResponse(int iOffset)
0217 {
0218     Q_ASSERT(m_control); // must have control connection socket
0219     const char *pTxt = m_lastControlLine.data();
0220 
0221     // read the next line ...
0222     if (iOffset < 0) {
0223         int iMore = 0;
0224         m_iRespCode = 0;
0225 
0226         if (!pTxt) {
0227             return nullptr; // avoid using a nullptr when calling atoi.
0228         }
0229 
0230         // If the server sends a multiline response starting with
0231         // "nnn-text" we loop here until a final "nnn text" line is
0232         // reached. Only data from the final line will be stored.
0233         do {
0234             while (!m_control->canReadLine() && m_control->waitForReadyRead((q->readTimeout() * 1000))) { }
0235             m_lastControlLine = m_control->readLine();
0236             pTxt = m_lastControlLine.data();
0237             int iCode = atoi(pTxt);
0238             if (iMore == 0) {
0239                 // first line
0240                 qCDebug(KIO_FTP) << "    > " << pTxt;
0241                 if (iCode >= 100) {
0242                     m_iRespCode = iCode;
0243                     if (pTxt[3] == '-') {
0244                         // marker for a multiple line response
0245                         iMore = iCode;
0246                     }
0247                 } else {
0248                     qCWarning(KIO_FTP) << "Cannot parse valid code from line" << pTxt;
0249                 }
0250             } else {
0251                 // multi-line
0252                 qCDebug(KIO_FTP) << "    > " << pTxt;
0253                 if (iCode >= 100 && iCode == iMore && pTxt[3] == ' ') {
0254                     iMore = 0;
0255                 }
0256             }
0257         } while (iMore != 0);
0258         qCDebug(KIO_FTP) << "resp> " << pTxt;
0259 
0260         m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0;
0261     }
0262 
0263     // return text with offset ...
0264     while (iOffset-- > 0 && pTxt[0]) {
0265         pTxt++;
0266     }
0267     return pTxt;
0268 }
0269 
0270 void FtpInternal::closeConnection()
0271 {
0272     if (m_control || m_data) {
0273         qCDebug(KIO_FTP) << "m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy;
0274     }
0275 
0276     if (m_bBusy) { // ftpCloseCommand not called
0277         qCWarning(KIO_FTP) << "Abandoned data stream";
0278         ftpCloseDataConnection();
0279     }
0280 
0281     if (m_bLoggedOn) { // send quit
0282         if (!ftpSendCmd(QByteArrayLiteral("quit"), 0) || (m_iRespType != 2)) {
0283             qCWarning(KIO_FTP) << "QUIT returned error: " << m_iRespCode;
0284         }
0285     }
0286 
0287     // close the data and control connections ...
0288     ftpCloseDataConnection();
0289     ftpCloseControlConnection();
0290 }
0291 
0292 FtpInternal::FtpInternal(Ftp *qptr)
0293     : QObject()
0294     , q(qptr)
0295 {
0296     ftpCloseControlConnection();
0297 }
0298 
0299 FtpInternal::~FtpInternal()
0300 {
0301     qCDebug(KIO_FTP);
0302     closeConnection();
0303 }
0304 
0305 void FtpInternal::setHost(const QString &_host, quint16 _port, const QString &_user, const QString &_pass)
0306 {
0307     qCDebug(KIO_FTP) << _host << "port=" << _port << "user=" << _user;
0308 
0309     m_proxyURL.clear();
0310     m_proxyUrls = q->mapConfig().value(QStringLiteral("ProxyUrls"), QString()).toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
0311 
0312     qCDebug(KIO_FTP) << "proxy urls:" << m_proxyUrls;
0313 
0314     if (m_host != _host || m_port != _port || m_user != _user || m_pass != _pass) {
0315         closeConnection();
0316     }
0317 
0318     m_host = _host;
0319     m_port = _port;
0320     m_user = _user;
0321     m_pass = _pass;
0322 }
0323 
0324 Result FtpInternal::openConnection()
0325 {
0326     return ftpOpenConnection(LoginMode::Explicit);
0327 }
0328 
0329 Result FtpInternal::ftpOpenConnection(LoginMode loginMode)
0330 {
0331     // check for implicit login if we are already logged on ...
0332     if (loginMode == LoginMode::Implicit && m_bLoggedOn) {
0333         Q_ASSERT(m_control); // must have control connection socket
0334         return Result::pass();
0335     }
0336 
0337     qCDebug(KIO_FTP) << "host=" << m_host << ", port=" << m_port << ", user=" << m_user << "password= [password hidden]";
0338 
0339     q->infoMessage(i18n("Opening connection to host %1", m_host));
0340 
0341     if (m_host.isEmpty()) {
0342         return Result::fail(ERR_UNKNOWN_HOST);
0343     }
0344 
0345     Q_ASSERT(!m_bLoggedOn);
0346 
0347     m_initialPath.clear();
0348     m_currentPath.clear();
0349 
0350     const Result result = ftpOpenControlConnection();
0351     if (!result.success()) {
0352         return result;
0353     }
0354     q->infoMessage(i18n("Connected to host %1", m_host));
0355 
0356     bool userNameChanged = false;
0357     if (loginMode != LoginMode::Deferred) {
0358         const Result result = ftpLogin(&userNameChanged);
0359         m_bLoggedOn = result.success();
0360         if (!m_bLoggedOn) {
0361             return result;
0362         }
0363     }
0364 
0365     m_bTextMode = q->configValue(QStringLiteral("textmode"), false);
0366 
0367     // Redirected due to credential change...
0368     if (userNameChanged && m_bLoggedOn) {
0369         QUrl realURL;
0370         realURL.setScheme(QStringLiteral("ftp"));
0371         if (m_user != QLatin1String(s_ftpLogin)) {
0372             realURL.setUserName(m_user);
0373         }
0374         if (m_pass != QLatin1String(s_ftpPasswd)) {
0375             realURL.setPassword(m_pass);
0376         }
0377         realURL.setHost(m_host);
0378         if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
0379             realURL.setPort(m_port);
0380         }
0381         if (m_initialPath.isEmpty()) {
0382             m_initialPath = QStringLiteral("/");
0383         }
0384         realURL.setPath(m_initialPath);
0385         qCDebug(KIO_FTP) << "User name changed! Redirecting to" << realURL;
0386         q->redirection(realURL);
0387         return Result::fail();
0388     }
0389 
0390     return Result::pass();
0391 }
0392 
0393 /**
0394  * Called by @ref openConnection. It opens the control connection to the ftp server.
0395  *
0396  * @return true on success.
0397  */
0398 Result FtpInternal::ftpOpenControlConnection()
0399 {
0400     if (m_proxyUrls.isEmpty()) {
0401         return ftpOpenControlConnection(m_host, m_port);
0402     }
0403 
0404     Result result = Result::fail();
0405 
0406     for (const QString &proxyUrl : std::as_const(m_proxyUrls)) {
0407         const QUrl url(proxyUrl);
0408         const QString scheme(url.scheme());
0409 
0410         if (!supportedProxyScheme(scheme)) {
0411             // TODO: Need a new error code to indicate unsupported URL scheme.
0412             result = Result::fail(ERR_CANNOT_CONNECT, url.toString());
0413             continue;
0414         }
0415 
0416         if (!isSocksProxyScheme(scheme)) {
0417             const Result result = ftpOpenControlConnection(url.host(), url.port());
0418             if (result.success()) {
0419                 return Result::pass();
0420             }
0421             continue;
0422         }
0423 
0424         qCDebug(KIO_FTP) << "Connecting to SOCKS proxy @" << url;
0425         m_proxyURL = url;
0426         result = ftpOpenControlConnection(m_host, m_port);
0427         if (result.success()) {
0428             return result;
0429         }
0430         m_proxyURL.clear();
0431     }
0432 
0433     return result;
0434 }
0435 
0436 Result FtpInternal::ftpOpenControlConnection(const QString &host, int port)
0437 {
0438     // implicitly close, then try to open a new connection ...
0439     closeConnection();
0440     QString sErrorMsg;
0441 
0442     // now connect to the server and read the login message ...
0443     if (port == 0) {
0444         port = 21; // default FTP port
0445     }
0446     const auto connectionResult = synchronousConnectToHost(host, port);
0447     m_control = connectionResult.socket;
0448 
0449     int iErrorCode = m_control->state() == QAbstractSocket::ConnectedState ? 0 : ERR_CANNOT_CONNECT;
0450     if (!connectionResult.result.success()) {
0451         qDebug() << "overriding error code!!1" << connectionResult.result.error();
0452         iErrorCode = connectionResult.result.error();
0453         sErrorMsg = connectionResult.result.errorString();
0454     }
0455 
0456     // on connect success try to read the server message...
0457     if (iErrorCode == 0) {
0458         const char *psz = ftpResponse(-1);
0459         if (m_iRespType != 2) {
0460             // login not successful, do we have an message text?
0461             if (psz[0]) {
0462                 sErrorMsg = i18n("%1 (Error %2)", host, q->remoteEncoding()->decode(psz).trimmed());
0463             }
0464             iErrorCode = ERR_CANNOT_CONNECT;
0465         }
0466     } else {
0467         const auto socketError = m_control->error();
0468         if (socketError == QAbstractSocket::HostNotFoundError) {
0469             iErrorCode = ERR_UNKNOWN_HOST;
0470         }
0471 
0472         sErrorMsg = QStringLiteral("%1: %2").arg(host, m_control->errorString());
0473     }
0474 
0475     // if there was a problem - report it ...
0476     if (iErrorCode == 0) { // OK, return success
0477         return Result::pass();
0478     }
0479     closeConnection(); // clean-up on error
0480     return Result::fail(iErrorCode, sErrorMsg);
0481 }
0482 
0483 /**
0484  * Called by @ref openConnection. It logs us in.
0485  * @ref m_initialPath is set to the current working directory
0486  * if logging on was successful.
0487  *
0488  * @return true on success.
0489  */
0490 Result FtpInternal::ftpLogin(bool *userChanged)
0491 {
0492     q->infoMessage(i18n("Sending login information"));
0493 
0494     Q_ASSERT(!m_bLoggedOn);
0495 
0496     QString user(m_user);
0497     QString pass(m_pass);
0498 
0499     if (q->configValue(QStringLiteral("EnableAutoLogin"), false)) {
0500         QString au = q->configValue(QStringLiteral("autoLoginUser"));
0501         if (!au.isEmpty()) {
0502             user = au;
0503             pass = q->configValue(QStringLiteral("autoLoginPass"));
0504         }
0505     }
0506 
0507     AuthInfo info;
0508     info.url.setScheme(QStringLiteral("ftp"));
0509     info.url.setHost(m_host);
0510     if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
0511         info.url.setPort(m_port);
0512     }
0513     if (!user.isEmpty()) {
0514         info.url.setUserName(user);
0515     }
0516 
0517     // Check for cached authentication first and fallback to
0518     // anonymous login when no stored credentials are found.
0519     if (!q->configValue(QStringLiteral("TryAnonymousLoginFirst"), false) && pass.isEmpty() && q->checkCachedAuthentication(info)) {
0520         user = info.username;
0521         pass = info.password;
0522     }
0523 
0524     // Try anonymous login if both username/password
0525     // information is blank.
0526     if (user.isEmpty() && pass.isEmpty()) {
0527         user = QString::fromLatin1(s_ftpLogin);
0528         pass = QString::fromLatin1(s_ftpPasswd);
0529     }
0530 
0531     QByteArray tempbuf;
0532     QString lastServerResponse;
0533     int failedAuth = 0;
0534     bool promptForRetry = false;
0535 
0536     // Give the user the option to login anonymously...
0537     info.setExtraField(QStringLiteral("anonymous"), false);
0538 
0539     do {
0540         // Check the cache and/or prompt user for password if 1st
0541         // login attempt failed OR the user supplied a login name,
0542         // but no password.
0543         if (failedAuth > 0 || (!user.isEmpty() && pass.isEmpty())) {
0544             QString errorMsg;
0545             qCDebug(KIO_FTP) << "Prompting user for login info...";
0546 
0547             // Ask user if we should retry after when login fails!
0548             if (failedAuth > 0 && promptForRetry) {
0549                 errorMsg = i18n(
0550                     "Message sent:\nLogin using username=%1 and "
0551                     "password=[hidden]\n\nServer replied:\n%2\n\n",
0552                     user,
0553                     lastServerResponse);
0554             }
0555 
0556             if (user != QLatin1String(s_ftpLogin)) {
0557                 info.username = user;
0558             }
0559 
0560             info.prompt = i18n(
0561                 "You need to supply a username and a password "
0562                 "to access this site.");
0563             info.commentLabel = i18n("Site:");
0564             info.comment = i18n("<b>%1</b>", m_host);
0565             info.keepPassword = true; // Prompt the user for persistence as well.
0566             info.setModified(false); // Default the modified flag since we reuse authinfo.
0567 
0568             const bool disablePassDlg = q->configValue(QStringLiteral("DisablePassDlg"), false);
0569             if (disablePassDlg) {
0570                 return Result::fail(ERR_USER_CANCELED, m_host);
0571             }
0572             const int errorCode = q->openPasswordDialog(info, errorMsg);
0573             if (errorCode) {
0574                 return Result::fail(errorCode);
0575             } else {
0576                 // User can decide go anonymous using checkbox
0577                 if (info.getExtraField(QStringLiteral("anonymous")).toBool()) {
0578                     user = QString::fromLatin1(s_ftpLogin);
0579                     pass = QString::fromLatin1(s_ftpPasswd);
0580                 } else {
0581                     user = info.username;
0582                     pass = info.password;
0583                 }
0584                 promptForRetry = true;
0585             }
0586         }
0587 
0588         tempbuf = "USER " + user.toLatin1();
0589         if (m_proxyURL.isValid()) {
0590             tempbuf += '@' + m_host.toLatin1();
0591             if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
0592                 tempbuf += ':' + QByteArray::number(m_port);
0593             }
0594         }
0595 
0596         qCDebug(KIO_FTP) << "Sending Login name: " << tempbuf;
0597 
0598         bool loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230));
0599         bool needPass = (m_iRespCode == 331);
0600         // Prompt user for login info if we do not
0601         // get back a "230" or "331".
0602         if (!loggedIn && !needPass) {
0603             lastServerResponse = QString::fromUtf8(ftpResponse(0));
0604             qCDebug(KIO_FTP) << "Login failed: " << lastServerResponse;
0605             ++failedAuth;
0606             continue; // Well we failed, prompt the user please!!
0607         }
0608 
0609         if (needPass) {
0610             tempbuf = "PASS " + pass.toLatin1();
0611             qCDebug(KIO_FTP) << "Sending Login password: "
0612                              << "[protected]";
0613             loggedIn = (ftpSendCmd(tempbuf) && (m_iRespCode == 230));
0614         }
0615 
0616         if (loggedIn) {
0617             // Make sure the user name changed flag is properly set.
0618             if (userChanged) {
0619                 *userChanged = (!m_user.isEmpty() && (m_user != user));
0620             }
0621 
0622             // Do not cache the default login!!
0623             if (user != QLatin1String(s_ftpLogin) && pass != QLatin1String(s_ftpPasswd)) {
0624                 // Update the username in case it was changed during login.
0625                 if (!m_user.isEmpty()) {
0626                     info.url.setUserName(user);
0627                     m_user = user;
0628                 }
0629 
0630                 // Cache the password if the user requested it.
0631                 if (info.keepPassword) {
0632                     q->cacheAuthentication(info);
0633                 }
0634             }
0635             failedAuth = -1;
0636         } else {
0637             // some servers don't let you login anymore
0638             // if you fail login once, so restart the connection here
0639             lastServerResponse = QString::fromUtf8(ftpResponse(0));
0640             const Result result = ftpOpenControlConnection();
0641             if (!result.success()) {
0642                 return result;
0643             }
0644         }
0645     } while (++failedAuth);
0646 
0647     qCDebug(KIO_FTP) << "Login OK";
0648     q->infoMessage(i18n("Login OK"));
0649 
0650     // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix:
0651     // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint
0652     if (ftpSendCmd(QByteArrayLiteral("SYST")) && (m_iRespType == 2)) {
0653         if (!qstrncmp(ftpResponse(0), "215 Windows_NT", 14)) { // should do for any version
0654             (void)ftpSendCmd(QByteArrayLiteral("site dirstyle"));
0655             // Check if it was already in Unix style
0656             // Patch from Keith Refson <Keith.Refson@earth.ox.ac.uk>
0657             if (!qstrncmp(ftpResponse(0), "200 MSDOS-like directory output is on", 37))
0658             // It was in Unix style already!
0659             {
0660                 (void)ftpSendCmd(QByteArrayLiteral("site dirstyle"));
0661             }
0662             // windows won't support chmod before KDE konquers their desktop...
0663             m_extControl |= chmodUnknown;
0664         }
0665     } else {
0666         qCWarning(KIO_FTP) << "SYST failed";
0667     }
0668 
0669     if (q->configValue(QStringLiteral("EnableAutoLoginMacro"), false)) {
0670         ftpAutoLoginMacro();
0671     }
0672 
0673     // Get the current working directory
0674     qCDebug(KIO_FTP) << "Searching for pwd";
0675     if (!ftpSendCmd(QByteArrayLiteral("PWD")) || (m_iRespType != 2)) {
0676         qCDebug(KIO_FTP) << "Couldn't issue pwd command";
0677         return Result::fail(ERR_CANNOT_LOGIN, i18n("Could not login to %1.", m_host)); // or anything better ?
0678     }
0679 
0680     QString sTmp = q->remoteEncoding()->decode(ftpResponse(3));
0681     const int iBeg = sTmp.indexOf(QLatin1Char('"'));
0682     const int iEnd = sTmp.lastIndexOf(QLatin1Char('"'));
0683     if (iBeg > 0 && iBeg < iEnd) {
0684         m_initialPath = sTmp.mid(iBeg + 1, iEnd - iBeg - 1);
0685         if (!m_initialPath.startsWith(QLatin1Char('/'))) {
0686             m_initialPath.prepend(QLatin1Char('/'));
0687         }
0688         qCDebug(KIO_FTP) << "Initial path set to: " << m_initialPath;
0689         m_currentPath = m_initialPath;
0690     }
0691 
0692     return Result::pass();
0693 }
0694 
0695 void FtpInternal::ftpAutoLoginMacro()
0696 {
0697     QString macro = q->metaData(QStringLiteral("autoLoginMacro"));
0698 
0699     if (macro.isEmpty()) {
0700         return;
0701     }
0702 
0703     QStringList list = macro.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0704     auto initIt = std::find_if(list.cbegin(), list.cend(), [](const QString &s) {
0705         return s.startsWith(QLatin1String("init"));
0706     });
0707 
0708     if (initIt != list.cend()) {
0709         list = macro.split(QLatin1Char('\\'), Qt::SkipEmptyParts);
0710         // Ignore the macro name, so start from list.cbegin() + 1
0711         for (auto it = list.cbegin() + 1; it != list.cend(); ++it) {
0712             // TODO: Add support for arbitrary commands besides simply changing directory!!
0713             if ((*it).startsWith(QLatin1String("cwd"))) {
0714                 (void)ftpFolder((*it).mid(4));
0715             }
0716         }
0717     }
0718 }
0719 
0720 /**
0721  * ftpSendCmd - send a command (@p cmd) and read response
0722  *
0723  * @param maxretries number of time it should retry. Since it recursively
0724  * calls itself if it can't read the answer (this happens especially after
0725  * timeouts), we need to limit the recursiveness ;-)
0726  *
0727  * return true if any response received, false on error
0728  */
0729 bool FtpInternal::ftpSendCmd(const QByteArray &cmd, int maxretries)
0730 {
0731     Q_ASSERT(m_control); // must have control connection socket
0732 
0733     if (cmd.indexOf('\r') != -1 || cmd.indexOf('\n') != -1) {
0734         qCWarning(KIO_FTP) << "Invalid command received (contains CR or LF):" << cmd.data();
0735         return false;
0736     }
0737 
0738     // Don't print out the password...
0739     bool isPassCmd = (cmd.left(4).toLower() == "pass");
0740 
0741     // Send the message...
0742     const QByteArray buf = cmd + "\r\n"; // Yes, must use CR/LF - see https://cr.yp.to/ftp/request.html
0743     int num = m_control->write(buf);
0744     while (m_control->bytesToWrite() && m_control->waitForBytesWritten()) { }
0745 
0746     // If we were able to successfully send the command, then we will
0747     // attempt to read the response. Otherwise, take action to re-attempt
0748     // the login based on the maximum number of retries specified...
0749     if (num > 0) {
0750         ftpResponse(-1);
0751     } else {
0752         m_iRespType = m_iRespCode = 0;
0753     }
0754 
0755     // If respCh is NULL or the response is 421 (Timed-out), we try to re-send
0756     // the command based on the value of maxretries.
0757     if ((m_iRespType <= 0) || (m_iRespCode == 421)) {
0758         // We have not yet logged on...
0759         if (!m_bLoggedOn) {
0760             // The command was sent from the ftpLogin function, i.e. we are actually
0761             // attempting to login in. NOTE: If we already sent the username, we
0762             // return false and let the user decide whether (s)he wants to start from
0763             // the beginning...
0764             if (maxretries > 0 && !isPassCmd) {
0765                 closeConnection();
0766                 const auto result = ftpOpenConnection(LoginMode::Deferred);
0767                 if (result.success() && ftpSendCmd(cmd, maxretries - 1)) {
0768                     return true;
0769                 }
0770             }
0771 
0772             return false;
0773         } else {
0774             if (maxretries < 1) {
0775                 return false;
0776             } else {
0777                 qCDebug(KIO_FTP) << "Was not able to communicate with " << m_host << "Attempting to re-establish connection.";
0778 
0779                 closeConnection(); // Close the old connection...
0780                 const Result openResult = openConnection(); // Attempt to re-establish a new connection...
0781 
0782                 if (!openResult.success()) {
0783                     if (m_control) { // if openConnection succeeded ...
0784                         qCDebug(KIO_FTP) << "Login failure, aborting";
0785                         closeConnection();
0786                     }
0787                     return false;
0788                 }
0789 
0790                 qCDebug(KIO_FTP) << "Logged back in, re-issuing command";
0791 
0792                 // If we were able to login, resend the command...
0793                 if (maxretries) {
0794                     maxretries--;
0795                 }
0796 
0797                 return ftpSendCmd(cmd, maxretries);
0798             }
0799         }
0800     }
0801 
0802     return true;
0803 }
0804 
0805 /*
0806  * ftpOpenPASVDataConnection - set up data connection, using PASV mode
0807  *
0808  * return 0 if successful, ERR_INTERNAL otherwise
0809  * doesn't set error message, since non-pasv mode will always be tried if
0810  * this one fails
0811  */
0812 int FtpInternal::ftpOpenPASVDataConnection()
0813 {
0814     Q_ASSERT(m_control); // must have control connection socket
0815     Q_ASSERT(!m_data); // ... but no data connection
0816 
0817     // Check that we can do PASV
0818     QHostAddress address = m_control->peerAddress();
0819     if (address.protocol() != QAbstractSocket::IPv4Protocol && !isSocksProxy()) {
0820         return ERR_INTERNAL; // no PASV for non-PF_INET connections
0821     }
0822 
0823     if (m_extControl & pasvUnknown) {
0824         return ERR_INTERNAL; // already tried and got "unknown command"
0825     }
0826 
0827     m_bPasv = true;
0828 
0829     /* Let's PASsiVe*/
0830     if (!ftpSendCmd(QByteArrayLiteral("PASV")) || (m_iRespType != 2)) {
0831         qCDebug(KIO_FTP) << "PASV attempt failed";
0832         // unknown command?
0833         if (m_iRespType == 5) {
0834             qCDebug(KIO_FTP) << "disabling use of PASV";
0835             m_extControl |= pasvUnknown;
0836         }
0837         return ERR_INTERNAL;
0838     }
0839 
0840     // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)'
0841     // but anonftpd gives '227 =160,39,200,55,6,245'
0842     int i[6];
0843     const char *start = strchr(ftpResponse(3), '(');
0844     if (!start) {
0845         start = strchr(ftpResponse(3), '=');
0846     }
0847     if (!start
0848         || (sscanf(start, "(%d,%d,%d,%d,%d,%d)", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6
0849             && sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6)) {
0850         qCritical() << "parsing IP and port numbers failed. String parsed: " << start;
0851         return ERR_INTERNAL;
0852     }
0853 
0854     // we ignore the host part on purpose for two reasons
0855     // a) it might be wrong anyway
0856     // b) it would make us being susceptible to a port scanning attack
0857 
0858     // now connect the data socket ...
0859     quint16 port = i[4] << 8 | i[5];
0860     const QString host = (isSocksProxy() ? m_host : address.toString());
0861     const auto connectionResult = synchronousConnectToHost(host, port);
0862     m_data = connectionResult.socket;
0863     if (!connectionResult.result.success()) {
0864         return connectionResult.result.error();
0865     }
0866 
0867     return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL;
0868 }
0869 
0870 /*
0871  * ftpOpenEPSVDataConnection - opens a data connection via EPSV
0872  */
0873 int FtpInternal::ftpOpenEPSVDataConnection()
0874 {
0875     Q_ASSERT(m_control); // must have control connection socket
0876     Q_ASSERT(!m_data); // ... but no data connection
0877 
0878     QHostAddress address = m_control->peerAddress();
0879     int portnum;
0880 
0881     if (m_extControl & epsvUnknown) {
0882         return ERR_INTERNAL;
0883     }
0884 
0885     m_bPasv = true;
0886     if (!ftpSendCmd(QByteArrayLiteral("EPSV")) || (m_iRespType != 2)) {
0887         // unknown command?
0888         if (m_iRespType == 5) {
0889             qCDebug(KIO_FTP) << "disabling use of EPSV";
0890             m_extControl |= epsvUnknown;
0891         }
0892         return ERR_INTERNAL;
0893     }
0894 
0895     const char *start = strchr(ftpResponse(3), '|');
0896     if (!start || sscanf(start, "|||%d|", &portnum) != 1) {
0897         return ERR_INTERNAL;
0898     }
0899     Q_ASSERT(portnum > 0);
0900 
0901     const QString host = (isSocksProxy() ? m_host : address.toString());
0902     const auto connectionResult = synchronousConnectToHost(host, static_cast<quint16>(portnum));
0903     m_data = connectionResult.socket;
0904     if (!connectionResult.result.success()) {
0905         return connectionResult.result.error();
0906     }
0907     return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL;
0908 }
0909 
0910 /*
0911  * ftpOpenDataConnection - set up data connection
0912  *
0913  * The routine calls several ftpOpenXxxxConnection() helpers to find
0914  * the best connection mode. If a helper cannot connect if returns
0915  * ERR_INTERNAL - so this is not really an error! All other error
0916  * codes are treated as fatal, e.g. they are passed back to the caller
0917  * who is responsible for calling error(). ftpOpenPortDataConnection
0918  * can be called as last try and it does never return ERR_INTERNAL.
0919  *
0920  * @return 0 if successful, err code otherwise
0921  */
0922 int FtpInternal::ftpOpenDataConnection()
0923 {
0924     // make sure that we are logged on and have no data connection...
0925     Q_ASSERT(m_bLoggedOn);
0926     ftpCloseDataConnection();
0927 
0928     int iErrCode = 0;
0929     int iErrCodePASV = 0; // Remember error code from PASV
0930 
0931     // First try passive (EPSV & PASV) modes
0932     if (!q->configValue(QStringLiteral("DisablePassiveMode"), false)) {
0933         iErrCode = ftpOpenPASVDataConnection();
0934         if (iErrCode == 0) {
0935             return 0; // success
0936         }
0937         iErrCodePASV = iErrCode;
0938         ftpCloseDataConnection();
0939 
0940         if (!q->configValue(QStringLiteral("DisableEPSV"), false)) {
0941             iErrCode = ftpOpenEPSVDataConnection();
0942             if (iErrCode == 0) {
0943                 return 0; // success
0944             }
0945             ftpCloseDataConnection();
0946         }
0947 
0948         // if we sent EPSV ALL already and it was accepted, then we can't
0949         // use active connections any more
0950         if (m_extControl & epsvAllSent) {
0951             return iErrCodePASV;
0952         }
0953     }
0954 
0955     // fall back to port mode
0956     iErrCode = ftpOpenPortDataConnection();
0957     if (iErrCode == 0) {
0958         return 0; // success
0959     }
0960 
0961     ftpCloseDataConnection();
0962     // prefer to return the error code from PASV if any, since that's what should have worked in the first place
0963     return iErrCodePASV ? iErrCodePASV : iErrCode;
0964 }
0965 
0966 /*
0967  * ftpOpenPortDataConnection - set up data connection
0968  *
0969  * @return 0 if successful, err code otherwise (but never ERR_INTERNAL
0970  *         because this is the last connection mode that is tried)
0971  */
0972 int FtpInternal::ftpOpenPortDataConnection()
0973 {
0974     Q_ASSERT(m_control); // must have control connection socket
0975     Q_ASSERT(!m_data); // ... but no data connection
0976 
0977     m_bPasv = false;
0978     if (m_extControl & eprtUnknown) {
0979         return ERR_INTERNAL;
0980     }
0981 
0982     if (!m_server) {
0983         m_server = new QTcpServer;
0984         m_server->listen(QHostAddress::Any, 0);
0985     }
0986 
0987     if (!m_server->isListening()) {
0988         delete m_server;
0989         m_server = nullptr;
0990         return ERR_CANNOT_LISTEN;
0991     }
0992 
0993     m_server->setMaxPendingConnections(1);
0994 
0995     QString command;
0996     QHostAddress localAddress = m_control->localAddress();
0997     if (localAddress.protocol() == QAbstractSocket::IPv4Protocol) {
0998         struct {
0999             quint32 ip4;
1000             quint16 port;
1001         } data;
1002         data.ip4 = localAddress.toIPv4Address();
1003         data.port = m_server->serverPort();
1004 
1005         unsigned char *pData = reinterpret_cast<unsigned char *>(&data);
1006         command = QStringLiteral("PORT %1,%2,%3,%4,%5,%6").arg(pData[3]).arg(pData[2]).arg(pData[1]).arg(pData[0]).arg(pData[5]).arg(pData[4]);
1007     } else if (localAddress.protocol() == QAbstractSocket::IPv6Protocol) {
1008         command = QStringLiteral("EPRT |2|%2|%3|").arg(localAddress.toString()).arg(m_server->serverPort());
1009     }
1010 
1011     if (ftpSendCmd(command.toLatin1()) && (m_iRespType == 2)) {
1012         return 0;
1013     }
1014 
1015     delete m_server;
1016     m_server = nullptr;
1017     return ERR_INTERNAL;
1018 }
1019 
1020 Result FtpInternal::ftpOpenCommand(const char *_command, const QString &_path, char _mode, int errorcode, KIO::fileoffset_t _offset)
1021 {
1022     if (!ftpDataMode(ftpModeFromPath(_path, _mode))) {
1023         return Result::fail(ERR_CANNOT_CONNECT, m_host);
1024     }
1025 
1026     if (int error = ftpOpenDataConnection()) {
1027         return Result::fail(error, m_host);
1028     }
1029 
1030     if (_offset > 0) {
1031         // send rest command if offset > 0, this applies to retr and stor commands
1032         char buf[100];
1033         sprintf(buf, "rest %lld", _offset);
1034         if (!ftpSendCmd(buf)) {
1035             return Result::fail();
1036         }
1037         if (m_iRespType != 3) {
1038             return Result::fail(ERR_CANNOT_RESUME, _path); // should never happen
1039         }
1040     }
1041 
1042     QByteArray tmp = _command;
1043     QString errormessage;
1044 
1045     if (!_path.isEmpty()) {
1046         tmp += ' ' + q->remoteEncoding()->encode(ftpCleanPath(_path));
1047     }
1048 
1049     if (!ftpSendCmd(tmp) || (m_iRespType != 1)) {
1050         if (_offset > 0 && qstrcmp(_command, "retr") == 0 && (m_iRespType == 4)) {
1051             errorcode = ERR_CANNOT_RESUME;
1052         }
1053         // The error code here depends on the command
1054         errormessage = _path + i18n("\nThe server said: \"%1\"", QString::fromUtf8(ftpResponse(0)).trimmed());
1055     }
1056 
1057     else {
1058         // Only now we know for sure that we can resume
1059         if (_offset > 0 && qstrcmp(_command, "retr") == 0) {
1060             q->canResume();
1061         }
1062 
1063         if (m_server && !m_data) {
1064             qCDebug(KIO_FTP) << "waiting for connection from remote.";
1065             m_server->waitForNewConnection(q->connectTimeout() * 1000);
1066             m_data = m_server->nextPendingConnection();
1067         }
1068 
1069         if (m_data) {
1070             qCDebug(KIO_FTP) << "connected with remote.";
1071             m_bBusy = true; // cleared in ftpCloseCommand
1072             return Result::pass();
1073         }
1074 
1075         qCDebug(KIO_FTP) << "no connection received from remote.";
1076         errorcode = ERR_CANNOT_ACCEPT;
1077         errormessage = m_host;
1078     }
1079 
1080     if (errorcode != KJob::NoError) {
1081         return Result::fail(errorcode, errormessage);
1082     }
1083     return Result::fail();
1084 }
1085 
1086 bool FtpInternal::ftpCloseCommand()
1087 {
1088     // first close data sockets (if opened), then read response that
1089     // we got for whatever was used in ftpOpenCommand ( should be 226 )
1090     ftpCloseDataConnection();
1091 
1092     if (!m_bBusy) {
1093         return true;
1094     }
1095 
1096     qCDebug(KIO_FTP) << "ftpCloseCommand: reading command result";
1097     m_bBusy = false;
1098 
1099     if (!ftpResponse(-1) || (m_iRespType != 2)) {
1100         qCDebug(KIO_FTP) << "ftpCloseCommand: no transfer complete message";
1101         return false;
1102     }
1103     return true;
1104 }
1105 
1106 Result FtpInternal::mkdir(const QUrl &url, int permissions)
1107 {
1108     auto result = ftpOpenConnection(LoginMode::Implicit);
1109     if (!result.success()) {
1110         return result;
1111     }
1112 
1113     const QByteArray encodedPath(q->remoteEncoding()->encode(url));
1114     const QString path = QString::fromLatin1(encodedPath.constData(), encodedPath.size());
1115 
1116     if (!ftpSendCmd((QByteArrayLiteral("mkd ") + encodedPath)) || (m_iRespType != 2)) {
1117         QString currentPath(m_currentPath);
1118 
1119         // Check whether or not mkdir failed because
1120         // the directory already exists...
1121         if (ftpFolder(path)) {
1122             const QString &failedPath = path;
1123             // Change the directory back to what it was...
1124             (void)ftpFolder(currentPath);
1125             return Result::fail(ERR_DIR_ALREADY_EXIST, failedPath);
1126         }
1127 
1128         return Result::fail(ERR_CANNOT_MKDIR, path);
1129     }
1130 
1131     if (permissions != -1) {
1132         // chmod the dir we just created, ignoring errors.
1133         (void)ftpChmod(path, permissions);
1134     }
1135 
1136     return Result::pass();
1137 }
1138 
1139 Result FtpInternal::rename(const QUrl &src, const QUrl &dst, KIO::JobFlags flags)
1140 {
1141     const auto result = ftpOpenConnection(LoginMode::Implicit);
1142     if (!result.success()) {
1143         return result;
1144     }
1145 
1146     // The actual functionality is in ftpRename because put needs it
1147     return ftpRename(src.path(), dst.path(), flags);
1148 }
1149 
1150 Result FtpInternal::ftpRename(const QString &src, const QString &dst, KIO::JobFlags jobFlags)
1151 {
1152     Q_ASSERT(m_bLoggedOn);
1153 
1154     // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793).
1155     if (!(jobFlags & KIO::Overwrite)) {
1156         if (ftpFileExists(dst)) {
1157             return Result::fail(ERR_FILE_ALREADY_EXIST, dst);
1158         }
1159     }
1160 
1161     if (ftpFolder(dst)) {
1162         return Result::fail(ERR_DIR_ALREADY_EXIST, dst);
1163     }
1164 
1165     // CD into parent folder
1166     const int pos = src.lastIndexOf(QLatin1Char('/'));
1167     if (pos >= 0) {
1168         if (!ftpFolder(src.left(pos + 1))) {
1169             return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, src);
1170         }
1171     }
1172 
1173     const QByteArray from_cmd = "RNFR " + q->remoteEncoding()->encode(src.mid(pos + 1));
1174     if (!ftpSendCmd(from_cmd) || (m_iRespType != 3)) {
1175         return Result::fail(ERR_CANNOT_RENAME, src);
1176     }
1177 
1178     const QByteArray to_cmd = "RNTO " + q->remoteEncoding()->encode(dst);
1179     if (!ftpSendCmd(to_cmd) || (m_iRespType != 2)) {
1180         return Result::fail(ERR_CANNOT_RENAME, src);
1181     }
1182 
1183     return Result::pass();
1184 }
1185 
1186 Result FtpInternal::del(const QUrl &url, bool isfile)
1187 {
1188     auto result = ftpOpenConnection(LoginMode::Implicit);
1189     if (!result.success()) {
1190         return result;
1191     }
1192 
1193     // When deleting a directory, we must exit from it first
1194     // The last command probably went into it (to stat it)
1195     if (!isfile) {
1196         (void)ftpFolder(q->remoteEncoding()->decode(q->remoteEncoding()->directory(url))); // ignore errors
1197     }
1198 
1199     const QByteArray cmd = (isfile ? "DELE " : "RMD ") + q->remoteEncoding()->encode(url);
1200 
1201     if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
1202         return Result::fail(ERR_CANNOT_DELETE, url.path());
1203     }
1204 
1205     return Result::pass();
1206 }
1207 
1208 bool FtpInternal::ftpChmod(const QString &path, int permissions)
1209 {
1210     Q_ASSERT(m_bLoggedOn);
1211 
1212     if (m_extControl & chmodUnknown) { // previous errors?
1213         return false;
1214     }
1215 
1216     // we need to do bit AND 777 to get permissions, in case
1217     // we were sent a full mode (unlikely)
1218     const QByteArray cmd = "SITE CHMOD " + QByteArray::number(permissions & 0777 /*octal*/, 8 /*octal*/) + ' ' + q->remoteEncoding()->encode(path);
1219 
1220     if (ftpSendCmd(cmd)) {
1221         qCDebug(KIO_FTP) << "ftpChmod: Failed to issue chmod";
1222         return false;
1223     }
1224 
1225     if (m_iRespType == 2) {
1226         return true;
1227     }
1228 
1229     if (m_iRespCode == 500) {
1230         m_extControl |= chmodUnknown;
1231         qCDebug(KIO_FTP) << "ftpChmod: CHMOD not supported - disabling";
1232     }
1233     return false;
1234 }
1235 
1236 Result FtpInternal::chmod(const QUrl &url, int permissions)
1237 {
1238     const auto result = ftpOpenConnection(LoginMode::Implicit);
1239     if (!result.success()) {
1240         return result;
1241     }
1242 
1243     if (!ftpChmod(url.path(), permissions)) {
1244         return Result::fail(ERR_CANNOT_CHMOD, url.path());
1245     }
1246 
1247     return Result::pass();
1248 }
1249 
1250 void FtpInternal::ftpCreateUDSEntry(const QString &filename, const FtpEntry &ftpEnt, UDSEntry &entry, bool isDir)
1251 {
1252     Q_ASSERT(entry.count() == 0); // by contract :-)
1253 
1254     entry.reserve(9);
1255     entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
1256     entry.fastInsert(KIO::UDSEntry::UDS_SIZE, ftpEnt.size);
1257     entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, ftpEnt.date.toSecsSinceEpoch());
1258     entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, ftpEnt.access);
1259     entry.fastInsert(KIO::UDSEntry::UDS_USER, ftpEnt.owner);
1260     if (!ftpEnt.group.isEmpty()) {
1261         entry.fastInsert(KIO::UDSEntry::UDS_GROUP, ftpEnt.group);
1262     }
1263 
1264     if (!ftpEnt.link.isEmpty()) {
1265         entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, ftpEnt.link);
1266 
1267         QMimeDatabase db;
1268         QMimeType mime = db.mimeTypeForUrl(QUrl(QLatin1String("ftp://host/") + filename));
1269         // Links on ftp sites are often links to dirs, and we have no way to check
1270         // that. Let's do like Netscape : assume dirs generally.
1271         // But we do this only when the MIME type can't be known from the filename.
1272         // --> we do better than Netscape :-)
1273         if (mime.isDefault()) {
1274             qCDebug(KIO_FTP) << "Setting guessed MIME type to inode/directory for " << filename;
1275             entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, QStringLiteral("inode/directory"));
1276             isDir = true;
1277         }
1278     }
1279 
1280     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : ftpEnt.type);
1281     // entry.insert KIO::UDSEntry::UDS_ACCESS_TIME,buff.st_atime);
1282     // entry.insert KIO::UDSEntry::UDS_CREATION_TIME,buff.st_ctime);
1283 }
1284 
1285 void FtpInternal::ftpShortStatAnswer(const QString &filename, bool isDir)
1286 {
1287     UDSEntry entry;
1288 
1289     entry.reserve(4);
1290     entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
1291     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : S_IFREG);
1292     entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1293     if (isDir) {
1294         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
1295     }
1296     // No details about size, ownership, group, etc.
1297 
1298     q->statEntry(entry);
1299 }
1300 
1301 Result FtpInternal::ftpStatAnswerNotFound(const QString &path, const QString &filename)
1302 {
1303     // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source")
1304     // When e.g. uploading a file, we still need stat() to return "not found"
1305     // when the file doesn't exist.
1306     QString statSide = q->metaData(QStringLiteral("statSide"));
1307     qCDebug(KIO_FTP) << "statSide=" << statSide;
1308     if (statSide == QLatin1String("source")) {
1309         qCDebug(KIO_FTP) << "Not found, but assuming found, because some servers don't allow listing";
1310         // MS Server is incapable of handling "list <blah>" in a case insensitive way
1311         // But "retr <blah>" works. So lie in stat(), to get going...
1312         //
1313         // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run
1314         // where listing permissions are denied, but downloading is still possible.
1315         ftpShortStatAnswer(filename, false /*file, not dir*/);
1316 
1317         return Result::pass();
1318     }
1319 
1320     return Result::fail(ERR_DOES_NOT_EXIST, path);
1321 }
1322 
1323 Result FtpInternal::stat(const QUrl &url)
1324 {
1325     qCDebug(KIO_FTP) << "path=" << url.path();
1326     auto result = ftpOpenConnection(LoginMode::Implicit);
1327     if (!result.success()) {
1328         return result;
1329     }
1330 
1331     const QString path = ftpCleanPath(QDir::cleanPath(url.path()));
1332     qCDebug(KIO_FTP) << "cleaned path=" << path;
1333 
1334     // We can't stat root, but we know it's a dir.
1335     if (path.isEmpty() || path == QLatin1String("/")) {
1336         UDSEntry entry;
1337         entry.reserve(6);
1338         // entry.insert( KIO::UDSEntry::UDS_NAME, UDSField( QString() ) );
1339         entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral("."));
1340         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1341         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
1342         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1343         entry.fastInsert(KIO::UDSEntry::UDS_USER, QStringLiteral("root"));
1344         entry.fastInsert(KIO::UDSEntry::UDS_GROUP, QStringLiteral("root"));
1345         // no size
1346 
1347         q->statEntry(entry);
1348         return Result::pass();
1349     }
1350 
1351     QUrl tempurl(url);
1352     tempurl.setPath(path); // take the clean one
1353     QString listarg; // = tempurl.directory(QUrl::ObeyTrailingSlash);
1354     QString parentDir;
1355     const QString filename = tempurl.fileName();
1356     Q_ASSERT(!filename.isEmpty());
1357 
1358     // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info)
1359     // if it doesn't work, it's a file (and then we'll use dir filename)
1360     bool isDir = ftpFolder(path);
1361 
1362     // if we're only interested in "file or directory", we should stop here
1363     QString sDetails = q->metaData(QStringLiteral("details"));
1364     int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
1365     qCDebug(KIO_FTP) << "details=" << details;
1366     if (details == 0) {
1367         if (!isDir && !ftpFileExists(path)) { // ok, not a dir -> is it a file ?
1368             // no -> it doesn't exist at all
1369             return ftpStatAnswerNotFound(path, filename);
1370         }
1371         ftpShortStatAnswer(filename, isDir);
1372         return Result::pass(); // successfully found a dir or a file -> done
1373     }
1374 
1375     if (!isDir) {
1376         // It is a file or it doesn't exist, try going to parent directory
1377         parentDir = tempurl.adjusted(QUrl::RemoveFilename).path();
1378         // With files we can do "LIST <filename>" to avoid listing the whole dir
1379         listarg = filename;
1380     } else {
1381         // --- New implementation:
1382         // Don't list the parent dir. Too slow, might not show it, etc.
1383         // Just return that it's a dir.
1384         UDSEntry entry;
1385         entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename);
1386         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1387         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1388         // No clue about size, ownership, group, etc.
1389 
1390         q->statEntry(entry);
1391         return Result::pass();
1392     }
1393 
1394     // Now cwd the parent dir, to prepare for listing
1395     if (!ftpFolder(parentDir)) {
1396         return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, parentDir);
1397     }
1398 
1399     result = ftpOpenCommand("list", listarg, 'I', ERR_DOES_NOT_EXIST);
1400     if (!result.success()) {
1401         qCritical() << "COULD NOT LIST";
1402         return result;
1403     }
1404     qCDebug(KIO_FTP) << "Starting of list was ok";
1405 
1406     Q_ASSERT(!filename.isEmpty() && filename != QLatin1String("/"));
1407 
1408     bool bFound = false;
1409     QUrl linkURL;
1410     FtpEntry ftpEnt;
1411     QList<FtpEntry> ftpValidateEntList;
1412     while (ftpReadDir(ftpEnt)) {
1413         if (!ftpEnt.name.isEmpty() && ftpEnt.name.at(0).isSpace()) {
1414             ftpValidateEntList.append(ftpEnt);
1415             continue;
1416         }
1417 
1418         // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at)
1419         // return only the filename when doing "dir /full/path/to/file"
1420         if (!bFound) {
1421             bFound = maybeEmitStatEntry(ftpEnt, filename, isDir);
1422         }
1423         qCDebug(KIO_FTP) << ftpEnt.name;
1424     }
1425 
1426     for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) {
1427         FtpEntry &ftpEnt = ftpValidateEntList[i];
1428         fixupEntryName(&ftpEnt);
1429         if (maybeEmitStatEntry(ftpEnt, filename, isDir)) {
1430             break;
1431         }
1432     }
1433 
1434     ftpCloseCommand(); // closes the data connection only
1435 
1436     if (!bFound) {
1437         return ftpStatAnswerNotFound(path, filename);
1438     }
1439 
1440     if (!linkURL.isEmpty()) {
1441         if (linkURL == url || linkURL == tempurl) {
1442             return Result::fail(ERR_CYCLIC_LINK, linkURL.toString());
1443         }
1444         return FtpInternal::stat(linkURL);
1445     }
1446 
1447     qCDebug(KIO_FTP) << "stat : finished successfully";
1448     ;
1449     return Result::pass();
1450 }
1451 
1452 bool FtpInternal::maybeEmitStatEntry(FtpEntry &ftpEnt, const QString &filename, bool isDir)
1453 {
1454     if (filename == ftpEnt.name && !filename.isEmpty()) {
1455         UDSEntry entry;
1456         ftpCreateUDSEntry(filename, ftpEnt, entry, isDir);
1457         q->statEntry(entry);
1458         return true;
1459     }
1460 
1461     return false;
1462 }
1463 
1464 Result FtpInternal::listDir(const QUrl &url)
1465 {
1466     qCDebug(KIO_FTP) << url;
1467     auto result = ftpOpenConnection(LoginMode::Implicit);
1468     if (!result.success()) {
1469         return result;
1470     }
1471 
1472     // No path specified ?
1473     QString path = url.path();
1474     if (path.isEmpty()) {
1475         QUrl realURL;
1476         realURL.setScheme(QStringLiteral("ftp"));
1477         realURL.setUserName(m_user);
1478         realURL.setPassword(m_pass);
1479         realURL.setHost(m_host);
1480         if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
1481             realURL.setPort(m_port);
1482         }
1483         if (m_initialPath.isEmpty()) {
1484             m_initialPath = QStringLiteral("/");
1485         }
1486         realURL.setPath(m_initialPath);
1487         qCDebug(KIO_FTP) << "REDIRECTION to " << realURL;
1488         q->redirection(realURL);
1489         return Result::pass();
1490     }
1491 
1492     qCDebug(KIO_FTP) << "hunting for path" << path;
1493 
1494     result = ftpOpenDir(path);
1495     if (!result.success()) {
1496         if (ftpFileExists(path)) {
1497             return Result::fail(ERR_IS_FILE, path);
1498         }
1499         // not sure which to emit
1500         // error( ERR_DOES_NOT_EXIST, path );
1501         return Result::fail(ERR_CANNOT_ENTER_DIRECTORY, path);
1502     }
1503 
1504     UDSEntry entry;
1505     FtpEntry ftpEnt;
1506     QList<FtpEntry> ftpValidateEntList;
1507     while (ftpReadDir(ftpEnt)) {
1508         qCDebug(KIO_FTP) << ftpEnt.name;
1509         // Q_ASSERT( !ftpEnt.name.isEmpty() );
1510         if (!ftpEnt.name.isEmpty()) {
1511             if (ftpEnt.name.at(0).isSpace()) {
1512                 ftpValidateEntList.append(ftpEnt);
1513                 continue;
1514             }
1515 
1516             // if ( S_ISDIR( (mode_t)ftpEnt.type ) )
1517             //   qDebug() << "is a dir";
1518             // if ( !ftpEnt.link.isEmpty() )
1519             //   qDebug() << "is a link to " << ftpEnt.link;
1520             ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false);
1521             q->listEntry(entry);
1522             entry.clear();
1523         }
1524     }
1525 
1526     for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) {
1527         FtpEntry &ftpEnt = ftpValidateEntList[i];
1528         fixupEntryName(&ftpEnt);
1529         ftpCreateUDSEntry(ftpEnt.name, ftpEnt, entry, false);
1530         q->listEntry(entry);
1531         entry.clear();
1532     }
1533 
1534     ftpCloseCommand(); // closes the data connection only
1535     return Result::pass();
1536 }
1537 
1538 void FtpInternal::worker_status()
1539 {
1540     qCDebug(KIO_FTP) << "Got worker_status host = " << (!m_host.toLatin1().isEmpty() ? m_host.toLatin1() : "[None]") << " ["
1541                      << (m_bLoggedOn ? "Connected" : "Not connected") << "]";
1542     q->workerStatus(m_host, m_bLoggedOn);
1543 }
1544 
1545 Result FtpInternal::ftpOpenDir(const QString &path)
1546 {
1547     // QString path( _url.path(QUrl::RemoveTrailingSlash) );
1548 
1549     // We try to change to this directory first to see whether it really is a directory.
1550     // (And also to follow symlinks)
1551     QString tmp = path.isEmpty() ? QStringLiteral("/") : path;
1552 
1553     // We get '550', whether it's a file or doesn't exist...
1554     if (!ftpFolder(tmp)) {
1555         return Result::fail();
1556     }
1557 
1558     // Don't use the path in the list command:
1559     // We changed into this directory anyway - so it's enough just to send "list".
1560     // We use '-a' because the application MAY be interested in dot files.
1561     // The only way to really know would be to have a metadata flag for this...
1562     // Since some windows ftp server seems not to support the -a argument, we use a fallback here.
1563     // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com)
1564     // Pass KJob::NoError first because we don't want to emit error before we
1565     // have tried all commands.
1566     auto result = ftpOpenCommand("list -la", QString(), 'I', KJob::NoError);
1567     if (!result.success()) {
1568         result = ftpOpenCommand("list", QString(), 'I', KJob::NoError);
1569     }
1570     if (!result.success()) {
1571         // Servers running with Turkish locale having problems converting 'i' letter to upper case.
1572         // So we send correct upper case command as last resort.
1573         result = ftpOpenCommand("LIST -la", QString(), 'I', ERR_CANNOT_ENTER_DIRECTORY);
1574     }
1575 
1576     if (!result.success()) {
1577         qCWarning(KIO_FTP) << "Can't open for listing";
1578         return result;
1579     }
1580 
1581     qCDebug(KIO_FTP) << "Starting of list was ok";
1582     return Result::pass();
1583 }
1584 
1585 bool FtpInternal::ftpReadDir(FtpEntry &de)
1586 {
1587     Q_ASSERT(m_data);
1588 
1589     // get a line from the data connection ...
1590     while (true) {
1591         while (!m_data->canReadLine() && m_data->waitForReadyRead((q->readTimeout() * 1000))) { }
1592         QByteArray data = m_data->readLine();
1593         if (data.size() == 0) {
1594             break;
1595         }
1596 
1597         const char *buffer = data.data();
1598         qCDebug(KIO_FTP) << "dir > " << buffer;
1599 
1600         // Normally the listing looks like
1601         // -rw-r--r--   1 dfaure   dfaure        102 Nov  9 12:30 log
1602         // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442)
1603         // d [RWCEAFMS] Admin                     512 Oct 13  2004 PSI
1604 
1605         // we should always get the following 5 fields ...
1606         const char *p_access;
1607         const char *p_junk;
1608         const char *p_owner;
1609         const char *p_group;
1610         const char *p_size;
1611         if ((p_access = strtok((char *)buffer, " ")) == nullptr) {
1612             continue;
1613         }
1614         if ((p_junk = strtok(nullptr, " ")) == nullptr) {
1615             continue;
1616         }
1617         if ((p_owner = strtok(nullptr, " ")) == nullptr) {
1618             continue;
1619         }
1620         if ((p_group = strtok(nullptr, " ")) == nullptr) {
1621             continue;
1622         }
1623         if ((p_size = strtok(nullptr, " ")) == nullptr) {
1624             continue;
1625         }
1626 
1627         qCDebug(KIO_FTP) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size;
1628 
1629         de.access = 0;
1630         if (qstrlen(p_access) == 1 && p_junk[0] == '[') { // Netware
1631             de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions
1632         }
1633 
1634         const char *p_date_1;
1635         const char *p_date_2;
1636         const char *p_date_3;
1637         const char *p_name;
1638 
1639         // A special hack for "/dev". A listing may look like this:
1640         // crw-rw-rw-   1 root     root       1,   5 Jun 29  1997 zero
1641         // So we just ignore the number in front of the ",". Ok, it is a hack :-)
1642         if (strchr(p_size, ',') != nullptr) {
1643             qCDebug(KIO_FTP) << "Size contains a ',' -> reading size again (/dev hack)";
1644             if ((p_size = strtok(nullptr, " ")) == nullptr) {
1645                 continue;
1646             }
1647         }
1648 
1649         // This is needed for ftp servers with a directory listing like this (#375610):
1650         // drwxr-xr-x               folder        0 Mar 15 15:50 directory_name
1651         if (strcmp(p_junk, "folder") == 0) {
1652             p_date_1 = p_group;
1653             p_date_2 = p_size;
1654             p_size = p_owner;
1655             p_group = nullptr;
1656             p_owner = nullptr;
1657         }
1658         // Check whether the size we just read was really the size
1659         // or a month (this happens when the server lists no group)
1660         // Used to be the case on sunsite.uio.no, but not anymore
1661         // This is needed for the Netware case, too.
1662         else if (!isdigit(*p_size)) {
1663             p_date_1 = p_size;
1664             p_date_2 = strtok(nullptr, " ");
1665             p_size = p_group;
1666             p_group = nullptr;
1667             qCDebug(KIO_FTP) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1;
1668         } else {
1669             p_date_1 = strtok(nullptr, " ");
1670             p_date_2 = strtok(nullptr, " ");
1671             qCDebug(KIO_FTP) << "Size has a digit -> ok. p_date_1=" << p_date_1;
1672         }
1673 
1674         if (p_date_1 != nullptr && p_date_2 != nullptr && (p_date_3 = strtok(nullptr, " ")) != nullptr && (p_name = strtok(nullptr, "\r\n")) != nullptr) {
1675             {
1676                 QByteArray tmp(p_name);
1677                 if (p_access[0] == 'l') {
1678                     int i = tmp.lastIndexOf(" -> ");
1679                     if (i != -1) {
1680                         de.link = q->remoteEncoding()->decode(p_name + i + 4);
1681                         tmp.truncate(i);
1682                     } else {
1683                         de.link.clear();
1684                     }
1685                 } else {
1686                     de.link.clear();
1687                 }
1688 
1689                 if (tmp.startsWith('/')) { // listing on ftp://ftp.gnupg.org/ starts with '/'
1690                     tmp.remove(0, 1);
1691                 }
1692 
1693                 if (tmp.indexOf('/') != -1) {
1694                     continue; // Don't trick us!
1695                 }
1696 
1697                 de.name = q->remoteEncoding()->decode(tmp);
1698             }
1699 
1700             de.type = S_IFREG;
1701             switch (p_access[0]) {
1702             case 'd':
1703                 de.type = S_IFDIR;
1704                 break;
1705             case 's':
1706                 de.type = S_IFSOCK;
1707                 break;
1708             case 'b':
1709                 de.type = S_IFBLK;
1710                 break;
1711             case 'c':
1712                 de.type = S_IFCHR;
1713                 break;
1714             case 'l':
1715                 de.type = S_IFREG;
1716                 // we don't set S_IFLNK here.  de.link says it.
1717                 break;
1718             default:
1719                 break;
1720             }
1721 
1722             if (p_access[1] == 'r') {
1723                 de.access |= S_IRUSR;
1724             }
1725             if (p_access[2] == 'w') {
1726                 de.access |= S_IWUSR;
1727             }
1728             if (p_access[3] == 'x' || p_access[3] == 's') {
1729                 de.access |= S_IXUSR;
1730             }
1731             if (p_access[4] == 'r') {
1732                 de.access |= S_IRGRP;
1733             }
1734             if (p_access[5] == 'w') {
1735                 de.access |= S_IWGRP;
1736             }
1737             if (p_access[6] == 'x' || p_access[6] == 's') {
1738                 de.access |= S_IXGRP;
1739             }
1740             if (p_access[7] == 'r') {
1741                 de.access |= S_IROTH;
1742             }
1743             if (p_access[8] == 'w') {
1744                 de.access |= S_IWOTH;
1745             }
1746             if (p_access[9] == 'x' || p_access[9] == 't') {
1747                 de.access |= S_IXOTH;
1748             }
1749             if (p_access[3] == 's' || p_access[3] == 'S') {
1750                 de.access |= S_ISUID;
1751             }
1752             if (p_access[6] == 's' || p_access[6] == 'S') {
1753                 de.access |= S_ISGID;
1754             }
1755             if (p_access[9] == 't' || p_access[9] == 'T') {
1756                 de.access |= S_ISVTX;
1757             }
1758 
1759             de.owner = q->remoteEncoding()->decode(p_owner);
1760             de.group = q->remoteEncoding()->decode(p_group);
1761             de.size = charToLongLong(p_size);
1762 
1763             // Parsing the date is somewhat tricky
1764             // Examples : "Oct  6 22:49", "May 13  1999"
1765 
1766             // First get current date - we need the current month and year
1767             QDate currentDate(QDate::currentDate());
1768             int currentMonth = currentDate.month();
1769             int day = currentDate.day();
1770             int month = currentDate.month();
1771             int year = currentDate.year();
1772             int minute = 0;
1773             int hour = 0;
1774             // Get day number (always second field)
1775             if (p_date_2) {
1776                 day = atoi(p_date_2);
1777             }
1778             // Get month from first field
1779             // NOTE : no, we don't want to use KLocale here
1780             // It seems all FTP servers use the English way
1781             qCDebug(KIO_FTP) << "Looking for month " << p_date_1;
1782             static const char s_months[][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
1783             for (int c = 0; c < 12; c++) {
1784                 if (!qstrcmp(p_date_1, s_months[c])) {
1785                     qCDebug(KIO_FTP) << "Found month " << c << " for " << p_date_1;
1786                     month = c + 1;
1787                     break;
1788                 }
1789             }
1790 
1791             // Parse third field
1792             if (p_date_3 && !strchr(p_date_3, ':')) { // No colon, looks like a year
1793                 year = atoi(p_date_3);
1794             } else {
1795                 // otherwise, the year is implicit
1796                 // according to man ls, this happens when it is between than 6 months
1797                 // old and 1 hour in the future.
1798                 // So the year is : current year if tm_mon <= currentMonth+1
1799                 // otherwise current year minus one
1800                 // (The +1 is a security for the "+1 hour" at the end of the month issue)
1801                 if (month > currentMonth + 1) {
1802                     year--;
1803                 }
1804 
1805                 // and p_date_3 contains probably a time
1806                 char *semicolon;
1807                 if (p_date_3 && (semicolon = (char *)strchr(p_date_3, ':'))) {
1808                     *semicolon = '\0';
1809                     minute = atoi(semicolon + 1);
1810                     hour = atoi(p_date_3);
1811                 } else {
1812                     qCWarning(KIO_FTP) << "Can't parse third field " << p_date_3;
1813                 }
1814             }
1815 
1816             de.date = QDateTime(QDate(year, month, day), QTime(hour, minute));
1817             qCDebug(KIO_FTP) << de.date;
1818             return true;
1819         }
1820     } // line invalid, loop to get another line
1821     return false;
1822 }
1823 
1824 //===============================================================================
1825 // public: get           download file from server
1826 // helper: ftpGet        called from get() and copy()
1827 //===============================================================================
1828 Result FtpInternal::get(const QUrl &url)
1829 {
1830     qCDebug(KIO_FTP) << url;
1831     const Result result = ftpGet(-1, QString(), url, 0);
1832     ftpCloseCommand(); // must close command!
1833     return result;
1834 }
1835 
1836 Result FtpInternal::ftpGet(int iCopyFile, const QString &sCopyFile, const QUrl &url, KIO::fileoffset_t llOffset)
1837 {
1838     auto result = ftpOpenConnection(LoginMode::Implicit);
1839     if (!result.success()) {
1840         return result;
1841     }
1842 
1843     // Try to find the size of the file (and check that it exists at
1844     // the same time). If we get back a 550, "File does not exist"
1845     // or "not a plain file", check if it is a directory. If it is a
1846     // directory, return an error; otherwise simply try to retrieve
1847     // the request...
1848     if (!ftpSize(url.path(), '?') && (m_iRespCode == 550) && ftpFolder(url.path())) {
1849         // Ok it's a dir in fact
1850         qCDebug(KIO_FTP) << "it is a directory in fact";
1851         return Result::fail(ERR_IS_DIRECTORY);
1852     }
1853 
1854     QString resumeOffset = q->metaData(QStringLiteral("range-start"));
1855     if (resumeOffset.isEmpty()) {
1856         resumeOffset = q->metaData(QStringLiteral("resume")); // old name
1857     }
1858     if (!resumeOffset.isEmpty()) {
1859         llOffset = resumeOffset.toLongLong();
1860         qCDebug(KIO_FTP) << "got offset from metadata : " << llOffset;
1861     }
1862 
1863     result = ftpOpenCommand("retr", url.path(), '?', ERR_CANNOT_OPEN_FOR_READING, llOffset);
1864     if (!result.success()) {
1865         qCWarning(KIO_FTP) << "Can't open for reading";
1866         return result;
1867     }
1868 
1869     // Read the size from the response string
1870     if (m_size == UnknownSize) {
1871         const char *psz = strrchr(ftpResponse(4), '(');
1872         if (psz) {
1873             m_size = charToLongLong(psz + 1);
1874         }
1875         if (!m_size) {
1876             m_size = UnknownSize;
1877         }
1878     }
1879 
1880     // Send the MIME type...
1881     if (iCopyFile == -1) {
1882         const auto result = ftpSendMimeType(url);
1883         if (!result.success()) {
1884             return result;
1885         }
1886     }
1887 
1888     KIO::filesize_t bytesLeft = 0;
1889     if (m_size != UnknownSize) {
1890         bytesLeft = m_size - llOffset;
1891         q->totalSize(m_size); // emit the total size...
1892     }
1893 
1894     qCDebug(KIO_FTP) << "starting with offset=" << llOffset;
1895     KIO::fileoffset_t processed_size = llOffset;
1896 
1897     QByteArray array;
1898     char buffer[maximumIpcSize];
1899     // start with small data chunks in case of a slow data source (modem)
1900     // - unfortunately this has a negative impact on performance for large
1901     // - files - so we will increase the block size after a while ...
1902     int iBlockSize = initialIpcSize;
1903     int iBufferCur = 0;
1904 
1905     while (m_size == UnknownSize || bytesLeft > 0) {
1906         // let the buffer size grow if the file is larger 64kByte ...
1907         if (processed_size - llOffset > 1024 * 64) {
1908             iBlockSize = maximumIpcSize;
1909         }
1910 
1911         // read the data and detect EOF or error ...
1912         if (iBlockSize + iBufferCur > (int)sizeof(buffer)) {
1913             iBlockSize = sizeof(buffer) - iBufferCur;
1914         }
1915         if (m_data->bytesAvailable() == 0) {
1916             m_data->waitForReadyRead((q->readTimeout() * 1000));
1917         }
1918         int n = m_data->read(buffer + iBufferCur, iBlockSize);
1919         if (n <= 0) {
1920             // this is how we detect EOF in case of unknown size
1921             if (m_size == UnknownSize && n == 0) {
1922                 break;
1923             }
1924             // unexpected eof. Happens when the daemon gets killed.
1925             return Result::fail(ERR_CANNOT_READ);
1926         }
1927         processed_size += n;
1928 
1929         // collect very small data chunks in buffer before processing ...
1930         if (m_size != UnknownSize) {
1931             bytesLeft -= n;
1932             iBufferCur += n;
1933             if (iBufferCur < minimumMimeSize && bytesLeft > 0) {
1934                 q->processedSize(processed_size);
1935                 continue;
1936             }
1937             n = iBufferCur;
1938             iBufferCur = 0;
1939         }
1940 
1941         // write output file or pass to data pump ...
1942         int writeError = 0;
1943         if (iCopyFile == -1) {
1944             array = QByteArray::fromRawData(buffer, n);
1945             q->data(array);
1946             array.clear();
1947         } else if ((writeError = WriteToFile(iCopyFile, buffer, n)) != 0) {
1948             return Result::fail(writeError, sCopyFile);
1949         }
1950 
1951         Q_ASSERT(processed_size >= 0);
1952         q->processedSize(static_cast<KIO::filesize_t>(processed_size));
1953     }
1954 
1955     qCDebug(KIO_FTP) << "done";
1956     if (iCopyFile == -1) { // must signal EOF to data pump ...
1957         q->data(array); // array is empty and must be empty!
1958     }
1959 
1960     q->processedSize(m_size == UnknownSize ? processed_size : m_size);
1961     return Result::pass();
1962 }
1963 
1964 //===============================================================================
1965 // public: put           upload file to server
1966 // helper: ftpPut        called from put() and copy()
1967 //===============================================================================
1968 Result FtpInternal::put(const QUrl &url, int permissions, KIO::JobFlags flags)
1969 {
1970     qCDebug(KIO_FTP) << url;
1971     const auto result = ftpPut(-1, url, permissions, flags);
1972     ftpCloseCommand(); // must close command!
1973     return result;
1974 }
1975 
1976 Result FtpInternal::ftpPut(int iCopyFile, const QUrl &dest_url, int permissions, KIO::JobFlags flags)
1977 {
1978     const auto openResult = ftpOpenConnection(LoginMode::Implicit);
1979     if (!openResult.success()) {
1980         return openResult;
1981     }
1982 
1983     // Don't use mark partial over anonymous FTP.
1984     // My incoming dir allows put but not rename...
1985     bool bMarkPartial;
1986     if (m_user.isEmpty() || m_user == QLatin1String(s_ftpLogin)) {
1987         bMarkPartial = false;
1988     } else {
1989         bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true);
1990     }
1991 
1992     QString dest_orig = dest_url.path();
1993     const QString dest_part = dest_orig + QLatin1String(".part");
1994 
1995     if (ftpSize(dest_orig, 'I')) {
1996         if (m_size == 0) {
1997             // delete files with zero size
1998             const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_orig);
1999             if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
2000                 return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString());
2001             }
2002         } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
2003             return Result::fail(ERR_FILE_ALREADY_EXIST, QString());
2004         } else if (bMarkPartial) {
2005             // when using mark partial, append .part extension
2006             const auto result = ftpRename(dest_orig, dest_part, KIO::Overwrite);
2007             if (!result.success()) {
2008                 return Result::fail(ERR_CANNOT_RENAME_PARTIAL, QString());
2009             }
2010         }
2011         // Don't chmod an existing file
2012         permissions = -1;
2013     } else if (bMarkPartial && ftpSize(dest_part, 'I')) {
2014         // file with extension .part exists
2015         if (m_size == 0) {
2016             // delete files with zero size
2017             const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest_part);
2018             if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
2019                 return Result::fail(ERR_CANNOT_DELETE_PARTIAL, QString());
2020             }
2021         } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
2022             flags |= q->canResume(m_size) ? KIO::Resume : KIO::DefaultFlags;
2023             if (!(flags & KIO::Resume)) {
2024                 return Result::fail(ERR_FILE_ALREADY_EXIST, QString());
2025             }
2026         }
2027     } else {
2028         m_size = 0;
2029     }
2030 
2031     QString dest;
2032 
2033     // if we are using marking of partial downloads -> add .part extension
2034     if (bMarkPartial) {
2035         qCDebug(KIO_FTP) << "Adding .part extension to " << dest_orig;
2036         dest = dest_part;
2037     } else {
2038         dest = dest_orig;
2039     }
2040 
2041     KIO::fileoffset_t offset = 0;
2042 
2043     // set the mode according to offset
2044     if ((flags & KIO::Resume) && m_size > 0) {
2045         offset = m_size;
2046         if (iCopyFile != -1) {
2047             if (QT_LSEEK(iCopyFile, offset, SEEK_SET) < 0) {
2048                 return Result::fail(ERR_CANNOT_RESUME, QString());
2049             }
2050         }
2051     }
2052 
2053     const auto storResult = ftpOpenCommand("stor", dest, '?', ERR_CANNOT_WRITE, offset);
2054     if (!storResult.success()) {
2055         return storResult;
2056     }
2057 
2058     qCDebug(KIO_FTP) << "ftpPut: starting with offset=" << offset;
2059     KIO::fileoffset_t processed_size = offset;
2060 
2061     QByteArray buffer;
2062     int result;
2063     int iBlockSize = initialIpcSize;
2064     int writeError = 0;
2065     // Loop until we got 'dataEnd'
2066     do {
2067         if (iCopyFile == -1) {
2068             q->dataReq(); // Request for data
2069             result = q->readData(buffer);
2070         } else {
2071             // let the buffer size grow if the file is larger 64kByte ...
2072             if (processed_size - offset > 1024 * 64) {
2073                 iBlockSize = maximumIpcSize;
2074             }
2075             buffer.resize(iBlockSize);
2076             result = QT_READ(iCopyFile, buffer.data(), buffer.size());
2077             if (result < 0) {
2078                 writeError = ERR_CANNOT_READ;
2079             } else {
2080                 buffer.resize(result);
2081             }
2082         }
2083 
2084         if (result > 0) {
2085             m_data->write(buffer);
2086             while (m_data->bytesToWrite() && m_data->waitForBytesWritten()) { }
2087             processed_size += result;
2088             q->processedSize(processed_size);
2089         }
2090     } while (result > 0);
2091 
2092     if (result != 0) { // error
2093         ftpCloseCommand(); // don't care about errors
2094         qCDebug(KIO_FTP) << "Error during 'put'. Aborting.";
2095         if (bMarkPartial) {
2096             // Remove if smaller than minimum size
2097             if (ftpSize(dest, 'I') && (processed_size < q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE))) {
2098                 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(dest);
2099                 (void)ftpSendCmd(cmd);
2100             }
2101         }
2102         return Result::fail(writeError, dest_url.toString());
2103     }
2104 
2105     if (!ftpCloseCommand()) {
2106         return Result::fail(ERR_CANNOT_WRITE);
2107     }
2108 
2109     // after full download rename the file back to original name
2110     if (bMarkPartial) {
2111         qCDebug(KIO_FTP) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")";
2112         const auto result = ftpRename(dest, dest_orig, KIO::Overwrite);
2113         if (!result.success()) {
2114             return Result::fail(ERR_CANNOT_RENAME_PARTIAL);
2115         }
2116     }
2117 
2118     // set final permissions
2119     if (permissions != -1) {
2120         if (m_user == QLatin1String(s_ftpLogin)) {
2121             qCDebug(KIO_FTP) << "Trying to chmod over anonymous FTP ???";
2122         }
2123         // chmod the file we just put
2124         if (!ftpChmod(dest_orig, permissions)) {
2125             // To be tested
2126             // if ( m_user != s_ftpLogin )
2127             //    warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) );
2128         }
2129     }
2130 
2131     return Result::pass();
2132 }
2133 
2134 /** Use the SIZE command to get the file size.
2135     Warning : the size depends on the transfer mode, hence the second arg. */
2136 bool FtpInternal::ftpSize(const QString &path, char mode)
2137 {
2138     m_size = UnknownSize;
2139     if (!ftpDataMode(mode)) {
2140         return false;
2141     }
2142 
2143     const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path);
2144     if (!ftpSendCmd(buf) || (m_iRespType != 2)) {
2145         return false;
2146     }
2147 
2148     // skip leading "213 " (response code)
2149     QByteArray psz(ftpResponse(4));
2150     if (psz.isEmpty()) {
2151         return false;
2152     }
2153     bool ok = false;
2154     m_size = psz.trimmed().toLongLong(&ok);
2155     if (!ok) {
2156         m_size = UnknownSize;
2157     }
2158     return true;
2159 }
2160 
2161 bool FtpInternal::ftpFileExists(const QString &path)
2162 {
2163     const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(path);
2164     if (!ftpSendCmd(buf) || (m_iRespType != 2)) {
2165         return false;
2166     }
2167 
2168     // skip leading "213 " (response code)
2169     const char *psz = ftpResponse(4);
2170     return psz != nullptr;
2171 }
2172 
2173 // Today the differences between ASCII and BINARY are limited to
2174 // CR or CR/LF line terminators. Many servers ignore ASCII (like
2175 // win2003 -or- vsftp with default config). In the early days of
2176 // computing, when even text-files had structure, this stuff was
2177 // more important.
2178 // Theoretically "list" could return different results in ASCII
2179 // and BINARY mode. But again, most servers ignore ASCII here.
2180 bool FtpInternal::ftpDataMode(char cMode)
2181 {
2182     if (cMode == '?') {
2183         cMode = m_bTextMode ? 'A' : 'I';
2184     } else if (cMode == 'a') {
2185         cMode = 'A';
2186     } else if (cMode != 'A') {
2187         cMode = 'I';
2188     }
2189 
2190     qCDebug(KIO_FTP) << "want" << cMode << "has" << m_cDataMode;
2191     if (m_cDataMode == cMode) {
2192         return true;
2193     }
2194 
2195     const QByteArray buf = QByteArrayLiteral("TYPE ") + cMode;
2196     if (!ftpSendCmd(buf) || (m_iRespType != 2)) {
2197         return false;
2198     }
2199     m_cDataMode = cMode;
2200     return true;
2201 }
2202 
2203 bool FtpInternal::ftpFolder(const QString &path)
2204 {
2205     QString newPath = path;
2206     int iLen = newPath.length();
2207     if (iLen > 1 && newPath[iLen - 1] == QLatin1Char('/')) {
2208         newPath.chop(1);
2209     }
2210 
2211     qCDebug(KIO_FTP) << "want" << newPath << "has" << m_currentPath;
2212     if (m_currentPath == newPath) {
2213         return true;
2214     }
2215 
2216     const QByteArray tmp = "cwd " + q->remoteEncoding()->encode(newPath);
2217     if (!ftpSendCmd(tmp)) {
2218         return false; // connection failure
2219     }
2220     if (m_iRespType != 2) {
2221         return false; // not a folder
2222     }
2223     m_currentPath = newPath;
2224     return true;
2225 }
2226 
2227 //===============================================================================
2228 // public: copy          don't use kio data pump if one side is a local file
2229 // helper: ftpCopyPut    called from copy() on upload
2230 // helper: ftpCopyGet    called from copy() on download
2231 //===============================================================================
2232 Result FtpInternal::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags)
2233 {
2234     int iCopyFile = -1;
2235     bool bSrcLocal = src.isLocalFile();
2236     bool bDestLocal = dest.isLocalFile();
2237     QString sCopyFile;
2238 
2239     Result result = Result::pass();
2240     if (bSrcLocal && !bDestLocal) { // File -> Ftp
2241         sCopyFile = src.toLocalFile();
2242         qCDebug(KIO_FTP) << "local file" << sCopyFile << "-> ftp" << dest.path();
2243         result = ftpCopyPut(iCopyFile, sCopyFile, dest, permissions, flags);
2244     } else if (!bSrcLocal && bDestLocal) { // Ftp -> File
2245         sCopyFile = dest.toLocalFile();
2246         qCDebug(KIO_FTP) << "ftp" << src.path() << "-> local file" << sCopyFile;
2247         result = ftpCopyGet(iCopyFile, sCopyFile, src, permissions, flags);
2248     } else {
2249         return Result::fail(ERR_UNSUPPORTED_ACTION, QString());
2250     }
2251 
2252     // perform clean-ups and report error (if any)
2253     if (iCopyFile != -1) {
2254         QT_CLOSE(iCopyFile);
2255     }
2256     ftpCloseCommand(); // must close command!
2257 
2258     return result;
2259 }
2260 
2261 bool FtpInternal::isSocksProxyScheme(const QString &scheme)
2262 {
2263     return scheme == QLatin1String("socks") || scheme == QLatin1String("socks5");
2264 }
2265 
2266 bool FtpInternal::isSocksProxy() const
2267 {
2268     return isSocksProxyScheme(m_proxyURL.scheme());
2269 }
2270 
2271 Result FtpInternal::ftpCopyPut(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags)
2272 {
2273     // check if source is ok ...
2274     QFileInfo info(sCopyFile);
2275     bool bSrcExists = info.exists();
2276     if (bSrcExists) {
2277         if (info.isDir()) {
2278             return Result::fail(ERR_IS_DIRECTORY);
2279         }
2280     } else {
2281         return Result::fail(ERR_DOES_NOT_EXIST);
2282     }
2283 
2284     iCopyFile = QT_OPEN(QFile::encodeName(sCopyFile).constData(), O_RDONLY);
2285     if (iCopyFile == -1) {
2286         return Result::fail(ERR_CANNOT_OPEN_FOR_READING);
2287     }
2288 
2289     // delegate the real work (iError gets status) ...
2290     q->totalSize(info.size());
2291     if (s_enableCanResume) {
2292         return ftpPut(iCopyFile, url, permissions, flags & ~KIO::Resume);
2293     } else {
2294         return ftpPut(iCopyFile, url, permissions, flags | KIO::Resume);
2295     }
2296 }
2297 
2298 Result FtpInternal::ftpCopyGet(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags)
2299 {
2300     // check if destination is ok ...
2301     QFileInfo info(sCopyFile);
2302     const bool bDestExists = info.exists();
2303     if (bDestExists) {
2304         if (info.isDir()) {
2305             return Result::fail(ERR_IS_DIRECTORY);
2306         }
2307         if (!(flags & KIO::Overwrite)) {
2308             return Result::fail(ERR_FILE_ALREADY_EXIST);
2309         }
2310     }
2311 
2312     // do we have a ".part" file?
2313     const QString sPart = sCopyFile + QLatin1String(".part");
2314     bool bResume = false;
2315     QFileInfo sPartInfo(sPart);
2316     const bool bPartExists = sPartInfo.exists();
2317     const bool bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), true);
2318     const QString dest = bMarkPartial ? sPart : sCopyFile;
2319     if (bMarkPartial && bPartExists && sPartInfo.size() > 0) {
2320         // must not be a folder! please fix a similar bug in kio_file!!
2321         if (sPartInfo.isDir()) {
2322             return Result::fail(ERR_DIR_ALREADY_EXIST);
2323         }
2324         // doesn't work for copy? -> design flaw?
2325         bResume = s_enableCanResume ? q->canResume(sPartInfo.size()) : true;
2326     }
2327 
2328     if (bPartExists && !bResume) { // get rid of an unwanted ".part" file
2329         QFile::remove(sPart);
2330     }
2331 
2332     // WABA: Make sure that we keep writing permissions ourselves,
2333     // otherwise we can be in for a surprise on NFS.
2334     mode_t initialMode;
2335     if (permissions >= 0) {
2336         initialMode = static_cast<mode_t>(permissions | S_IWUSR);
2337     } else {
2338         initialMode = 0666;
2339     }
2340 
2341     // open the output file ...
2342     KIO::fileoffset_t hCopyOffset = 0;
2343     if (bResume) {
2344         iCopyFile = QT_OPEN(QFile::encodeName(sPart).constData(), O_RDWR); // append if resuming
2345         hCopyOffset = QT_LSEEK(iCopyFile, 0, SEEK_END);
2346         if (hCopyOffset < 0) {
2347             return Result::fail(ERR_CANNOT_RESUME);
2348         }
2349         qCDebug(KIO_FTP) << "resuming at " << hCopyOffset;
2350     } else {
2351         iCopyFile = QT_OPEN(QFile::encodeName(dest).constData(), O_CREAT | O_TRUNC | O_WRONLY, initialMode);
2352     }
2353 
2354     if (iCopyFile == -1) {
2355         qCDebug(KIO_FTP) << "### COULD NOT WRITE " << sCopyFile;
2356         const int error = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING;
2357         return Result::fail(error);
2358     }
2359 
2360     // delegate the real work (iError gets status) ...
2361     auto result = ftpGet(iCopyFile, sCopyFile, url, hCopyOffset);
2362 
2363     if (QT_CLOSE(iCopyFile) == 0 && !result.success()) {
2364         // If closing the file failed but there isn't an error yet, switch
2365         // into an error!
2366         result = Result::fail(ERR_CANNOT_WRITE);
2367     }
2368     iCopyFile = -1;
2369 
2370     // handle renaming or deletion of a partial file ...
2371     if (bMarkPartial) {
2372         if (result.success()) {
2373             // rename ".part" on success
2374             if (!QFile::rename(sPart, sCopyFile)) {
2375                 // If rename fails, try removing the destination first if it exists.
2376                 if (!bDestExists || !(QFile::remove(sCopyFile) && QFile::rename(sPart, sCopyFile))) {
2377                     qCDebug(KIO_FTP) << "cannot rename " << sPart << " to " << sCopyFile;
2378                     result = Result::fail(ERR_CANNOT_RENAME_PARTIAL);
2379                 }
2380             }
2381         } else {
2382             sPartInfo.refresh();
2383             if (sPartInfo.exists()) { // should a very small ".part" be deleted?
2384                 int size = q->configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE);
2385                 if (sPartInfo.size() < size) {
2386                     QFile::remove(sPart);
2387                 }
2388             }
2389         }
2390     }
2391 
2392     if (result.success()) {
2393         const QString mtimeStr = q->metaData(QStringLiteral("modified"));
2394         if (!mtimeStr.isEmpty()) {
2395             QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate);
2396             if (dt.isValid()) {
2397                 qCDebug(KIO_FTP) << "Updating modified timestamp to" << mtimeStr;
2398                 struct utimbuf utbuf;
2399                 info.refresh();
2400                 utbuf.actime = info.lastRead().toSecsSinceEpoch(); // access time, unchanged
2401                 utbuf.modtime = dt.toSecsSinceEpoch(); // modification time
2402                 ::utime(QFile::encodeName(sCopyFile).constData(), &utbuf);
2403             }
2404         }
2405     }
2406 
2407     return result;
2408 }
2409 
2410 Result FtpInternal::ftpSendMimeType(const QUrl &url)
2411 {
2412     const int totalSize = ((m_size == UnknownSize || m_size > 1024) ? 1024 : static_cast<int>(m_size));
2413     QByteArray buffer(totalSize, '\0');
2414 
2415     while (true) {
2416         // Wait for content to be available...
2417         if (m_data->bytesAvailable() == 0 && !m_data->waitForReadyRead((q->readTimeout() * 1000))) {
2418             return Result::fail(ERR_CANNOT_READ, url.toString());
2419         }
2420 
2421         const qint64 bytesRead = m_data->peek(buffer.data(), totalSize);
2422 
2423         // If we got a -1, it must be an error so return an error.
2424         if (bytesRead == -1) {
2425             return Result::fail(ERR_CANNOT_READ, url.toString());
2426         }
2427 
2428         // If m_size is unknown, peek returns 0 (0 sized file ??), or peek returns size
2429         // equal to the size we want, then break.
2430         if (bytesRead == 0 || bytesRead == totalSize || m_size == UnknownSize) {
2431             break;
2432         }
2433     }
2434 
2435     if (!buffer.isEmpty()) {
2436         QMimeDatabase db;
2437         QMimeType mime = db.mimeTypeForFileNameAndData(url.path(), buffer);
2438         qCDebug(KIO_FTP) << "Emitting MIME type" << mime.name();
2439         q->mimeType(mime.name()); // emit the MIME type...
2440     }
2441 
2442     return Result::pass();
2443 }
2444 
2445 void FtpInternal::fixupEntryName(FtpEntry *e)
2446 {
2447     Q_ASSERT(e);
2448     if (e->type == S_IFDIR) {
2449         if (!ftpFolder(e->name)) {
2450             QString name(e->name.trimmed());
2451             if (ftpFolder(name)) {
2452                 e->name = name;
2453                 qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name;
2454             } else {
2455                 int index = 0;
2456                 while (e->name.at(index).isSpace()) {
2457                     index++;
2458                     name = e->name.mid(index);
2459                     if (ftpFolder(name)) {
2460                         qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name;
2461                         e->name = name;
2462                         break;
2463                     }
2464                 }
2465             }
2466         }
2467     } else {
2468         if (!ftpFileExists(e->name)) {
2469             QString name(e->name.trimmed());
2470             if (ftpFileExists(name)) {
2471                 e->name = name;
2472                 qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name;
2473             } else {
2474                 int index = 0;
2475                 while (e->name.at(index).isSpace()) {
2476                     index++;
2477                     name = e->name.mid(index);
2478                     if (ftpFileExists(name)) {
2479                         qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name;
2480                         e->name = name;
2481                         break;
2482                     }
2483                 }
2484             }
2485         }
2486     }
2487 }
2488 
2489 ConnectionResult FtpInternal::synchronousConnectToHost(const QString &host, quint16 port)
2490 {
2491     const QUrl proxyUrl = m_proxyURL;
2492     QNetworkProxy proxy;
2493     if (!proxyUrl.isEmpty()) {
2494         proxy = QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyUrl.host(), static_cast<quint16>(proxyUrl.port(0)), proxyUrl.userName(), proxyUrl.password());
2495     }
2496 
2497     QTcpSocket *socket = new QSslSocket;
2498     socket->setProxy(proxy);
2499     socket->connectToHost(host, port);
2500     socket->waitForConnected(q->connectTimeout() * 1000);
2501     const auto socketError = socket->error();
2502     if (socketError == QAbstractSocket::ProxyAuthenticationRequiredError) {
2503         AuthInfo info;
2504         info.url = proxyUrl;
2505         info.verifyPath = true; // ### whatever
2506 
2507         if (!q->checkCachedAuthentication(info)) {
2508             info.prompt = i18n(
2509                 "You need to supply a username and a password for "
2510                 "the proxy server listed below before you are allowed "
2511                 "to access any sites.");
2512             info.keepPassword = true;
2513             info.commentLabel = i18n("Proxy:");
2514             info.comment = i18n("<b>%1</b>", proxy.hostName());
2515 
2516             const int errorCode = q->openPasswordDialog(info, i18n("Proxy Authentication Failed."));
2517             if (errorCode != KJob::NoError) {
2518                 qCDebug(KIO_FTP) << "user canceled proxy authentication, or communication error." << errorCode;
2519                 return ConnectionResult{socket, Result::fail(errorCode, proxyUrl.toString())};
2520             }
2521         }
2522 
2523         proxy.setUser(info.username);
2524         proxy.setPassword(info.password);
2525 
2526         delete socket;
2527         socket = new QSslSocket;
2528         socket->setProxy(proxy);
2529         socket->connectToHost(host, port);
2530         socket->waitForConnected(q->connectTimeout() * 1000);
2531 
2532         if (socket->state() == QAbstractSocket::ConnectedState) {
2533             // reconnect with credentials was successful -> save data
2534             q->cacheAuthentication(info);
2535 
2536             m_proxyURL.setUserName(info.username);
2537             m_proxyURL.setPassword(info.password);
2538         }
2539     }
2540 
2541     return ConnectionResult{socket, Result::pass()};
2542 }
2543 
2544 //===============================================================================
2545 // Ftp
2546 //===============================================================================
2547 
2548 Ftp::Ftp(const QByteArray &pool, const QByteArray &app)
2549     : WorkerBase(QByteArrayLiteral("ftp"), pool, app)
2550     , d(new FtpInternal(this))
2551 {
2552 }
2553 
2554 Ftp::~Ftp() = default;
2555 
2556 void Ftp::setHost(const QString &host, quint16 port, const QString &user, const QString &pass)
2557 {
2558     d->setHost(host, port, user, pass);
2559 }
2560 
2561 KIO::WorkerResult Ftp::openConnection()
2562 {
2563     return d->openConnection();
2564 }
2565 
2566 void Ftp::closeConnection()
2567 {
2568     d->closeConnection();
2569 }
2570 
2571 KIO::WorkerResult Ftp::stat(const QUrl &url)
2572 {
2573     return d->stat(url);
2574 }
2575 
2576 KIO::WorkerResult Ftp::listDir(const QUrl &url)
2577 {
2578     return d->listDir(url);
2579 }
2580 
2581 KIO::WorkerResult Ftp::mkdir(const QUrl &url, int permissions)
2582 {
2583     return d->mkdir(url, permissions);
2584 }
2585 
2586 KIO::WorkerResult Ftp::rename(const QUrl &src, const QUrl &dst, JobFlags flags)
2587 {
2588     return d->rename(src, dst, flags);
2589 }
2590 
2591 KIO::WorkerResult Ftp::del(const QUrl &url, bool isfile)
2592 {
2593     return d->del(url, isfile);
2594 }
2595 
2596 KIO::WorkerResult Ftp::chmod(const QUrl &url, int permissions)
2597 {
2598     return d->chmod(url, permissions);
2599 }
2600 
2601 KIO::WorkerResult Ftp::get(const QUrl &url)
2602 {
2603     return d->get(url);
2604 }
2605 
2606 KIO::WorkerResult Ftp::put(const QUrl &url, int permissions, JobFlags flags)
2607 {
2608     return d->put(url, permissions, flags);
2609 }
2610 
2611 void Ftp::worker_status()
2612 {
2613     d->worker_status();
2614 }
2615 
2616 KIO::WorkerResult Ftp::copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
2617 {
2618     return d->copy(src, dest, permissions, flags);
2619 }
2620 
2621 QDebug operator<<(QDebug dbg, const Result &r)
2622 
2623 {
2624     QDebugStateSaver saver(dbg);
2625     dbg.nospace() << "Result("
2626                   << "success=" << r.success() << ", err=" << r.error() << ", str=" << r.errorString() << ')';
2627     return dbg;
2628 }
2629 
2630 // needed for JSON file embedding
2631 #include "ftp.moc"
2632 
2633 #include "moc_ftp.cpp"