1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000-2006 David Faure <faure@kde.org>
4 SPDX-FileCopyrightText: 2019-2021 Harald Sitter <sitter@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9/*
10 Recommended reading explaining FTP details and quirks:
11 https://cr.yp.to/ftp.html (by D.J. Bernstein)
12
13 RFC:
14 RFC 959 "File Transfer Protocol (FTP)"
15 RFC 1635 "How to Use Anonymous FTP"
16 RFC 2428 "FTP Extensions for IPv6 and NATs" (defines EPRT and EPSV)
17*/
18
19#include <config-kioworker-ftp.h>
20
21#include "ftp.h"
22
23#ifdef Q_OS_WIN
24#include <sys/utime.h>
25#else
26#include <utime.h>
27#endif
28
29#include <cctype>
30#include <cerrno>
31#include <cstdlib>
32#include <cstring>
33
34#include <QAuthenticator>
35#include <QCoreApplication>
36#include <QDir>
37#include <QHostAddress>
38#include <QMimeDatabase>
39#include <QNetworkProxy>
40#include <QSslSocket>
41#include <QTcpServer>
42#include <QTcpSocket>
43
44#include <KConfigGroup>
45#include <KLocalizedString>
46#include <QDebug>
47#include <authinfo.h>
48#include <ioworker_defaults.h>
49#include <kremoteencoding.h>
50
51#include "kioglobal_p.h"
52
53#include <QLoggingCategory>
54Q_DECLARE_LOGGING_CATEGORY(KIO_FTP)
55Q_LOGGING_CATEGORY(KIO_FTP, "kf.kio.workers.ftp", QtWarningMsg)
56
57#if HAVE_STRTOLL
58#define charToLongLong(a) strtoll(a, nullptr, 10)
59#else
60#define charToLongLong(a) strtol(a, nullptr, 10)
61#endif
62
63static constexpr char s_ftpLogin[] = "anonymous";
64static constexpr char s_ftpPasswd[] = "anonymous@";
65
66static constexpr bool s_enableCanResume = true;
67
68// Pseudo plugin class to embed meta data
69class KIOPluginForMetaData : public QObject
70{
71 Q_OBJECT
72 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.ftp" FILE "ftp.json")
73};
74
75static QString ftpCleanPath(const QString &path)
76{
77 if (path.endsWith(s: QLatin1String(";type=A"), cs: Qt::CaseInsensitive) || path.endsWith(s: QLatin1String(";type=I"), cs: Qt::CaseInsensitive)
78 || path.endsWith(s: QLatin1String(";type=D"), cs: Qt::CaseInsensitive)) {
79 return path.left(n: (path.length() - qstrlen(str: ";type=X")));
80 }
81
82 return path;
83}
84
85static char ftpModeFromPath(const QString &path, char defaultMode = '\0')
86{
87 const int index = path.lastIndexOf(s: QLatin1String(";type="));
88
89 if (index > -1 && (index + 6) < path.size()) {
90 const QChar mode = path.at(i: index + 6);
91 // kio_ftp supports only A (ASCII) and I(BINARY) modes.
92 if (mode == QLatin1Char('A') || mode == QLatin1Char('a') || mode == QLatin1Char('I') || mode == QLatin1Char('i')) {
93 return mode.toUpper().toLatin1();
94 }
95 }
96
97 return defaultMode;
98}
99
100static bool supportedProxyScheme(const QString &scheme)
101{
102 return (scheme == QLatin1String("ftp") || scheme == QLatin1String("socks"));
103}
104
105// JPF: somebody should find a better solution for this or move this to KIO
106namespace KIO
107{
108enum buffersizes {
109 /*!
110 * largest buffer size that should be used to transfer data between
111 * KIO workers using the data() function
112 */
113 maximumIpcSize = 32 * 1024,
114 /*!
115 * this is a reasonable value for an initial read() that a KIO worker
116 * can do to obtain data via a slow network connection.
117 */
118 initialIpcSize = 2 * 1024,
119 /*!
120 * recommended size of a data block passed to findBufferFileType()
121 */
122 minimumMimeSize = 1024,
123};
124
125// JPF: this helper was derived from write_all in file.cc (FileProtocol).
126static // JPF: in ftp.cc we make it static
127 /*!
128 * This helper handles some special issues (blocking and interrupted
129 * system call) when writing to a file handle.
130 *
131 * Returns 0 on success or an error code on failure (ERR_CANNOT_WRITE,
132 * ERR_DISK_FULL, ERR_CONNECTION_BROKEN).
133 */
134 int
135 WriteToFile(int fd, const char *buf, size_t len)
136{
137 while (len > 0) {
138 // JPF: shouldn't there be a KDE_write?
139 ssize_t written = write(fd: fd, buf: buf, n: len);
140 if (written >= 0) {
141 buf += written;
142 len -= written;
143 continue;
144 }
145 switch (errno) {
146 case EINTR:
147 continue;
148 case EPIPE:
149 return ERR_CONNECTION_BROKEN;
150 case ENOSPC:
151 return ERR_DISK_FULL;
152 default:
153 return ERR_CANNOT_WRITE;
154 }
155 }
156 return 0;
157}
158}
159
160const KIO::filesize_t FtpInternal::UnknownSize = (KIO::filesize_t)-1;
161
162using namespace KIO;
163
164extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
165{
166 QCoreApplication app(argc, argv);
167 app.setApplicationName(QStringLiteral("kio_ftp"));
168
169 qCDebug(KIO_FTP) << "Starting";
170
171 if (argc != 4) {
172 fprintf(stderr, format: "Usage: kio_ftp protocol domain-socket1 domain-socket2\n");
173 exit(status: -1);
174 }
175
176 Ftp worker(argv[2], argv[3]);
177 worker.dispatchLoop();
178
179 qCDebug(KIO_FTP) << "Done";
180 return 0;
181}
182
183//===============================================================================
184// FtpInternal
185//===============================================================================
186
187/*!
188 * This closes a data connection opened by ftpOpenDataConnection().
189 */
190void FtpInternal::ftpCloseDataConnection()
191{
192 delete m_data;
193 m_data = nullptr;
194 delete m_server;
195 m_server = nullptr;
196}
197
198/*!
199 * This closes a control connection opened by ftpOpenControlConnection() and reinits the
200 * related states. This method gets called from the constructor with m_control = nullptr.
201 */
202void FtpInternal::ftpCloseControlConnection()
203{
204 m_extControl = 0;
205 delete m_control;
206 m_control = nullptr;
207 m_cDataMode = 0;
208 m_bLoggedOn = false; // logon needs control connection
209 m_bTextMode = false;
210 m_bBusy = false;
211}
212
213/*!
214 * Returns the last response from the server (iOffset >= 0) -or- reads a new response
215 * (iOffset < 0). The result is returned (with iOffset chars skipped for iOffset > 0).
216 */
217const char *FtpInternal::ftpResponse(int iOffset)
218{
219 Q_ASSERT(m_control); // must have control connection socket
220 const char *pTxt = m_lastControlLine.data();
221
222 // read the next line ...
223 if (iOffset < 0) {
224 int iMore = 0;
225 m_iRespCode = 0;
226
227 if (!pTxt) {
228 return nullptr; // avoid using a nullptr when calling atoi.
229 }
230
231 // If the server sends a multiline response starting with
232 // "nnn-text" we loop here until a final "nnn text" line is
233 // reached. Only data from the final line will be stored.
234 do {
235 while (!m_control->canReadLine() && m_control->waitForReadyRead(msecs: (DEFAULT_READ_TIMEOUT * 1000))) { }
236 m_lastControlLine = m_control->readLine();
237 pTxt = m_lastControlLine.data();
238 int iCode = atoi(nptr: pTxt);
239 if (iMore == 0) {
240 // first line
241 qCDebug(KIO_FTP) << " > " << pTxt;
242 if (iCode >= 100) {
243 m_iRespCode = iCode;
244 if (pTxt[3] == '-') {
245 // marker for a multiple line response
246 iMore = iCode;
247 }
248 } else {
249 qCWarning(KIO_FTP) << "Cannot parse valid code from line" << pTxt;
250 }
251 } else {
252 // multi-line
253 qCDebug(KIO_FTP) << " > " << pTxt;
254 if (iCode >= 100 && iCode == iMore && pTxt[3] == ' ') {
255 iMore = 0;
256 }
257 }
258 } while (iMore != 0);
259 qCDebug(KIO_FTP) << "resp> " << pTxt;
260
261 m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0;
262 }
263
264 // return text with offset ...
265 while (iOffset-- > 0 && pTxt[0]) {
266 pTxt++;
267 }
268 return pTxt;
269}
270
271void FtpInternal::closeConnection()
272{
273 if (m_control || m_data) {
274 qCDebug(KIO_FTP) << "m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy;
275 }
276
277 if (m_bBusy) { // ftpCloseCommand not called
278 qCWarning(KIO_FTP) << "Abandoned data stream";
279 ftpCloseDataConnection();
280 }
281
282 if (m_bLoggedOn) { // send quit
283 if (!ftpSendCmd(QByteArrayLiteral("quit"), maxretries: 0) || (m_iRespType != 2)) {
284 qCWarning(KIO_FTP) << "QUIT returned error: " << m_iRespCode;
285 }
286 }
287
288 // close the data and control connections ...
289 ftpCloseDataConnection();
290 ftpCloseControlConnection();
291}
292
293FtpInternal::FtpInternal(Ftp *qptr)
294 : QObject()
295 , q(qptr)
296{
297 ftpCloseControlConnection();
298}
299
300FtpInternal::~FtpInternal()
301{
302 qCDebug(KIO_FTP);
303 closeConnection();
304}
305
306void FtpInternal::setHost(const QString &_host, quint16 _port, const QString &_user, const QString &_pass)
307{
308 qCDebug(KIO_FTP) << _host << "port=" << _port << "user=" << _user;
309
310 m_proxyURL.clear();
311 m_proxyUrls.clear();
312 const auto proxies = QNetworkProxyFactory::proxyForQuery(query: QNetworkProxyQuery(_host, _port, QStringLiteral("ftp"), QNetworkProxyQuery::UrlRequest));
313
314 for (const QNetworkProxy &proxy : proxies) {
315 if (proxy.type() != QNetworkProxy::NoProxy) {
316 QUrl proxyUrl;
317 proxyUrl.setScheme(QStringLiteral("ftp"));
318 proxyUrl.setUserName(userName: proxy.user());
319 proxyUrl.setPassword(password: proxy.password());
320 proxyUrl.setHost(host: proxy.hostName());
321 proxyUrl.setPort(proxy.port());
322
323 m_proxyUrls << proxyUrl.toString();
324 }
325 }
326
327 qCDebug(KIO_FTP) << "proxy urls:" << m_proxyUrls;
328
329 if (m_host != _host || m_port != _port || m_user != _user || m_pass != _pass) {
330 closeConnection();
331 }
332
333 m_host = _host;
334 m_port = _port;
335 m_user = _user;
336 m_pass = _pass;
337}
338
339Result FtpInternal::openConnection()
340{
341 return ftpOpenConnection(loginMode: LoginMode::Explicit);
342}
343
344Result FtpInternal::ftpOpenConnection(LoginMode loginMode)
345{
346 // check for implicit login if we are already logged on ...
347 if (loginMode == LoginMode::Implicit && m_bLoggedOn) {
348 Q_ASSERT(m_control); // must have control connection socket
349 return Result::pass();
350 }
351
352 qCDebug(KIO_FTP) << "host=" << m_host << ", port=" << m_port << ", user=" << m_user << "password= [password hidden]";
353
354 q->infoMessage(i18n("Opening connection to host %1", m_host));
355
356 if (m_host.isEmpty()) {
357 return Result::fail(error: ERR_UNKNOWN_HOST);
358 }
359
360 Q_ASSERT(!m_bLoggedOn);
361
362 m_initialPath.clear();
363 m_currentPath.clear();
364
365 const Result result = ftpOpenControlConnection();
366 if (!result.success()) {
367 return result;
368 }
369 q->infoMessage(i18n("Connected to host %1", m_host));
370
371 bool userNameChanged = false;
372 if (loginMode != LoginMode::Deferred) {
373 const Result result = ftpLogin(userChanged: &userNameChanged);
374 m_bLoggedOn = result.success();
375 if (!m_bLoggedOn) {
376 return result;
377 }
378 }
379
380 m_bTextMode = q->configValue(QStringLiteral("textmode"), defaultValue: false);
381
382 // Redirected due to credential change...
383 if (userNameChanged && m_bLoggedOn) {
384 QUrl realURL;
385 realURL.setScheme(QStringLiteral("ftp"));
386 if (m_user != QLatin1String(s_ftpLogin)) {
387 realURL.setUserName(userName: m_user);
388 }
389 if (m_pass != QLatin1String(s_ftpPasswd)) {
390 realURL.setPassword(password: m_pass);
391 }
392 realURL.setHost(host: m_host);
393 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
394 realURL.setPort(m_port);
395 }
396 if (m_initialPath.isEmpty()) {
397 m_initialPath = QStringLiteral("/");
398 }
399 realURL.setPath(path: m_initialPath);
400 qCDebug(KIO_FTP) << "User name changed! Redirecting to" << realURL;
401 q->redirection(url: realURL);
402 return Result::fail();
403 }
404
405 return Result::pass();
406}
407
408/*!
409 * Called by @ref openConnection. It opens the control connection to the ftp server.
410 *
411 * Returns true on success.
412 */
413Result FtpInternal::ftpOpenControlConnection()
414{
415 if (m_proxyUrls.isEmpty()) {
416 return ftpOpenControlConnection(host: m_host, port: m_port);
417 }
418
419 Result result = Result::fail();
420
421 for (const QString &proxyUrl : std::as_const(t&: m_proxyUrls)) {
422 const QUrl url(proxyUrl);
423 const QString scheme(url.scheme());
424
425 if (!supportedProxyScheme(scheme)) {
426 // TODO: Need a new error code to indicate unsupported URL scheme.
427 result = Result::fail(error: ERR_CANNOT_CONNECT, errorString: url.toString());
428 continue;
429 }
430
431 if (!isSocksProxyScheme(scheme)) {
432 const Result result = ftpOpenControlConnection(host: url.host(), port: url.port());
433 if (result.success()) {
434 return Result::pass();
435 }
436 continue;
437 }
438
439 qCDebug(KIO_FTP) << "Connecting to SOCKS proxy @" << url;
440 m_proxyURL = url;
441 result = ftpOpenControlConnection(host: m_host, port: m_port);
442 if (result.success()) {
443 return result;
444 }
445 m_proxyURL.clear();
446 }
447
448 return result;
449}
450
451Result FtpInternal::ftpOpenControlConnection(const QString &host, int port)
452{
453 // implicitly close, then try to open a new connection ...
454 closeConnection();
455 QString sErrorMsg;
456
457 // now connect to the server and read the login message ...
458 if (port == 0) {
459 port = 21; // default FTP port
460 }
461 const auto connectionResult = synchronousConnectToHost(host, port);
462 m_control = connectionResult.socket;
463
464 int iErrorCode = m_control->state() == QAbstractSocket::ConnectedState ? 0 : ERR_CANNOT_CONNECT;
465 if (!connectionResult.result.success()) {
466 qDebug() << "overriding error code!!1" << connectionResult.result.error();
467 iErrorCode = connectionResult.result.error();
468 sErrorMsg = connectionResult.result.errorString();
469 }
470
471 // on connect success try to read the server message...
472 if (iErrorCode == 0) {
473 const char *psz = ftpResponse(iOffset: -1);
474 if (m_iRespType != 2) {
475 // login not successful, do we have an message text?
476 if (psz[0]) {
477 sErrorMsg = i18n("%1 (Error %2)", host, q->remoteEncoding()->decode(psz).trimmed());
478 }
479 iErrorCode = ERR_CANNOT_CONNECT;
480 }
481 } else {
482 const auto socketError = m_control->error();
483 if (socketError == QAbstractSocket::HostNotFoundError) {
484 iErrorCode = ERR_UNKNOWN_HOST;
485 }
486
487 sErrorMsg = QStringLiteral("%1: %2").arg(args: host, args: m_control->errorString());
488 }
489
490 // if there was a problem - report it ...
491 if (iErrorCode == 0) { // OK, return success
492 return Result::pass();
493 }
494 closeConnection(); // clean-up on error
495 return Result::fail(error: iErrorCode, errorString: sErrorMsg);
496}
497
498/*!
499 * Called by @ref openConnection. It logs us in.
500 * @ref m_initialPath is set to the current working directory
501 * if logging on was successful.
502 *
503 * Returns true on success.
504 */
505Result FtpInternal::ftpLogin(bool *userChanged)
506{
507 q->infoMessage(i18n("Sending login information"));
508
509 Q_ASSERT(!m_bLoggedOn);
510
511 QString user(m_user);
512 QString pass(m_pass);
513
514 AuthInfo info;
515 info.url.setScheme(QStringLiteral("ftp"));
516 info.url.setHost(host: m_host);
517 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
518 info.url.setPort(m_port);
519 }
520 if (!user.isEmpty()) {
521 info.url.setUserName(userName: user);
522 }
523
524 // Check for cached authentication first and fallback to
525 // anonymous login when no stored credentials are found.
526 if (!q->configValue(QStringLiteral("TryAnonymousLoginFirst"), defaultValue: false) && pass.isEmpty() && q->checkCachedAuthentication(info)) {
527 user = info.username;
528 pass = info.password;
529 }
530
531 // Try anonymous login if both username/password
532 // information is blank.
533 if (user.isEmpty() && pass.isEmpty()) {
534 user = QString::fromLatin1(ba: s_ftpLogin);
535 pass = QString::fromLatin1(ba: s_ftpPasswd);
536 }
537
538 QByteArray tempbuf;
539 QString lastServerResponse;
540 int failedAuth = 0;
541 bool promptForRetry = false;
542
543 // Give the user the option to login anonymously...
544 info.setExtraField(QStringLiteral("anonymous"), value: false);
545
546 do {
547 // Check the cache and/or prompt user for password if 1st
548 // login attempt failed OR the user supplied a login name,
549 // but no password.
550 if (failedAuth > 0 || (!user.isEmpty() && pass.isEmpty())) {
551 QString errorMsg;
552 qCDebug(KIO_FTP) << "Prompting user for login info...";
553
554 // Ask user if we should retry after when login fails!
555 if (failedAuth > 0 && promptForRetry) {
556 errorMsg = i18n(
557 "Message sent:\nLogin using username=%1 and "
558 "password=[hidden]\n\nServer replied:\n%2\n\n",
559 user,
560 lastServerResponse);
561 }
562
563 if (user != QLatin1String(s_ftpLogin)) {
564 info.username = user;
565 }
566
567 info.prompt = i18n(
568 "You need to supply a username and a password "
569 "to access this site.");
570 info.commentLabel = i18n("Site:");
571 info.comment = i18n("<b>%1</b>", m_host);
572 info.keepPassword = true; // Prompt the user for persistence as well.
573 info.setModified(false); // Default the modified flag since we reuse authinfo.
574
575 const bool disablePassDlg = q->configValue(QStringLiteral("DisablePassDlg"), defaultValue: false);
576 if (disablePassDlg) {
577 return Result::fail(error: ERR_USER_CANCELED, errorString: m_host);
578 }
579 const int errorCode = q->openPasswordDialog(info, errorMsg);
580 if (errorCode) {
581 return Result::fail(error: errorCode);
582 } else {
583 // User can decide go anonymous using checkbox
584 if (info.getExtraField(QStringLiteral("anonymous")).toBool()) {
585 user = QString::fromLatin1(ba: s_ftpLogin);
586 pass = QString::fromLatin1(ba: s_ftpPasswd);
587 } else {
588 user = info.username;
589 pass = info.password;
590 }
591 promptForRetry = true;
592 }
593 }
594
595 tempbuf = "USER " + user.toLatin1();
596 if (m_proxyURL.isValid()) {
597 tempbuf += '@' + m_host.toLatin1();
598 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
599 tempbuf += ':' + QByteArray::number(m_port);
600 }
601 }
602
603 qCDebug(KIO_FTP) << "Sending Login name: " << tempbuf;
604
605 bool loggedIn = (ftpSendCmd(cmd: tempbuf) && (m_iRespCode == 230));
606 bool needPass = (m_iRespCode == 331);
607 // Prompt user for login info if we do not
608 // get back a "230" or "331".
609 if (!loggedIn && !needPass) {
610 lastServerResponse = QString::fromUtf8(utf8: ftpResponse(iOffset: 0));
611 qCDebug(KIO_FTP) << "Login failed: " << lastServerResponse;
612 ++failedAuth;
613 continue; // Well we failed, prompt the user please!!
614 }
615
616 if (needPass) {
617 tempbuf = "PASS " + pass.toLatin1();
618 qCDebug(KIO_FTP) << "Sending Login password: "
619 << "[protected]";
620 loggedIn = (ftpSendCmd(cmd: tempbuf) && (m_iRespCode == 230));
621 }
622
623 if (loggedIn) {
624 // Make sure the user name changed flag is properly set.
625 if (userChanged) {
626 *userChanged = (!m_user.isEmpty() && (m_user != user));
627 }
628
629 // Do not cache the default login!!
630 if (user != QLatin1String(s_ftpLogin) && pass != QLatin1String(s_ftpPasswd)) {
631 // Update the username in case it was changed during login.
632 if (!m_user.isEmpty()) {
633 info.url.setUserName(userName: user);
634 m_user = user;
635 }
636
637 // Cache the password if the user requested it.
638 if (info.keepPassword) {
639 q->cacheAuthentication(info);
640 }
641 }
642 failedAuth = -1;
643 } else {
644 // some servers don't let you login anymore
645 // if you fail login once, so restart the connection here
646 lastServerResponse = QString::fromUtf8(utf8: ftpResponse(iOffset: 0));
647 const Result result = ftpOpenControlConnection();
648 if (!result.success()) {
649 return result;
650 }
651 }
652 } while (++failedAuth);
653
654 qCDebug(KIO_FTP) << "Login OK";
655 q->infoMessage(i18n("Login OK"));
656
657 // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix:
658 // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint
659 if (ftpSendCmd(QByteArrayLiteral("SYST")) && (m_iRespType == 2)) {
660 if (!qstrncmp(str1: ftpResponse(iOffset: 0), str2: "215 Windows_NT", len: 14)) { // should do for any version
661 (void)ftpSendCmd(QByteArrayLiteral("site dirstyle"));
662 // Check if it was already in Unix style
663 // Patch from Keith Refson <Keith.Refson@earth.ox.ac.uk>
664 if (!qstrncmp(str1: ftpResponse(iOffset: 0), str2: "200 MSDOS-like directory output is on", len: 37))
665 // It was in Unix style already!
666 {
667 (void)ftpSendCmd(QByteArrayLiteral("site dirstyle"));
668 }
669 // windows won't support chmod before KDE konquers their desktop...
670 m_extControl |= chmodUnknown;
671 }
672 } else {
673 qCWarning(KIO_FTP) << "SYST failed";
674 }
675
676 // Get the current working directory
677 qCDebug(KIO_FTP) << "Searching for pwd";
678 if (!ftpSendCmd(QByteArrayLiteral("PWD")) || (m_iRespType != 2)) {
679 qCDebug(KIO_FTP) << "Couldn't issue pwd command";
680 return Result::fail(error: ERR_CANNOT_LOGIN, i18n("Could not login to %1.", m_host)); // or anything better ?
681 }
682
683 QString sTmp = q->remoteEncoding()->decode(name: ftpResponse(iOffset: 3));
684 const int iBeg = sTmp.indexOf(ch: QLatin1Char('"'));
685 const int iEnd = sTmp.lastIndexOf(c: QLatin1Char('"'));
686 if (iBeg > 0 && iBeg < iEnd) {
687 m_initialPath = sTmp.mid(position: iBeg + 1, n: iEnd - iBeg - 1);
688 if (!m_initialPath.startsWith(c: QLatin1Char('/'))) {
689 m_initialPath.prepend(c: QLatin1Char('/'));
690 }
691 qCDebug(KIO_FTP) << "Initial path set to: " << m_initialPath;
692 m_currentPath = m_initialPath;
693 }
694
695 return Result::pass();
696}
697
698/*!
699 * ftpSendCmd - send a command (@p cmd) and read response
700 *
701 * \a maxretries number of time it should retry. Since it recursively
702 * calls itself if it can't read the answer (this happens especially after
703 * timeouts), we need to limit the recursiveness ;-)
704 *
705 * return true if any response received, false on error
706 */
707bool FtpInternal::ftpSendCmd(const QByteArray &cmd, int maxretries)
708{
709 Q_ASSERT(m_control); // must have control connection socket
710
711 if (cmd.indexOf(ch: '\r') != -1 || cmd.indexOf(ch: '\n') != -1) {
712 qCWarning(KIO_FTP) << "Invalid command received (contains CR or LF):" << cmd.data();
713 return false;
714 }
715
716 // Don't print out the password...
717 bool isPassCmd = (cmd.left(n: 4).toLower() == "pass");
718
719 // Send the message...
720 const QByteArray buf = cmd + "\r\n"; // Yes, must use CR/LF - see https://cr.yp.to/ftp/request.html
721 int num = m_control->write(data: buf);
722 while (m_control->bytesToWrite() && m_control->waitForBytesWritten()) { }
723
724 // If we were able to successfully send the command, then we will
725 // attempt to read the response. Otherwise, take action to re-attempt
726 // the login based on the maximum number of retries specified...
727 if (num > 0) {
728 ftpResponse(iOffset: -1);
729 } else {
730 m_iRespType = m_iRespCode = 0;
731 }
732
733 // If respCh is NULL or the response is 421 (Timed-out), we try to re-send
734 // the command based on the value of maxretries.
735 if ((m_iRespType <= 0) || (m_iRespCode == 421)) {
736 // We have not yet logged on...
737 if (!m_bLoggedOn) {
738 // The command was sent from the ftpLogin function, i.e. we are actually
739 // attempting to login in. NOTE: If we already sent the username, we
740 // return false and let the user decide whether (s)he wants to start from
741 // the beginning...
742 if (maxretries > 0 && !isPassCmd) {
743 closeConnection();
744 const auto result = ftpOpenConnection(loginMode: LoginMode::Deferred);
745 if (result.success() && ftpSendCmd(cmd, maxretries: maxretries - 1)) {
746 return true;
747 }
748 }
749
750 return false;
751 } else {
752 if (maxretries < 1) {
753 return false;
754 } else {
755 qCDebug(KIO_FTP) << "Was not able to communicate with " << m_host << "Attempting to re-establish connection.";
756
757 closeConnection(); // Close the old connection...
758 const Result openResult = openConnection(); // Attempt to re-establish a new connection...
759
760 if (!openResult.success()) {
761 if (m_control) { // if openConnection succeeded ...
762 qCDebug(KIO_FTP) << "Login failure, aborting";
763 closeConnection();
764 }
765 return false;
766 }
767
768 qCDebug(KIO_FTP) << "Logged back in, re-issuing command";
769
770 // If we were able to login, resend the command...
771 if (maxretries) {
772 maxretries--;
773 }
774
775 return ftpSendCmd(cmd, maxretries);
776 }
777 }
778 }
779
780 return true;
781}
782
783/*
784 * ftpOpenPASVDataConnection - set up data connection, using PASV mode
785 *
786 * return 0 if successful, ERR_INTERNAL otherwise
787 * doesn't set error message, since non-pasv mode will always be tried if
788 * this one fails
789 */
790int FtpInternal::ftpOpenPASVDataConnection()
791{
792 Q_ASSERT(m_control); // must have control connection socket
793 Q_ASSERT(!m_data); // ... but no data connection
794
795 // Check that we can do PASV
796 QHostAddress address = m_control->peerAddress();
797 if (address.protocol() != QAbstractSocket::IPv4Protocol && !isSocksProxy()) {
798 return ERR_INTERNAL; // no PASV for non-PF_INET connections
799 }
800
801 if (m_extControl & pasvUnknown) {
802 return ERR_INTERNAL; // already tried and got "unknown command"
803 }
804
805 m_bPasv = true;
806
807 /* Let's PASsiVe*/
808 if (!ftpSendCmd(QByteArrayLiteral("PASV")) || (m_iRespType != 2)) {
809 qCDebug(KIO_FTP) << "PASV attempt failed";
810 // unknown command?
811 if (m_iRespType == 5) {
812 qCDebug(KIO_FTP) << "disabling use of PASV";
813 m_extControl |= pasvUnknown;
814 }
815 return ERR_INTERNAL;
816 }
817
818 // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)'
819 // but anonftpd gives '227 =160,39,200,55,6,245'
820 int i[6];
821 const char *start = strchr(s: ftpResponse(iOffset: 3), c: '(');
822 if (!start) {
823 start = strchr(s: ftpResponse(iOffset: 3), c: '=');
824 }
825 if (!start
826 || (sscanf(s: start, format: "(%d,%d,%d,%d,%d,%d)", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6
827 && sscanf(s: start, format: "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6)) {
828 qCritical() << "parsing IP and port numbers failed. String parsed: " << start;
829 return ERR_INTERNAL;
830 }
831
832 // we ignore the host part on purpose for two reasons
833 // a) it might be wrong anyway
834 // b) it would make us being susceptible to a port scanning attack
835
836 // now connect the data socket ...
837 quint16 port = i[4] << 8 | i[5];
838 const QString host = (isSocksProxy() ? m_host : address.toString());
839 const auto connectionResult = synchronousConnectToHost(host, port);
840 m_data = connectionResult.socket;
841 if (!connectionResult.result.success()) {
842 return connectionResult.result.error();
843 }
844
845 return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL;
846}
847
848/*
849 * ftpOpenEPSVDataConnection - opens a data connection via EPSV
850 */
851int FtpInternal::ftpOpenEPSVDataConnection()
852{
853 Q_ASSERT(m_control); // must have control connection socket
854 Q_ASSERT(!m_data); // ... but no data connection
855
856 QHostAddress address = m_control->peerAddress();
857 int portnum;
858
859 if (m_extControl & epsvUnknown) {
860 return ERR_INTERNAL;
861 }
862
863 m_bPasv = true;
864 if (!ftpSendCmd(QByteArrayLiteral("EPSV")) || (m_iRespType != 2)) {
865 // unknown command?
866 if (m_iRespType == 5) {
867 qCDebug(KIO_FTP) << "disabling use of EPSV";
868 m_extControl |= epsvUnknown;
869 }
870 return ERR_INTERNAL;
871 }
872
873 const char *start = strchr(s: ftpResponse(iOffset: 3), c: '|');
874 if (!start || sscanf(s: start, format: "|||%d|", &portnum) != 1) {
875 return ERR_INTERNAL;
876 }
877 Q_ASSERT(portnum > 0);
878
879 const QString host = (isSocksProxy() ? m_host : address.toString());
880 const auto connectionResult = synchronousConnectToHost(host, port: static_cast<quint16>(portnum));
881 m_data = connectionResult.socket;
882 if (!connectionResult.result.success()) {
883 return connectionResult.result.error();
884 }
885 return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL;
886}
887
888/*
889 * ftpOpenDataConnection - set up data connection
890 *
891 * The routine calls several ftpOpenXxxxConnection() helpers to find
892 * the best connection mode. If a helper cannot connect if returns
893 * ERR_INTERNAL - so this is not really an error! All other error
894 * codes are treated as fatal, e.g. they are passed back to the caller
895 * who is responsible for calling error(). ftpOpenPortDataConnection
896 * can be called as last try and it does never return ERR_INTERNAL.
897 *
898 * Returns 0 if successful, err code otherwise
899 */
900int FtpInternal::ftpOpenDataConnection()
901{
902 // make sure that we are logged on and have no data connection...
903 Q_ASSERT(m_bLoggedOn);
904 ftpCloseDataConnection();
905
906 int iErrCode = 0;
907 int iErrCodePASV = 0; // Remember error code from PASV
908
909 // First try passive (EPSV & PASV) modes
910 if (!q->configValue(QStringLiteral("DisablePassiveMode"), defaultValue: false)) {
911 iErrCode = ftpOpenPASVDataConnection();
912 if (iErrCode == 0) {
913 return 0; // success
914 }
915 iErrCodePASV = iErrCode;
916 ftpCloseDataConnection();
917
918 if (!q->configValue(QStringLiteral("DisableEPSV"), defaultValue: false)) {
919 iErrCode = ftpOpenEPSVDataConnection();
920 if (iErrCode == 0) {
921 return 0; // success
922 }
923 ftpCloseDataConnection();
924 }
925
926 // if we sent EPSV ALL already and it was accepted, then we can't
927 // use active connections any more
928 if (m_extControl & epsvAllSent) {
929 return iErrCodePASV;
930 }
931 }
932
933 // fall back to port mode
934 iErrCode = ftpOpenPortDataConnection();
935 if (iErrCode == 0) {
936 return 0; // success
937 }
938
939 ftpCloseDataConnection();
940 // prefer to return the error code from PASV if any, since that's what should have worked in the first place
941 return iErrCodePASV ? iErrCodePASV : iErrCode;
942}
943
944/*
945 * ftpOpenPortDataConnection - set up data connection
946 *
947 * Returns 0 if successful, err code otherwise (but never ERR_INTERNAL
948 * because this is the last connection mode that is tried)
949 */
950int FtpInternal::ftpOpenPortDataConnection()
951{
952 Q_ASSERT(m_control); // must have control connection socket
953 Q_ASSERT(!m_data); // ... but no data connection
954
955 m_bPasv = false;
956 if (m_extControl & eprtUnknown) {
957 return ERR_INTERNAL;
958 }
959
960 if (!m_server) {
961 m_server = new QTcpServer;
962 m_server->listen(address: QHostAddress::Any, port: 0);
963 }
964
965 if (!m_server->isListening()) {
966 delete m_server;
967 m_server = nullptr;
968 return ERR_CANNOT_LISTEN;
969 }
970
971 m_server->setMaxPendingConnections(1);
972
973 QString command;
974 QHostAddress localAddress = m_control->localAddress();
975 if (localAddress.protocol() == QAbstractSocket::IPv4Protocol) {
976 struct {
977 quint32 ip4;
978 quint16 port;
979 } data;
980 data.ip4 = localAddress.toIPv4Address();
981 data.port = m_server->serverPort();
982
983 unsigned char *pData = reinterpret_cast<unsigned char *>(&data);
984 command = QStringLiteral("PORT %1,%2,%3,%4,%5,%6").arg(a: pData[3]).arg(a: pData[2]).arg(a: pData[1]).arg(a: pData[0]).arg(a: pData[5]).arg(a: pData[4]);
985 } else if (localAddress.protocol() == QAbstractSocket::IPv6Protocol) {
986 command = QStringLiteral("EPRT |2|%2|%3|").arg(a: localAddress.toString()).arg(a: m_server->serverPort());
987 }
988
989 if (ftpSendCmd(cmd: command.toLatin1()) && (m_iRespType == 2)) {
990 return 0;
991 }
992
993 delete m_server;
994 m_server = nullptr;
995 return ERR_INTERNAL;
996}
997
998Result FtpInternal::ftpOpenCommand(const char *_command, const QString &_path, char _mode, int errorcode, KIO::fileoffset_t _offset)
999{
1000 if (!ftpDataMode(cMode: ftpModeFromPath(path: _path, defaultMode: _mode))) {
1001 return Result::fail(error: ERR_CANNOT_CONNECT, errorString: m_host);
1002 }
1003
1004 if (int error = ftpOpenDataConnection()) {
1005 return Result::fail(error: error, errorString: 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(s: buf, format: "rest %lld", _offset);
1012 if (!ftpSendCmd(cmd: buf)) {
1013 return Result::fail();
1014 }
1015 if (m_iRespType != 3) {
1016 return Result::fail(error: ERR_CANNOT_RESUME, errorString: _path); // should never happen
1017 }
1018 }
1019
1020 QByteArray tmp = _command;
1021 QString errormessage;
1022
1023 if (!_path.isEmpty()) {
1024 tmp += ' ' + q->remoteEncoding()->encode(name: ftpCleanPath(path: _path));
1025 }
1026
1027 if (!ftpSendCmd(cmd: tmp) || (m_iRespType != 1)) {
1028 if (_offset > 0 && qstrcmp(str1: _command, str2: "retr") == 0 && (m_iRespType == 4)) {
1029 errorcode = ERR_CANNOT_RESUME;
1030 }
1031 if (qstrcmp(str1: _command, str2: "stor") == 0 && m_iRespCode == 550) {
1032 errorcode = ERR_WRITE_ACCESS_DENIED;
1033 errormessage = _path;
1034 } else {
1035 // The error code here depends on the command
1036 errormessage = _path + i18n("\nThe server said: \"%1\"", QString::fromUtf8(ftpResponse(0)).trimmed());
1037 }
1038 }
1039
1040 else {
1041 // Only now we know for sure that we can resume
1042 if (_offset > 0 && qstrcmp(str1: _command, str2: "retr") == 0) {
1043 q->canResume();
1044 }
1045
1046 if (m_server && !m_data) {
1047 qCDebug(KIO_FTP) << "waiting for connection from remote.";
1048 m_server->waitForNewConnection(msec: DEFAULT_CONNECT_TIMEOUT * 1000);
1049 m_data = m_server->nextPendingConnection();
1050 }
1051
1052 if (m_data) {
1053 qCDebug(KIO_FTP) << "connected with remote.";
1054 m_bBusy = true; // cleared in ftpCloseCommand
1055 return Result::pass();
1056 }
1057
1058 qCDebug(KIO_FTP) << "no connection received from remote.";
1059 errorcode = ERR_CANNOT_ACCEPT;
1060 errormessage = m_host;
1061 }
1062
1063 if (errorcode != KJob::NoError) {
1064 return Result::fail(error: errorcode, errorString: errormessage);
1065 }
1066 return Result::fail();
1067}
1068
1069bool FtpInternal::ftpCloseCommand()
1070{
1071 // first close data sockets (if opened), then read response that
1072 // we got for whatever was used in ftpOpenCommand ( should be 226 )
1073 ftpCloseDataConnection();
1074
1075 if (!m_bBusy) {
1076 return true;
1077 }
1078
1079 qCDebug(KIO_FTP) << "ftpCloseCommand: reading command result";
1080 m_bBusy = false;
1081
1082 if (!ftpResponse(iOffset: -1) || (m_iRespType != 2)) {
1083 qCDebug(KIO_FTP) << "ftpCloseCommand: no transfer complete message";
1084 return false;
1085 }
1086 return true;
1087}
1088
1089Result FtpInternal::mkdir(const QUrl &url, int permissions)
1090{
1091 auto result = ftpOpenConnection(loginMode: LoginMode::Implicit);
1092 if (!result.success()) {
1093 return result;
1094 }
1095
1096 const QByteArray encodedPath(q->remoteEncoding()->encode(url));
1097 const QString path = QString::fromLatin1(str: encodedPath.constData(), size: encodedPath.size());
1098
1099 if (!ftpSendCmd(cmd: (QByteArrayLiteral("mkd ") + encodedPath)) || (m_iRespType != 2)) {
1100 QString currentPath(m_currentPath);
1101
1102 // Check whether or not mkdir failed because
1103 // the directory already exists...
1104 if (ftpFolder(path)) {
1105 const QString &failedPath = path;
1106 // Change the directory back to what it was...
1107 (void)ftpFolder(path: currentPath);
1108 return Result::fail(error: ERR_DIR_ALREADY_EXIST, errorString: failedPath);
1109 }
1110
1111 return Result::fail(error: ERR_CANNOT_MKDIR, errorString: path);
1112 }
1113
1114 if (permissions != -1) {
1115 // chmod the dir we just created, ignoring errors.
1116 (void)ftpChmod(path, permissions);
1117 }
1118
1119 return Result::pass();
1120}
1121
1122Result FtpInternal::rename(const QUrl &src, const QUrl &dst, KIO::JobFlags flags)
1123{
1124 const auto result = ftpOpenConnection(loginMode: LoginMode::Implicit);
1125 if (!result.success()) {
1126 return result;
1127 }
1128
1129 // The actual functionality is in ftpRename because put needs it
1130 return ftpRename(src: src.path(), dst: dst.path(), flags);
1131}
1132
1133Result FtpInternal::ftpRename(const QString &src, const QString &dst, KIO::JobFlags jobFlags)
1134{
1135 Q_ASSERT(m_bLoggedOn);
1136
1137 // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793).
1138 if (!(jobFlags & KIO::Overwrite)) {
1139 if (ftpFileExists(path: dst)) {
1140 return Result::fail(error: ERR_FILE_ALREADY_EXIST, errorString: dst);
1141 }
1142 }
1143
1144 if (ftpFolder(path: dst)) {
1145 return Result::fail(error: ERR_DIR_ALREADY_EXIST, errorString: dst);
1146 }
1147
1148 // CD into parent folder
1149 const int pos = src.lastIndexOf(c: QLatin1Char('/'));
1150 if (pos >= 0) {
1151 if (!ftpFolder(path: src.left(n: pos + 1))) {
1152 return Result::fail(error: ERR_CANNOT_ENTER_DIRECTORY, errorString: src);
1153 }
1154 }
1155
1156 const QByteArray from_cmd = "RNFR " + q->remoteEncoding()->encode(name: src.mid(position: pos + 1));
1157 if (!ftpSendCmd(cmd: from_cmd) || (m_iRespType != 3)) {
1158 return Result::fail(error: ERR_CANNOT_RENAME, errorString: src);
1159 }
1160
1161 const QByteArray to_cmd = "RNTO " + q->remoteEncoding()->encode(name: dst);
1162 if (!ftpSendCmd(cmd: to_cmd) || (m_iRespType != 2)) {
1163 return Result::fail(error: ERR_CANNOT_RENAME, errorString: src);
1164 }
1165
1166 return Result::pass();
1167}
1168
1169Result FtpInternal::del(const QUrl &url, bool isfile)
1170{
1171 auto result = ftpOpenConnection(loginMode: LoginMode::Implicit);
1172 if (!result.success()) {
1173 return result;
1174 }
1175
1176 // When deleting a directory, we must exit from it first
1177 // The last command probably went into it (to stat it)
1178 if (!isfile) {
1179 (void)ftpFolder(path: q->remoteEncoding()->decode(name: q->remoteEncoding()->directory(url))); // ignore errors
1180 }
1181
1182 const QByteArray cmd = (isfile ? "DELE " : "RMD ") + q->remoteEncoding()->encode(url);
1183
1184 if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
1185 return Result::fail(error: ERR_CANNOT_DELETE, errorString: url.path());
1186 }
1187
1188 return Result::pass();
1189}
1190
1191bool FtpInternal::ftpChmod(const QString &path, int permissions)
1192{
1193 Q_ASSERT(m_bLoggedOn);
1194
1195 if (m_extControl & chmodUnknown) { // previous errors?
1196 return false;
1197 }
1198
1199 // we need to do bit AND 777 to get permissions, in case
1200 // we were sent a full mode (unlikely)
1201 const QByteArray cmd = "SITE CHMOD " + QByteArray::number(permissions & 0777 /*octal*/, base: 8 /*octal*/) + ' ' + q->remoteEncoding()->encode(name: path);
1202
1203 if (ftpSendCmd(cmd)) {
1204 qCDebug(KIO_FTP) << "ftpChmod: Failed to issue chmod";
1205 return false;
1206 }
1207
1208 if (m_iRespType == 2) {
1209 return true;
1210 }
1211
1212 if (m_iRespCode == 500) {
1213 m_extControl |= chmodUnknown;
1214 qCDebug(KIO_FTP) << "ftpChmod: CHMOD not supported - disabling";
1215 }
1216 return false;
1217}
1218
1219Result FtpInternal::chmod(const QUrl &url, int permissions)
1220{
1221 const auto result = ftpOpenConnection(loginMode: LoginMode::Implicit);
1222 if (!result.success()) {
1223 return result;
1224 }
1225
1226 if (!ftpChmod(path: url.path(), permissions)) {
1227 return Result::fail(error: ERR_CANNOT_CHMOD, errorString: url.path());
1228 }
1229
1230 return Result::pass();
1231}
1232
1233void FtpInternal::ftpCreateUDSEntry(const QString &filename, const FtpEntry &ftpEnt, UDSEntry &entry, bool isDir)
1234{
1235 Q_ASSERT(entry.count() == 0); // by contract :-)
1236
1237 entry.reserve(size: 9);
1238 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: filename);
1239 entry.fastInsert(field: KIO::UDSEntry::UDS_SIZE, l: ftpEnt.size);
1240 entry.fastInsert(field: KIO::UDSEntry::UDS_MODIFICATION_TIME, l: ftpEnt.date.toSecsSinceEpoch());
1241 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS, l: ftpEnt.access);
1242 entry.fastInsert(field: KIO::UDSEntry::UDS_USER, value: ftpEnt.owner);
1243 if (!ftpEnt.group.isEmpty()) {
1244 entry.fastInsert(field: KIO::UDSEntry::UDS_GROUP, value: ftpEnt.group);
1245 }
1246
1247 if (!ftpEnt.link.isEmpty()) {
1248 entry.fastInsert(field: KIO::UDSEntry::UDS_LINK_DEST, value: ftpEnt.link);
1249
1250 QMimeDatabase db;
1251 QMimeType mime = db.mimeTypeForUrl(url: QUrl(QLatin1String("ftp://host/") + filename));
1252 // Links on ftp sites are often links to dirs, and we have no way to check
1253 // that. Let's do like Netscape : assume dirs generally.
1254 // But we do this only when the MIME type can't be known from the filename.
1255 // --> we do better than Netscape :-)
1256 if (mime.isDefault()) {
1257 qCDebug(KIO_FTP) << "Setting guessed MIME type to inode/directory for " << filename;
1258 entry.fastInsert(field: KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, QStringLiteral("inode/directory"));
1259 isDir = true;
1260 }
1261 }
1262
1263 entry.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, l: isDir ? S_IFDIR : ftpEnt.type);
1264 // entry.insert KIO::UDSEntry::UDS_ACCESS_TIME,buff.st_atime);
1265 // entry.insert KIO::UDSEntry::UDS_CREATION_TIME,buff.st_ctime);
1266}
1267
1268void FtpInternal::ftpShortStatAnswer(const QString &filename, bool isDir)
1269{
1270 UDSEntry entry;
1271
1272 entry.reserve(size: 4);
1273 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: filename);
1274 entry.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, l: isDir ? S_IFDIR : S_IFREG);
1275 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1276 if (isDir) {
1277 entry.fastInsert(field: KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
1278 }
1279 // No details about size, ownership, group, etc.
1280
1281 q->statEntry(entry: entry);
1282}
1283
1284Result FtpInternal::ftpStatAnswerNotFound(const QString &path, const QString &filename)
1285{
1286 // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source")
1287 // When e.g. uploading a file, we still need stat() to return "not found"
1288 // when the file doesn't exist.
1289 QString statSide = q->metaData(QStringLiteral("statSide"));
1290 qCDebug(KIO_FTP) << "statSide=" << statSide;
1291 if (statSide == QLatin1String("source")) {
1292 qCDebug(KIO_FTP) << "Not found, but assuming found, because some servers don't allow listing";
1293 // MS Server is incapable of handling "list <blah>" in a case insensitive way
1294 // But "retr <blah>" works. So lie in stat(), to get going...
1295 //
1296 // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run
1297 // where listing permissions are denied, but downloading is still possible.
1298 ftpShortStatAnswer(filename, isDir: false /*file, not dir*/);
1299
1300 return Result::pass();
1301 }
1302
1303 return Result::fail(error: ERR_DOES_NOT_EXIST, errorString: path);
1304}
1305
1306Result FtpInternal::stat(const QUrl &url)
1307{
1308 qCDebug(KIO_FTP) << "path=" << url.path();
1309 auto result = ftpOpenConnection(loginMode: LoginMode::Implicit);
1310 if (!result.success()) {
1311 return result;
1312 }
1313
1314 const QString path = ftpCleanPath(path: QDir::cleanPath(path: url.path()));
1315 qCDebug(KIO_FTP) << "cleaned path=" << path;
1316
1317 // We can't stat root, but we know it's a dir.
1318 if (path.isEmpty() || path == QLatin1String("/")) {
1319 UDSEntry entry;
1320 entry.reserve(size: 6);
1321 // entry.insert( KIO::UDSEntry::UDS_NAME, UDSField( QString() ) );
1322 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, QStringLiteral("."));
1323 entry.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1324 entry.fastInsert(field: KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
1325 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1326 entry.fastInsert(field: KIO::UDSEntry::UDS_USER, QStringLiteral("root"));
1327 entry.fastInsert(field: KIO::UDSEntry::UDS_GROUP, QStringLiteral("root"));
1328 // no size
1329
1330 q->statEntry(entry: entry);
1331 return Result::pass();
1332 }
1333
1334 QUrl tempurl(url);
1335 tempurl.setPath(path); // take the clean one
1336 QString listarg; // = tempurl.directory(QUrl::ObeyTrailingSlash);
1337 QString parentDir;
1338 const QString filename = tempurl.fileName();
1339 Q_ASSERT(!filename.isEmpty());
1340
1341 // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info)
1342 // if it doesn't work, it's a file (and then we'll use dir filename)
1343 bool isDir = ftpFolder(path);
1344
1345 // if we're only interested in "file or directory", we should stop here
1346 QString sDetails = q->metaData(QStringLiteral("details"));
1347 int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
1348 qCDebug(KIO_FTP) << "details=" << details;
1349 if (details == 0) {
1350 if (!isDir && !ftpFileExists(path)) { // ok, not a dir -> is it a file ?
1351 // no -> it doesn't exist at all
1352 return ftpStatAnswerNotFound(path, filename);
1353 }
1354 ftpShortStatAnswer(filename, isDir);
1355 return Result::pass(); // successfully found a dir or a file -> done
1356 }
1357
1358 if (!isDir) {
1359 // It is a file or it doesn't exist, try going to parent directory
1360 parentDir = tempurl.adjusted(options: QUrl::RemoveFilename).path();
1361 // With files we can do "LIST <filename>" to avoid listing the whole dir
1362 listarg = filename;
1363 } else {
1364 // --- New implementation:
1365 // Don't list the parent dir. Too slow, might not show it, etc.
1366 // Just return that it's a dir.
1367 UDSEntry entry;
1368 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: filename);
1369 entry.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
1370 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
1371 // No clue about size, ownership, group, etc.
1372
1373 q->statEntry(entry: entry);
1374 return Result::pass();
1375 }
1376
1377 // Now cwd the parent dir, to prepare for listing
1378 if (!ftpFolder(path: parentDir)) {
1379 return Result::fail(error: ERR_CANNOT_ENTER_DIRECTORY, errorString: parentDir);
1380 }
1381
1382 result = ftpOpenCommand(command: "list", path: listarg, mode: 'I', errorcode: ERR_DOES_NOT_EXIST);
1383 if (!result.success()) {
1384 qCritical() << "COULD NOT LIST";
1385 return result;
1386 }
1387 qCDebug(KIO_FTP) << "Starting of list was ok";
1388
1389 Q_ASSERT(!filename.isEmpty() && filename != QLatin1String("/"));
1390
1391 bool bFound = false;
1392 QUrl linkURL;
1393 FtpEntry ftpEnt;
1394 QList<FtpEntry> ftpValidateEntList;
1395 while (ftpReadDir(ftpEnt)) {
1396 if (!ftpEnt.name.isEmpty() && ftpEnt.name.at(i: 0).isSpace()) {
1397 ftpValidateEntList.append(t: ftpEnt);
1398 continue;
1399 }
1400
1401 // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at)
1402 // return only the filename when doing "dir /full/path/to/file"
1403 if (!bFound) {
1404 bFound = maybeEmitStatEntry(ftpEnt, filename, isDir);
1405 }
1406 qCDebug(KIO_FTP) << ftpEnt.name;
1407 }
1408
1409 for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) {
1410 FtpEntry &ftpEnt = ftpValidateEntList[i];
1411 fixupEntryName(ftpEnt: &ftpEnt);
1412 if (maybeEmitStatEntry(ftpEnt, filename, isDir)) {
1413 break;
1414 }
1415 }
1416
1417 ftpCloseCommand(); // closes the data connection only
1418
1419 if (!bFound) {
1420 return ftpStatAnswerNotFound(path, filename);
1421 }
1422
1423 if (!linkURL.isEmpty()) {
1424 if (linkURL == url || linkURL == tempurl) {
1425 return Result::fail(error: ERR_CYCLIC_LINK, errorString: linkURL.toString());
1426 }
1427 return FtpInternal::stat(url: linkURL);
1428 }
1429
1430 qCDebug(KIO_FTP) << "stat : finished successfully";
1431 ;
1432 return Result::pass();
1433}
1434
1435bool FtpInternal::maybeEmitStatEntry(FtpEntry &ftpEnt, const QString &filename, bool isDir)
1436{
1437 if (filename == ftpEnt.name && !filename.isEmpty()) {
1438 UDSEntry entry;
1439 ftpCreateUDSEntry(filename, ftpEnt, entry, isDir);
1440 q->statEntry(entry: entry);
1441 return true;
1442 }
1443
1444 return false;
1445}
1446
1447Result FtpInternal::listDir(const QUrl &url)
1448{
1449 qCDebug(KIO_FTP) << url;
1450 auto result = ftpOpenConnection(loginMode: LoginMode::Implicit);
1451 if (!result.success()) {
1452 return result;
1453 }
1454
1455 // No path specified ?
1456 QString path = url.path();
1457 if (path.isEmpty()) {
1458 QUrl realURL;
1459 realURL.setScheme(QStringLiteral("ftp"));
1460 realURL.setUserName(userName: m_user);
1461 realURL.setPassword(password: m_pass);
1462 realURL.setHost(host: m_host);
1463 if (m_port > 0 && m_port != DEFAULT_FTP_PORT) {
1464 realURL.setPort(m_port);
1465 }
1466 if (m_initialPath.isEmpty()) {
1467 m_initialPath = QStringLiteral("/");
1468 }
1469 realURL.setPath(path: m_initialPath);
1470 qCDebug(KIO_FTP) << "REDIRECTION to " << realURL;
1471 q->redirection(url: realURL);
1472 return Result::pass();
1473 }
1474
1475 qCDebug(KIO_FTP) << "hunting for path" << path;
1476
1477 result = ftpOpenDir(path);
1478 if (!result.success()) {
1479 if (ftpFileExists(path)) {
1480 return Result::fail(error: ERR_IS_FILE, errorString: path);
1481 }
1482 // not sure which to emit
1483 // error( ERR_DOES_NOT_EXIST, path );
1484 return Result::fail(error: ERR_CANNOT_ENTER_DIRECTORY, errorString: path);
1485 }
1486
1487 UDSEntry entry;
1488 FtpEntry ftpEnt;
1489 QList<FtpEntry> ftpValidateEntList;
1490 while (ftpReadDir(ftpEnt)) {
1491 qCDebug(KIO_FTP) << ftpEnt.name;
1492 // Q_ASSERT( !ftpEnt.name.isEmpty() );
1493 if (!ftpEnt.name.isEmpty()) {
1494 if (ftpEnt.name.at(i: 0).isSpace()) {
1495 ftpValidateEntList.append(t: ftpEnt);
1496 continue;
1497 }
1498
1499 // if ( S_ISDIR( (mode_t)ftpEnt.type ) )
1500 // qDebug() << "is a dir";
1501 // if ( !ftpEnt.link.isEmpty() )
1502 // qDebug() << "is a link to " << ftpEnt.link;
1503 ftpCreateUDSEntry(filename: ftpEnt.name, ftpEnt, entry, isDir: false);
1504 q->listEntry(entry);
1505 entry.clear();
1506 }
1507 }
1508
1509 for (int i = 0, count = ftpValidateEntList.count(); i < count; ++i) {
1510 FtpEntry &ftpEnt = ftpValidateEntList[i];
1511 fixupEntryName(ftpEnt: &ftpEnt);
1512 ftpCreateUDSEntry(filename: ftpEnt.name, ftpEnt, entry, isDir: false);
1513 q->listEntry(entry);
1514 entry.clear();
1515 }
1516
1517 ftpCloseCommand(); // closes the data connection only
1518 return Result::pass();
1519}
1520
1521void FtpInternal::worker_status()
1522{
1523 qCDebug(KIO_FTP) << "Got worker_status host = " << (!m_host.toLatin1().isEmpty() ? m_host.toLatin1() : "[None]") << " ["
1524 << (m_bLoggedOn ? "Connected" : "Not connected") << "]";
1525 q->workerStatus(host: m_host, connected: m_bLoggedOn);
1526}
1527
1528Result FtpInternal::ftpOpenDir(const QString &path)
1529{
1530 // QString path( _url.path(QUrl::RemoveTrailingSlash) );
1531
1532 // We try to change to this directory first to see whether it really is a directory.
1533 // (And also to follow symlinks)
1534 QString tmp = path.isEmpty() ? QStringLiteral("/") : path;
1535
1536 // We get '550', whether it's a file or doesn't exist...
1537 if (!ftpFolder(path: tmp)) {
1538 return Result::fail();
1539 }
1540
1541 // Don't use the path in the list command:
1542 // We changed into this directory anyway - so it's enough just to send "list".
1543 // We use '-a' because the application MAY be interested in dot files.
1544 // The only way to really know would be to have a metadata flag for this...
1545 // Since some windows ftp server seems not to support the -a argument, we use a fallback here.
1546 // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com)
1547 // Pass KJob::NoError first because we don't want to emit error before we
1548 // have tried all commands.
1549 auto result = ftpOpenCommand(command: "list -la", path: QString(), mode: 'I', errorcode: KJob::NoError);
1550 if (!result.success()) {
1551 result = ftpOpenCommand(command: "list", path: QString(), mode: 'I', errorcode: KJob::NoError);
1552 }
1553 if (!result.success()) {
1554 // Servers running with Turkish locale having problems converting 'i' letter to upper case.
1555 // So we send correct upper case command as last resort.
1556 result = ftpOpenCommand(command: "LIST -la", path: QString(), mode: 'I', errorcode: ERR_CANNOT_ENTER_DIRECTORY);
1557 }
1558
1559 if (!result.success()) {
1560 qCWarning(KIO_FTP) << "Can't open for listing";
1561 return result;
1562 }
1563
1564 qCDebug(KIO_FTP) << "Starting of list was ok";
1565 return Result::pass();
1566}
1567
1568bool FtpInternal::ftpReadDir(FtpEntry &de)
1569{
1570 Q_ASSERT(m_data);
1571
1572 // get a line from the data connection ...
1573 while (true) {
1574 while (!m_data->canReadLine() && m_data->waitForReadyRead(msecs: (DEFAULT_READ_TIMEOUT * 1000))) { }
1575 QByteArray data = m_data->readLine();
1576 if (data.size() == 0) {
1577 break;
1578 }
1579
1580 const char *buffer = data.data();
1581 qCDebug(KIO_FTP) << "dir > " << buffer;
1582
1583 // Normally the listing looks like
1584 // -rw-r--r-- 1 dfaure dfaure 102 Nov 9 12:30 log
1585 // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442)
1586 // d [RWCEAFMS] Admin 512 Oct 13 2004 PSI
1587
1588 // we should always get the following 5 fields ...
1589 const char *p_access;
1590 const char *p_junk;
1591 const char *p_owner;
1592 const char *p_group;
1593 const char *p_size;
1594 if ((p_access = strtok(s: (char *)buffer, delim: " ")) == nullptr) {
1595 continue;
1596 }
1597 if ((p_junk = strtok(s: nullptr, delim: " ")) == nullptr) {
1598 continue;
1599 }
1600 if ((p_owner = strtok(s: nullptr, delim: " ")) == nullptr) {
1601 continue;
1602 }
1603 if ((p_group = strtok(s: nullptr, delim: " ")) == nullptr) {
1604 continue;
1605 }
1606 if ((p_size = strtok(s: nullptr, delim: " ")) == nullptr) {
1607 continue;
1608 }
1609
1610 qCDebug(KIO_FTP) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size;
1611
1612 de.access = 0;
1613 if (qstrlen(str: p_access) == 1 && p_junk[0] == '[') { // Netware
1614 de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions
1615 }
1616
1617 const char *p_date_1;
1618 const char *p_date_2;
1619 const char *p_date_3;
1620 const char *p_name;
1621
1622 // A special hack for "/dev". A listing may look like this:
1623 // crw-rw-rw- 1 root root 1, 5 Jun 29 1997 zero
1624 // So we just ignore the number in front of the ",". Ok, it is a hack :-)
1625 if (strchr(s: p_size, c: ',') != nullptr) {
1626 qCDebug(KIO_FTP) << "Size contains a ',' -> reading size again (/dev hack)";
1627 if ((p_size = strtok(s: nullptr, delim: " ")) == nullptr) {
1628 continue;
1629 }
1630 }
1631
1632 // This is needed for ftp servers with a directory listing like this (#375610):
1633 // drwxr-xr-x folder 0 Mar 15 15:50 directory_name
1634 if (strcmp(s1: p_junk, s2: "folder") == 0) {
1635 p_date_1 = p_group;
1636 p_date_2 = p_size;
1637 p_size = p_owner;
1638 p_group = nullptr;
1639 p_owner = nullptr;
1640 }
1641 // Check whether the size we just read was really the size
1642 // or a month (this happens when the server lists no group)
1643 // Used to be the case on sunsite.uio.no, but not anymore
1644 // This is needed for the Netware case, too.
1645 else if (!isdigit(*p_size)) {
1646 p_date_1 = p_size;
1647 p_date_2 = strtok(s: nullptr, delim: " ");
1648 p_size = p_group;
1649 p_group = nullptr;
1650 qCDebug(KIO_FTP) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1;
1651 } else {
1652 p_date_1 = strtok(s: nullptr, delim: " ");
1653 p_date_2 = strtok(s: nullptr, delim: " ");
1654 qCDebug(KIO_FTP) << "Size has a digit -> ok. p_date_1=" << p_date_1;
1655 }
1656
1657 if (p_date_1 != nullptr && p_date_2 != nullptr && (p_date_3 = strtok(s: nullptr, delim: " ")) != nullptr && (p_name = strtok(s: nullptr, delim: "\r\n")) != nullptr) {
1658 {
1659 QByteArray tmp(p_name);
1660 if (p_access[0] == 'l') {
1661 int i = tmp.lastIndexOf(bv: " -> ");
1662 if (i != -1) {
1663 de.link = q->remoteEncoding()->decode(name: p_name + i + 4);
1664 tmp.truncate(pos: i);
1665 } else {
1666 de.link.clear();
1667 }
1668 } else {
1669 de.link.clear();
1670 }
1671
1672 if (tmp.startsWith(c: '/')) { // listing on ftp://ftp.gnupg.org/ starts with '/'
1673 tmp.remove(index: 0, len: 1);
1674 }
1675
1676 if (tmp.indexOf(ch: '/') != -1) {
1677 continue; // Don't trick us!
1678 }
1679
1680 de.name = q->remoteEncoding()->decode(name: tmp);
1681 }
1682
1683 de.type = S_IFREG;
1684 switch (p_access[0]) {
1685 case 'd':
1686 de.type = S_IFDIR;
1687 break;
1688 case 's':
1689 de.type = S_IFSOCK;
1690 break;
1691 case 'b':
1692 de.type = S_IFBLK;
1693 break;
1694 case 'c':
1695 de.type = S_IFCHR;
1696 break;
1697 case 'l':
1698 de.type = S_IFREG;
1699 // we don't set S_IFLNK here. de.link says it.
1700 break;
1701 default:
1702 break;
1703 }
1704
1705 if (p_access[1] == 'r') {
1706 de.access |= S_IRUSR;
1707 }
1708 if (p_access[2] == 'w') {
1709 de.access |= S_IWUSR;
1710 }
1711 if (p_access[3] == 'x' || p_access[3] == 's') {
1712 de.access |= S_IXUSR;
1713 }
1714 if (p_access[4] == 'r') {
1715 de.access |= S_IRGRP;
1716 }
1717 if (p_access[5] == 'w') {
1718 de.access |= S_IWGRP;
1719 }
1720 if (p_access[6] == 'x' || p_access[6] == 's') {
1721 de.access |= S_IXGRP;
1722 }
1723 if (p_access[7] == 'r') {
1724 de.access |= S_IROTH;
1725 }
1726 if (p_access[8] == 'w') {
1727 de.access |= S_IWOTH;
1728 }
1729 if (p_access[9] == 'x' || p_access[9] == 't') {
1730 de.access |= S_IXOTH;
1731 }
1732 if (p_access[3] == 's' || p_access[3] == 'S') {
1733 de.access |= S_ISUID;
1734 }
1735 if (p_access[6] == 's' || p_access[6] == 'S') {
1736 de.access |= S_ISGID;
1737 }
1738 if (p_access[9] == 't' || p_access[9] == 'T') {
1739 de.access |= S_ISVTX;
1740 }
1741
1742 de.owner = q->remoteEncoding()->decode(name: p_owner);
1743 de.group = q->remoteEncoding()->decode(name: p_group);
1744 de.size = charToLongLong(p_size);
1745
1746 // Parsing the date is somewhat tricky
1747 // Examples : "Oct 6 22:49", "May 13 1999"
1748
1749 // First get current date - we need the current month and year
1750 QDate currentDate(QDate::currentDate());
1751 int currentMonth = currentDate.month();
1752 int day = currentDate.day();
1753 int month = currentDate.month();
1754 int year = currentDate.year();
1755 int minute = 0;
1756 int hour = 0;
1757 // Get day number (always second field)
1758 if (p_date_2) {
1759 day = atoi(nptr: p_date_2);
1760 }
1761 // Get month from first field
1762 // NOTE : no, we don't want to use KLocale here
1763 // It seems all FTP servers use the English way
1764 qCDebug(KIO_FTP) << "Looking for month " << p_date_1;
1765 static const char s_months[][4] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
1766 for (int c = 0; c < 12; c++) {
1767 if (!qstrcmp(str1: p_date_1, str2: s_months[c])) {
1768 qCDebug(KIO_FTP) << "Found month " << c << " for " << p_date_1;
1769 month = c + 1;
1770 break;
1771 }
1772 }
1773
1774 // Parse third field
1775 if (p_date_3 && !strchr(s: p_date_3, c: ':')) { // No colon, looks like a year
1776 year = atoi(nptr: p_date_3);
1777 } else {
1778 // otherwise, the year is implicit
1779 // according to man ls, this happens when it is between than 6 months
1780 // old and 1 hour in the future.
1781 // So the year is : current year if tm_mon <= currentMonth+1
1782 // otherwise current year minus one
1783 // (The +1 is a security for the "+1 hour" at the end of the month issue)
1784 if (month > currentMonth + 1) {
1785 year--;
1786 }
1787
1788 // and p_date_3 contains probably a time
1789 char *semicolon;
1790 if (p_date_3 && (semicolon = (char *)strchr(s: p_date_3, c: ':'))) {
1791 *semicolon = '\0';
1792 minute = atoi(nptr: semicolon + 1);
1793 hour = atoi(nptr: p_date_3);
1794 } else {
1795 qCWarning(KIO_FTP) << "Can't parse third field " << p_date_3;
1796 }
1797 }
1798
1799 de.date = QDateTime(QDate(year, month, day), QTime(hour, minute));
1800 qCDebug(KIO_FTP) << de.date;
1801 return true;
1802 }
1803 } // line invalid, loop to get another line
1804 return false;
1805}
1806
1807//===============================================================================
1808// public: get download file from server
1809// helper: ftpGet called from get() and copy()
1810//===============================================================================
1811Result FtpInternal::get(const QUrl &url)
1812{
1813 qCDebug(KIO_FTP) << url;
1814 const Result result = ftpGet(iCopyFile: -1, sCopyFile: QString(), url, hCopyOffset: 0);
1815 ftpCloseCommand(); // must close command!
1816 return result;
1817}
1818
1819Result FtpInternal::ftpGet(int iCopyFile, const QString &sCopyFile, const QUrl &url, KIO::fileoffset_t llOffset)
1820{
1821 auto result = ftpOpenConnection(loginMode: LoginMode::Implicit);
1822 if (!result.success()) {
1823 return result;
1824 }
1825
1826 // Try to find the size of the file (and check that it exists at
1827 // the same time). If we get back a 550, "File does not exist"
1828 // or "not a plain file", check if it is a directory. If it is a
1829 // directory, return an error; otherwise simply try to retrieve
1830 // the request...
1831 if (!ftpSize(path: url.path(), mode: '?') && (m_iRespCode == 550) && ftpFolder(path: url.path())) {
1832 // Ok it's a dir in fact
1833 qCDebug(KIO_FTP) << "it is a directory in fact";
1834 return Result::fail(error: ERR_IS_DIRECTORY);
1835 }
1836
1837 QString resumeOffset = q->metaData(QStringLiteral("range-start"));
1838 if (resumeOffset.isEmpty()) {
1839 resumeOffset = q->metaData(QStringLiteral("resume")); // old name
1840 }
1841 if (!resumeOffset.isEmpty()) {
1842 llOffset = resumeOffset.toLongLong();
1843 qCDebug(KIO_FTP) << "got offset from metadata : " << llOffset;
1844 }
1845
1846 result = ftpOpenCommand(command: "retr", path: url.path(), mode: '?', errorcode: ERR_CANNOT_OPEN_FOR_READING, offset: llOffset);
1847 if (!result.success()) {
1848 qCWarning(KIO_FTP) << "Can't open for reading";
1849 return result;
1850 }
1851
1852 // Read the size from the response string
1853 if (m_size == UnknownSize) {
1854 const char *psz = strrchr(s: ftpResponse(iOffset: 4), c: '(');
1855 if (psz) {
1856 m_size = charToLongLong(psz + 1);
1857 }
1858 if (!m_size) {
1859 m_size = UnknownSize;
1860 }
1861 }
1862
1863 // Send the MIME type...
1864 if (iCopyFile == -1) {
1865 const auto result = ftpSendMimeType(url);
1866 if (!result.success()) {
1867 return result;
1868 }
1869 }
1870
1871 KIO::filesize_t bytesLeft = 0;
1872 if (m_size != UnknownSize) {
1873 bytesLeft = m_size - llOffset;
1874 q->totalSize(bytes: m_size); // emit the total size...
1875 }
1876
1877 qCDebug(KIO_FTP) << "starting with offset=" << llOffset;
1878 KIO::fileoffset_t processed_size = llOffset;
1879
1880 QByteArray array;
1881 char buffer[maximumIpcSize];
1882 // start with small data chunks in case of a slow data source (modem)
1883 // - unfortunately this has a negative impact on performance for large
1884 // - files - so we will increase the block size after a while ...
1885 int iBlockSize = initialIpcSize;
1886 int iBufferCur = 0;
1887
1888 while (m_size == UnknownSize || bytesLeft > 0) {
1889 // let the buffer size grow if the file is larger 64kByte ...
1890 if (processed_size - llOffset > 1024 * 64) {
1891 iBlockSize = maximumIpcSize;
1892 }
1893
1894 // read the data and detect EOF or error ...
1895 if (iBlockSize + iBufferCur > (int)sizeof(buffer)) {
1896 iBlockSize = sizeof(buffer) - iBufferCur;
1897 }
1898 if (m_data->bytesAvailable() == 0) {
1899 m_data->waitForReadyRead(msecs: (DEFAULT_READ_TIMEOUT * 1000));
1900 }
1901 int n = m_data->read(data: buffer + iBufferCur, maxlen: iBlockSize);
1902 if (n <= 0) {
1903 // this is how we detect EOF in case of unknown size
1904 if (m_size == UnknownSize && n == 0) {
1905 break;
1906 }
1907 // unexpected eof. Happens when the daemon gets killed.
1908 return Result::fail(error: ERR_CANNOT_READ);
1909 }
1910 processed_size += n;
1911
1912 // collect very small data chunks in buffer before processing ...
1913 if (m_size != UnknownSize) {
1914 bytesLeft -= n;
1915 iBufferCur += n;
1916 if (iBufferCur < minimumMimeSize && bytesLeft > 0) {
1917 q->processedSize(bytes: processed_size);
1918 continue;
1919 }
1920 n = iBufferCur;
1921 iBufferCur = 0;
1922 }
1923
1924 // write output file or pass to data pump ...
1925 int writeError = 0;
1926 if (iCopyFile == -1) {
1927 array = QByteArray::fromRawData(data: buffer, size: n);
1928 q->data(data: array);
1929 array.clear();
1930 } else if ((writeError = WriteToFile(fd: iCopyFile, buf: buffer, len: n)) != 0) {
1931 return Result::fail(error: writeError, errorString: sCopyFile);
1932 }
1933
1934 Q_ASSERT(processed_size >= 0);
1935 q->processedSize(bytes: static_cast<KIO::filesize_t>(processed_size));
1936 }
1937
1938 qCDebug(KIO_FTP) << "done";
1939 if (iCopyFile == -1) { // must signal EOF to data pump ...
1940 q->data(data: array); // array is empty and must be empty!
1941 }
1942
1943 q->processedSize(bytes: m_size == UnknownSize ? processed_size : m_size);
1944 return Result::pass();
1945}
1946
1947//===============================================================================
1948// public: put upload file to server
1949// helper: ftpPut called from put() and copy()
1950//===============================================================================
1951Result FtpInternal::put(const QUrl &url, int permissions, KIO::JobFlags flags)
1952{
1953 qCDebug(KIO_FTP) << url;
1954 const auto result = ftpPut(iCopyFile: -1, url, permissions, flags);
1955 ftpCloseCommand(); // must close command!
1956 return result;
1957}
1958
1959Result FtpInternal::ftpPut(int iCopyFile, const QUrl &dest_url, int permissions, KIO::JobFlags flags)
1960{
1961 const auto openResult = ftpOpenConnection(loginMode: LoginMode::Implicit);
1962 if (!openResult.success()) {
1963 return openResult;
1964 }
1965
1966 // Don't use mark partial over anonymous FTP.
1967 // My incoming dir allows put but not rename...
1968 bool bMarkPartial;
1969 if (m_user.isEmpty() || m_user == QLatin1String(s_ftpLogin)) {
1970 bMarkPartial = false;
1971 } else {
1972 bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), defaultValue: true);
1973 }
1974
1975 QString dest_orig = dest_url.path();
1976 const QString dest_part = dest_orig + QLatin1String(".part");
1977
1978 if (ftpSize(path: dest_orig, mode: 'I')) {
1979 if (m_size == 0) {
1980 // delete files with zero size
1981 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(name: dest_orig);
1982 if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
1983 return Result::fail(error: ERR_CANNOT_DELETE_PARTIAL, errorString: QString());
1984 }
1985 } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
1986 return Result::fail(error: ERR_FILE_ALREADY_EXIST, errorString: QString());
1987 } else if (bMarkPartial) {
1988 // when using mark partial, append .part extension
1989 const auto result = ftpRename(src: dest_orig, dst: dest_part, jobFlags: KIO::Overwrite);
1990 if (!result.success()) {
1991 return Result::fail(error: ERR_CANNOT_RENAME_PARTIAL, errorString: QString());
1992 }
1993 }
1994 // Don't chmod an existing file
1995 permissions = -1;
1996 } else if (bMarkPartial && ftpSize(path: dest_part, mode: 'I')) {
1997 // file with extension .part exists
1998 if (m_size == 0) {
1999 // delete files with zero size
2000 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(name: dest_part);
2001 if (!ftpSendCmd(cmd) || (m_iRespType != 2)) {
2002 return Result::fail(error: ERR_CANNOT_DELETE_PARTIAL, errorString: QString());
2003 }
2004 } else if (!(flags & KIO::Overwrite) && !(flags & KIO::Resume)) {
2005 flags |= q->canResume(offset: m_size) ? KIO::Resume : KIO::DefaultFlags;
2006 if (!(flags & KIO::Resume)) {
2007 return Result::fail(error: ERR_FILE_ALREADY_EXIST, errorString: QString());
2008 }
2009 }
2010 } else {
2011 m_size = 0;
2012 }
2013
2014 QString dest;
2015
2016 // if we are using marking of partial downloads -> add .part extension
2017 if (bMarkPartial) {
2018 qCDebug(KIO_FTP) << "Adding .part extension to " << dest_orig;
2019 dest = dest_part;
2020 } else {
2021 dest = dest_orig;
2022 }
2023
2024 KIO::fileoffset_t offset = 0;
2025
2026 // set the mode according to offset
2027 if ((flags & KIO::Resume) && m_size > 0) {
2028 offset = m_size;
2029 if (iCopyFile != -1) {
2030 if (QT_LSEEK(fd: iCopyFile, offset: offset, SEEK_SET) < 0) {
2031 return Result::fail(error: ERR_CANNOT_RESUME, errorString: QString());
2032 }
2033 }
2034 }
2035
2036 const auto storResult = ftpOpenCommand(command: "stor", path: dest, mode: '?', errorcode: ERR_CANNOT_WRITE, offset: offset);
2037 if (!storResult.success()) {
2038 return storResult;
2039 }
2040
2041 qCDebug(KIO_FTP) << "ftpPut: starting with offset=" << offset;
2042 KIO::fileoffset_t processed_size = offset;
2043
2044 QByteArray buffer;
2045 int result;
2046 int iBlockSize = initialIpcSize;
2047 int writeError = 0;
2048 // Loop until we got 'dataEnd'
2049 do {
2050 if (iCopyFile == -1) {
2051 q->dataReq(); // Request for data
2052 result = q->readData(buffer);
2053 } else {
2054 // let the buffer size grow if the file is larger 64kByte ...
2055 if (processed_size - offset > 1024 * 64) {
2056 iBlockSize = maximumIpcSize;
2057 }
2058 buffer.resize(size: iBlockSize);
2059 result = QT_READ(fd: iCopyFile, buf: buffer.data(), nbytes: buffer.size());
2060 if (result < 0) {
2061 writeError = ERR_CANNOT_READ;
2062 } else {
2063 buffer.resize(size: result);
2064 }
2065 }
2066
2067 if (result > 0) {
2068 m_data->write(data: buffer);
2069 while (m_data->bytesToWrite() && m_data->waitForBytesWritten()) { }
2070 processed_size += result;
2071 q->processedSize(bytes: processed_size);
2072 }
2073 } while (result > 0);
2074
2075 if (result != 0) { // error
2076 ftpCloseCommand(); // don't care about errors
2077 qCDebug(KIO_FTP) << "Error during 'put'. Aborting.";
2078 if (bMarkPartial) {
2079 // Remove if smaller than minimum size
2080 if (ftpSize(path: dest, mode: 'I') && (processed_size < q->configValue(QStringLiteral("MinimumKeepSize"), defaultValue: DEFAULT_MINIMUM_KEEP_SIZE))) {
2081 const QByteArray cmd = "DELE " + q->remoteEncoding()->encode(name: dest);
2082 (void)ftpSendCmd(cmd);
2083 }
2084 }
2085 return Result::fail(error: writeError, errorString: dest_url.toString());
2086 }
2087
2088 if (!ftpCloseCommand()) {
2089 return Result::fail(error: ERR_CANNOT_WRITE);
2090 }
2091
2092 // after full download rename the file back to original name
2093 if (bMarkPartial) {
2094 qCDebug(KIO_FTP) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")";
2095 const auto result = ftpRename(src: dest, dst: dest_orig, jobFlags: KIO::Overwrite);
2096 if (!result.success()) {
2097 return Result::fail(error: ERR_CANNOT_RENAME_PARTIAL);
2098 }
2099 }
2100
2101 // set final permissions
2102 if (permissions != -1) {
2103 if (m_user == QLatin1String(s_ftpLogin)) {
2104 qCDebug(KIO_FTP) << "Trying to chmod over anonymous FTP ???";
2105 }
2106 // chmod the file we just put
2107 if (!ftpChmod(path: dest_orig, permissions)) {
2108 // To be tested
2109 // if ( m_user != s_ftpLogin )
2110 // warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) );
2111 }
2112 }
2113
2114 return Result::pass();
2115}
2116
2117/*! Use the SIZE command to get the file size.
2118 Warning : the size depends on the transfer mode, hence the second arg. */
2119bool FtpInternal::ftpSize(const QString &path, char mode)
2120{
2121 m_size = UnknownSize;
2122 if (!ftpDataMode(cMode: mode)) {
2123 return false;
2124 }
2125
2126 const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(name: path);
2127 if (!ftpSendCmd(cmd: buf) || (m_iRespType != 2)) {
2128 return false;
2129 }
2130
2131 // skip leading "213 " (response code)
2132 QByteArray psz(ftpResponse(iOffset: 4));
2133 if (psz.isEmpty()) {
2134 return false;
2135 }
2136 bool ok = false;
2137 m_size = psz.trimmed().toLongLong(ok: &ok);
2138 if (!ok) {
2139 m_size = UnknownSize;
2140 }
2141 return true;
2142}
2143
2144bool FtpInternal::ftpFileExists(const QString &path)
2145{
2146 const QByteArray buf = "SIZE " + q->remoteEncoding()->encode(name: path);
2147 if (!ftpSendCmd(cmd: buf) || (m_iRespType != 2)) {
2148 return false;
2149 }
2150
2151 // skip leading "213 " (response code)
2152 const char *psz = ftpResponse(iOffset: 4);
2153 return psz != nullptr;
2154}
2155
2156// Today the differences between ASCII and BINARY are limited to
2157// CR or CR/LF line terminators. Many servers ignore ASCII (like
2158// win2003 -or- vsftp with default config). In the early days of
2159// computing, when even text-files had structure, this stuff was
2160// more important.
2161// Theoretically "list" could return different results in ASCII
2162// and BINARY mode. But again, most servers ignore ASCII here.
2163bool FtpInternal::ftpDataMode(char cMode)
2164{
2165 if (cMode == '?') {
2166 cMode = m_bTextMode ? 'A' : 'I';
2167 } else if (cMode == 'a') {
2168 cMode = 'A';
2169 } else if (cMode != 'A') {
2170 cMode = 'I';
2171 }
2172
2173 qCDebug(KIO_FTP) << "want" << cMode << "has" << m_cDataMode;
2174 if (m_cDataMode == cMode) {
2175 return true;
2176 }
2177
2178 const QByteArray buf = QByteArrayLiteral("TYPE ") + cMode;
2179 if (!ftpSendCmd(cmd: buf) || (m_iRespType != 2)) {
2180 return false;
2181 }
2182 m_cDataMode = cMode;
2183 return true;
2184}
2185
2186bool FtpInternal::ftpFolder(const QString &path)
2187{
2188 QString newPath = path;
2189 int iLen = newPath.length();
2190 if (iLen > 1 && newPath[iLen - 1] == QLatin1Char('/')) {
2191 newPath.chop(n: 1);
2192 }
2193
2194 qCDebug(KIO_FTP) << "want" << newPath << "has" << m_currentPath;
2195 if (m_currentPath == newPath) {
2196 return true;
2197 }
2198
2199 const QByteArray tmp = "cwd " + q->remoteEncoding()->encode(name: newPath);
2200 if (!ftpSendCmd(cmd: tmp)) {
2201 return false; // connection failure
2202 }
2203 if (m_iRespType != 2) {
2204 return false; // not a folder
2205 }
2206 m_currentPath = newPath;
2207 return true;
2208}
2209
2210//===============================================================================
2211// public: copy don't use kio data pump if one side is a local file
2212// helper: ftpCopyPut called from copy() on upload
2213// helper: ftpCopyGet called from copy() on download
2214//===============================================================================
2215Result FtpInternal::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags)
2216{
2217 int iCopyFile = -1;
2218 bool bSrcLocal = src.isLocalFile();
2219 bool bDestLocal = dest.isLocalFile();
2220 QString sCopyFile;
2221
2222 Result result = Result::pass();
2223 if (bSrcLocal && !bDestLocal) { // File -> Ftp
2224 sCopyFile = src.toLocalFile();
2225 qCDebug(KIO_FTP) << "local file" << sCopyFile << "-> ftp" << dest.path();
2226 result = ftpCopyPut(iCopyFile, sCopyFile, url: dest, permissions, flags);
2227 } else if (!bSrcLocal && bDestLocal) { // Ftp -> File
2228 sCopyFile = dest.toLocalFile();
2229 qCDebug(KIO_FTP) << "ftp" << src.path() << "-> local file" << sCopyFile;
2230 result = ftpCopyGet(iCopyFile, sCopyFile, url: src, permissions, flags);
2231 } else {
2232 return Result::fail(error: ERR_UNSUPPORTED_ACTION, errorString: QString());
2233 }
2234
2235 // perform clean-ups and report error (if any)
2236 if (iCopyFile != -1) {
2237 QT_CLOSE(fd: iCopyFile);
2238 }
2239 ftpCloseCommand(); // must close command!
2240
2241 return result;
2242}
2243
2244bool FtpInternal::isSocksProxyScheme(const QString &scheme)
2245{
2246 return scheme == QLatin1String("socks") || scheme == QLatin1String("socks5");
2247}
2248
2249bool FtpInternal::isSocksProxy() const
2250{
2251 return isSocksProxyScheme(scheme: m_proxyURL.scheme());
2252}
2253
2254Result FtpInternal::ftpCopyPut(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags)
2255{
2256 // check if source is ok ...
2257 QFileInfo info(sCopyFile);
2258 bool bSrcExists = info.exists();
2259 if (bSrcExists) {
2260 if (info.isDir()) {
2261 return Result::fail(error: ERR_IS_DIRECTORY);
2262 }
2263 } else {
2264 return Result::fail(error: ERR_DOES_NOT_EXIST);
2265 }
2266
2267 iCopyFile = QT_OPEN(file: QFile::encodeName(fileName: sCopyFile).constData(), O_RDONLY);
2268 if (iCopyFile == -1) {
2269 return Result::fail(error: ERR_CANNOT_OPEN_FOR_READING);
2270 }
2271
2272 // delegate the real work (iError gets status) ...
2273 q->totalSize(bytes: info.size());
2274 if (s_enableCanResume) {
2275 return ftpPut(iCopyFile, dest_url: url, permissions, flags: flags & ~KIO::Resume);
2276 } else {
2277 return ftpPut(iCopyFile, dest_url: url, permissions, flags: flags | KIO::Resume);
2278 }
2279}
2280
2281Result FtpInternal::ftpCopyGet(int &iCopyFile, const QString &sCopyFile, const QUrl &url, int permissions, KIO::JobFlags flags)
2282{
2283 // check if destination is ok ...
2284 QFileInfo info(sCopyFile);
2285 const bool bDestExists = info.exists();
2286 if (bDestExists) {
2287 if (info.isDir()) {
2288 return Result::fail(error: ERR_IS_DIRECTORY);
2289 }
2290 if (!(flags & KIO::Overwrite)) {
2291 return Result::fail(error: ERR_FILE_ALREADY_EXIST);
2292 }
2293 }
2294
2295 // do we have a ".part" file?
2296 const QString sPart = sCopyFile + QLatin1String(".part");
2297 bool bResume = false;
2298 QFileInfo sPartInfo(sPart);
2299 const bool bPartExists = sPartInfo.exists();
2300 const bool bMarkPartial = q->configValue(QStringLiteral("MarkPartial"), defaultValue: true);
2301 const QString dest = bMarkPartial ? sPart : sCopyFile;
2302 if (bMarkPartial && bPartExists && sPartInfo.size() > 0) {
2303 // must not be a folder! please fix a similar bug in kio_file!!
2304 if (sPartInfo.isDir()) {
2305 return Result::fail(error: ERR_DIR_ALREADY_EXIST);
2306 }
2307 // doesn't work for copy? -> design flaw?
2308 bResume = s_enableCanResume ? q->canResume(offset: sPartInfo.size()) : true;
2309 }
2310
2311 if (bPartExists && !bResume) { // get rid of an unwanted ".part" file
2312 QFile::remove(fileName: sPart);
2313 }
2314
2315 // WABA: Make sure that we keep writing permissions ourselves,
2316 // otherwise we can be in for a surprise on NFS.
2317 mode_t initialMode;
2318 if (permissions >= 0) {
2319 initialMode = static_cast<mode_t>(permissions | S_IWUSR);
2320 } else {
2321 initialMode = 0666;
2322 }
2323
2324 // open the output file ...
2325 KIO::fileoffset_t hCopyOffset = 0;
2326 if (bResume) {
2327 iCopyFile = QT_OPEN(file: QFile::encodeName(fileName: sPart).constData(), O_RDWR); // append if resuming
2328 hCopyOffset = QT_LSEEK(fd: iCopyFile, offset: 0, SEEK_END);
2329 if (hCopyOffset < 0) {
2330 return Result::fail(error: ERR_CANNOT_RESUME);
2331 }
2332 qCDebug(KIO_FTP) << "resuming at " << hCopyOffset;
2333 } else {
2334 iCopyFile = QT_OPEN(file: QFile::encodeName(fileName: dest).constData(), O_CREAT | O_TRUNC | O_WRONLY, initialMode);
2335 }
2336
2337 if (iCopyFile == -1) {
2338 qCDebug(KIO_FTP) << "### COULD NOT WRITE " << sCopyFile;
2339 const int error = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED : ERR_CANNOT_OPEN_FOR_WRITING;
2340 return Result::fail(error: error);
2341 }
2342
2343 // delegate the real work (iError gets status) ...
2344 auto result = ftpGet(iCopyFile, sCopyFile, url, llOffset: hCopyOffset);
2345
2346 if (QT_CLOSE(fd: iCopyFile) == 0 && !result.success()) {
2347 // If closing the file failed but there isn't an error yet, switch
2348 // into an error!
2349 result = Result::fail(error: ERR_CANNOT_WRITE);
2350 }
2351 iCopyFile = -1;
2352
2353 // handle renaming or deletion of a partial file ...
2354 if (bMarkPartial) {
2355 if (result.success()) {
2356 // rename ".part" on success
2357 if (!QFile::rename(oldName: sPart, newName: sCopyFile)) {
2358 // If rename fails, try removing the destination first if it exists.
2359 if (!bDestExists || !(QFile::remove(fileName: sCopyFile) && QFile::rename(oldName: sPart, newName: sCopyFile))) {
2360 qCDebug(KIO_FTP) << "cannot rename " << sPart << " to " << sCopyFile;
2361 result = Result::fail(error: ERR_CANNOT_RENAME_PARTIAL);
2362 }
2363 }
2364 } else {
2365 sPartInfo.refresh();
2366 if (sPartInfo.exists()) { // should a very small ".part" be deleted?
2367 int size = q->configValue(QStringLiteral("MinimumKeepSize"), defaultValue: DEFAULT_MINIMUM_KEEP_SIZE);
2368 if (sPartInfo.size() < size) {
2369 QFile::remove(fileName: sPart);
2370 }
2371 }
2372 }
2373 }
2374
2375 if (result.success()) {
2376 const QString mtimeStr = q->metaData(QStringLiteral("modified"));
2377 if (!mtimeStr.isEmpty()) {
2378 QDateTime dt = QDateTime::fromString(string: mtimeStr, format: Qt::ISODate);
2379 if (dt.isValid()) {
2380 qCDebug(KIO_FTP) << "Updating modified timestamp to" << mtimeStr;
2381 struct utimbuf utbuf;
2382 info.refresh();
2383 utbuf.actime = info.lastRead().toSecsSinceEpoch(); // access time, unchanged
2384 utbuf.modtime = dt.toSecsSinceEpoch(); // modification time
2385 ::utime(file: QFile::encodeName(fileName: sCopyFile).constData(), file_times: &utbuf);
2386 }
2387 }
2388 }
2389
2390 return result;
2391}
2392
2393Result FtpInternal::ftpSendMimeType(const QUrl &url)
2394{
2395 const int totalSize = ((m_size == UnknownSize || m_size > 1024) ? 1024 : static_cast<int>(m_size));
2396 QByteArray buffer(totalSize, '\0');
2397
2398 while (true) {
2399 // Wait for content to be available...
2400 if (m_data->bytesAvailable() == 0 && !m_data->waitForReadyRead(msecs: (DEFAULT_READ_TIMEOUT * 1000))) {
2401 return Result::fail(error: ERR_CANNOT_READ, errorString: url.toString());
2402 }
2403
2404 const qint64 bytesRead = m_data->peek(data: buffer.data(), maxlen: totalSize);
2405
2406 // If we got a -1, it must be an error so return an error.
2407 if (bytesRead == -1) {
2408 return Result::fail(error: ERR_CANNOT_READ, errorString: url.toString());
2409 }
2410
2411 // If m_size is unknown, peek returns 0 (0 sized file ??), or peek returns size
2412 // equal to the size we want, then break.
2413 if (bytesRead == 0 || bytesRead == totalSize || m_size == UnknownSize) {
2414 break;
2415 }
2416 }
2417
2418 if (!buffer.isEmpty()) {
2419 QMimeDatabase db;
2420 QMimeType mime = db.mimeTypeForFileNameAndData(fileName: url.path(), data: buffer);
2421 qCDebug(KIO_FTP) << "Emitting MIME type" << mime.name();
2422 q->mimeType(type: mime.name()); // emit the MIME type...
2423 }
2424
2425 return Result::pass();
2426}
2427
2428void FtpInternal::fixupEntryName(FtpEntry *e)
2429{
2430 Q_ASSERT(e);
2431 if (e->type == S_IFDIR) {
2432 if (!ftpFolder(path: e->name)) {
2433 QString name(e->name.trimmed());
2434 if (ftpFolder(path: name)) {
2435 e->name = name;
2436 qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name;
2437 } else {
2438 int index = 0;
2439 while (e->name.at(i: index).isSpace()) {
2440 index++;
2441 name = e->name.mid(position: index);
2442 if (ftpFolder(path: name)) {
2443 qCDebug(KIO_FTP) << "fixing up directory name from" << e->name << "to" << name;
2444 e->name = name;
2445 break;
2446 }
2447 }
2448 }
2449 }
2450 } else {
2451 if (!ftpFileExists(path: e->name)) {
2452 QString name(e->name.trimmed());
2453 if (ftpFileExists(path: name)) {
2454 e->name = name;
2455 qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name;
2456 } else {
2457 int index = 0;
2458 while (e->name.at(i: index).isSpace()) {
2459 index++;
2460 name = e->name.mid(position: index);
2461 if (ftpFileExists(path: name)) {
2462 qCDebug(KIO_FTP) << "fixing up filename from" << e->name << "to" << name;
2463 e->name = name;
2464 break;
2465 }
2466 }
2467 }
2468 }
2469 }
2470}
2471
2472ConnectionResult FtpInternal::synchronousConnectToHost(const QString &host, quint16 port)
2473{
2474 const QUrl proxyUrl = m_proxyURL;
2475 QNetworkProxy proxy;
2476 if (!proxyUrl.isEmpty()) {
2477 proxy = QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyUrl.host(), static_cast<quint16>(proxyUrl.port(defaultPort: 0)), proxyUrl.userName(), proxyUrl.password());
2478 }
2479
2480 QTcpSocket *socket = new QSslSocket;
2481 socket->setProxy(proxy);
2482 socket->connectToHost(hostName: host, port);
2483 socket->waitForConnected(msecs: DEFAULT_CONNECT_TIMEOUT * 1000);
2484 const auto socketError = socket->error();
2485 if (socketError == QAbstractSocket::ProxyAuthenticationRequiredError) {
2486 AuthInfo info;
2487 info.url = proxyUrl;
2488 info.verifyPath = true; // ### whatever
2489
2490 if (!q->checkCachedAuthentication(info)) {
2491 info.prompt = i18n(
2492 "You need to supply a username and a password for "
2493 "the proxy server listed below before you are allowed "
2494 "to access any sites.");
2495 info.keepPassword = true;
2496 info.commentLabel = i18n("Proxy:");
2497 info.comment = i18n("<b>%1</b>", proxy.hostName());
2498
2499 const int errorCode = q->openPasswordDialog(info, i18n("Proxy Authentication Failed."));
2500 if (errorCode != KJob::NoError) {
2501 qCDebug(KIO_FTP) << "user canceled proxy authentication, or communication error." << errorCode;
2502 return ConnectionResult{.socket: socket, .result: Result::fail(error: errorCode, errorString: proxyUrl.toString())};
2503 }
2504 }
2505
2506 proxy.setUser(info.username);
2507 proxy.setPassword(info.password);
2508
2509 delete socket;
2510 socket = new QSslSocket;
2511 socket->setProxy(proxy);
2512 socket->connectToHost(hostName: host, port);
2513 socket->waitForConnected(msecs: DEFAULT_CONNECT_TIMEOUT * 1000);
2514
2515 if (socket->state() == QAbstractSocket::ConnectedState) {
2516 // reconnect with credentials was successful -> save data
2517 q->cacheAuthentication(info);
2518
2519 m_proxyURL.setUserName(userName: info.username);
2520 m_proxyURL.setPassword(password: info.password);
2521 }
2522 }
2523
2524 return ConnectionResult{.socket: socket, .result: Result::pass()};
2525}
2526
2527//===============================================================================
2528// Ftp
2529//===============================================================================
2530
2531Ftp::Ftp(const QByteArray &pool, const QByteArray &app)
2532 : WorkerBase(QByteArrayLiteral("ftp"), pool, app)
2533 , d(new FtpInternal(this))
2534{
2535}
2536
2537Ftp::~Ftp() = default;
2538
2539void Ftp::setHost(const QString &host, quint16 port, const QString &user, const QString &pass)
2540{
2541 d->setHost(host: host, port: port, user: user, pass: pass);
2542}
2543
2544KIO::WorkerResult Ftp::openConnection()
2545{
2546 return d->openConnection();
2547}
2548
2549void Ftp::closeConnection()
2550{
2551 d->closeConnection();
2552}
2553
2554KIO::WorkerResult Ftp::stat(const QUrl &url)
2555{
2556 return d->stat(url);
2557}
2558
2559KIO::WorkerResult Ftp::listDir(const QUrl &url)
2560{
2561 return d->listDir(url);
2562}
2563
2564KIO::WorkerResult Ftp::mkdir(const QUrl &url, int permissions)
2565{
2566 return d->mkdir(url, permissions);
2567}
2568
2569KIO::WorkerResult Ftp::rename(const QUrl &src, const QUrl &dst, JobFlags flags)
2570{
2571 return d->rename(src, dst, flags);
2572}
2573
2574KIO::WorkerResult Ftp::del(const QUrl &url, bool isfile)
2575{
2576 return d->del(url, isfile);
2577}
2578
2579KIO::WorkerResult Ftp::chmod(const QUrl &url, int permissions)
2580{
2581 return d->chmod(url, permissions);
2582}
2583
2584KIO::WorkerResult Ftp::get(const QUrl &url)
2585{
2586 return d->get(url);
2587}
2588
2589KIO::WorkerResult Ftp::put(const QUrl &url, int permissions, JobFlags flags)
2590{
2591 return d->put(url, permissions, flags);
2592}
2593
2594void Ftp::worker_status()
2595{
2596 d->worker_status();
2597}
2598
2599KIO::WorkerResult Ftp::copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
2600{
2601 return d->copy(src, dest, permissions, flags);
2602}
2603
2604QDebug operator<<(QDebug dbg, const Result &r)
2605
2606{
2607 QDebugStateSaver saver(dbg);
2608 dbg.nospace() << "Result("
2609 << "success=" << r.success() << ", err=" << r.error() << ", str=" << r.errorString() << ')';
2610 return dbg;
2611}
2612
2613// needed for JSON file embedding
2614#include "ftp.moc"
2615
2616#include "moc_ftp.cpp"
2617

source code of kio/src/kioworkers/ftp/ftp.cpp