File indexing completed on 2024-05-12 03:56:45

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