• Skip to content
  • Skip to link menu
KDE 4.8 API Reference
  • KDE API Reference
  • kdelibs
  • KDE Home
  • Contact Us
 

KIOSlave

ftp.cpp
Go to the documentation of this file.
00001 // -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; c-file-style: "stroustrup" -*-
00002 /*  This file is part of the KDE libraries
00003     Copyright (C) 2000-2006 David Faure <faure@kde.org>
00004 
00005     This library is free software; you can redistribute it and/or
00006     modify it under the terms of the GNU Library General Public
00007     License as published by the Free Software Foundation; either
00008     version 2 of the License, or (at your option) any later version.
00009 
00010     This library is distributed in the hope that it will be useful,
00011     but WITHOUT ANY WARRANTY; without even the implied warranty of
00012     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
00013     Library General Public License for more details.
00014 
00015     You should have received a copy of the GNU Library General Public License
00016     along with this library; see the file COPYING.LIB.  If not, write to
00017     the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
00018     Boston, MA 02110-1301, USA.
00019 */
00020 
00021 /*
00022     Recommended reading explaining FTP details and quirks:
00023       http://cr.yp.to/ftp.html  (by D.J. Bernstein)
00024 
00025     RFC:
00026       RFC  959 "File Transfer Protocol (FTP)"
00027       RFC 1635 "How to Use Anonymous FTP"
00028       RFC 2428 "FTP Extensions for IPv6 and NATs" (defines EPRT and EPSV)
00029 */
00030 
00031 
00032 #define  KIO_FTP_PRIVATE_INCLUDE
00033 #include "ftp.h"
00034 
00035 #ifdef HAVE_SYS_TIME_H
00036 #include <sys/time.h>
00037 #endif
00038 
00039 #if TIME_WITH_SYS_TIME
00040 #include <ctime>
00041 #endif
00042 
00043 #include <cctype>
00044 #include <cerrno>
00045 #include <cstdlib>
00046 #include <cstring>
00047 
00048 #include <QtCore/QCoreApplication>
00049 #include <QtCore/QDir>
00050 #include <QtNetwork/QHostAddress>
00051 #include <QtNetwork/QTcpSocket>
00052 #include <QtNetwork/QTcpServer>
00053 #include <QtNetwork/QAuthenticator>
00054 
00055 #include <kdebug.h>
00056 #include <kglobal.h>
00057 #include <klocale.h>
00058 #include <kcomponentdata.h>
00059 #include <kmimetype.h>
00060 #include <kio/ioslave_defaults.h>
00061 #include <kio/slaveconfig.h>
00062 #include <kremoteencoding.h>
00063 #include <ksocketfactory.h>
00064 #include <kde_file.h>
00065 #include <kconfiggroup.h>
00066 
00067 #ifdef HAVE_STRTOLL
00068   #define charToLongLong(a) strtoll(a, 0, 10)
00069 #else
00070   #define charToLongLong(a) strtol(a, 0, 10)
00071 #endif
00072 
00073 #define FTP_LOGIN   "anonymous"
00074 #define FTP_PASSWD  "anonymous@"
00075 
00076 //#undef  kDebug
00077 #define ENABLE_CAN_RESUME
00078 
00079 static QString ftpCleanPath(const QString& path)
00080 {
00081     if (path.endsWith(QLatin1String(";type=A"), Qt::CaseInsensitive) ||
00082         path.endsWith(QLatin1String(";type=I"), Qt::CaseInsensitive) ||
00083         path.endsWith(QLatin1String(";type=D"), Qt::CaseInsensitive)) {
00084         return path.left((path.length() - strlen(";type=X")));
00085     }
00086 
00087     return path;
00088 }
00089 
00090 static char ftpModeFromPath(const QString& path, char defaultMode = '\0')
00091 {
00092     const int index = path.lastIndexOf(QLatin1String(";type="));
00093 
00094     if (index > -1 && (index+6) < path.size()) {
00095         const QChar mode = path.at(index+6);
00096         // kio_ftp supports only A (ASCII) and I(BINARY) modes.
00097         if (mode == QLatin1Char('A') || mode == QLatin1Char('a') ||
00098             mode == QLatin1Char('I') || mode == QLatin1Char('i')) {
00099             return mode.toUpper().toLatin1();
00100         }
00101     }
00102 
00103     return defaultMode;
00104 }
00105 
00106 static bool supportedProxyScheme(const QString& scheme)
00107 {
00108     return (scheme == QLatin1String("ftp") || scheme == QLatin1String("socks"));
00109 }
00110 
00111 static bool isSocksProxy()
00112 {
00113     return (QNetworkProxy::applicationProxy().type() == QNetworkProxy::Socks5Proxy);
00114 }
00115 
00116 
00117 // JPF: somebody should find a better solution for this or move this to KIO
00118 // JPF: anyhow, in KDE 3.2.0 I found diffent MAX_IPC_SIZE definitions!
00119 namespace KIO {
00120     enum buffersizes
00121     {  
00125         maximumIpcSize = 32 * 1024,
00130         initialIpcSize =  2 * 1024,
00134         minimumMimeSize =     1024
00135     };
00136 
00137     // JPF: this helper was derived from write_all in file.cc (FileProtocol).
00138     static // JPF: in ftp.cc we make it static
00146    int WriteToFile(int fd, const char *buf, size_t len)
00147    {
00148       while (len > 0)
00149       {  // JPF: shouldn't there be a KDE_write?
00150          ssize_t written = write(fd, buf, len);
00151          if (written >= 0)
00152          {   buf += written;
00153              len -= written;
00154              continue;
00155          }
00156          switch(errno)
00157          {   case EINTR:   continue;
00158              case EPIPE:   return ERR_CONNECTION_BROKEN;
00159              case ENOSPC:  return ERR_DISK_FULL;
00160              default:      return ERR_COULD_NOT_WRITE;
00161          }
00162       }
00163       return 0;
00164    }
00165 }
00166 
00167 KIO::filesize_t Ftp::UnknownSize = (KIO::filesize_t)-1;
00168 
00169 using namespace KIO;
00170 
00171 extern "C" int KDE_EXPORT kdemain( int argc, char **argv )
00172 {
00173   QCoreApplication app(argc, argv);
00174   KComponentData componentData( "kio_ftp", "kdelibs4" );
00175   ( void ) KGlobal::locale();
00176 
00177   kDebug(7102) << "Starting " << getpid();
00178 
00179   if (argc != 4)
00180   {
00181      fprintf(stderr, "Usage: kio_ftp protocol domain-socket1 domain-socket2\n");
00182      exit(-1);
00183   }
00184 
00185   Ftp slave(argv[2], argv[3]);
00186   slave.dispatchLoop();
00187 
00188   kDebug(7102) << "Done";
00189   return 0;
00190 }
00191 
00192 //===============================================================================
00193 // Ftp
00194 //===============================================================================
00195 
00196 Ftp::Ftp( const QByteArray &pool, const QByteArray &app )
00197     : SlaveBase( "ftp", pool, app )
00198 {
00199   // init the socket data
00200   m_data = m_control = NULL;
00201   m_server = NULL;
00202   ftpCloseControlConnection();
00203 
00204   // init other members
00205   m_port = 0;
00206   m_socketProxyAuth = 0;
00207 }
00208 
00209 
00210 Ftp::~Ftp()
00211 {
00212   kDebug(7102);
00213   closeConnection();
00214 }
00215 
00219 void Ftp::ftpCloseDataConnection()
00220 {
00221     delete m_data;
00222     m_data = NULL;
00223     delete m_server;
00224     m_server = NULL;
00225 }
00226 
00231 void Ftp::ftpCloseControlConnection()
00232 {
00233   m_extControl = 0;
00234   delete m_control;
00235   m_control = NULL;
00236   m_cDataMode = 0;
00237   m_bLoggedOn = false;    // logon needs control connction
00238   m_bTextMode = false;
00239   m_bBusy = false;
00240 }
00241 
00246 const char* Ftp::ftpResponse(int iOffset)
00247 {
00248   Q_ASSERT(m_control != NULL);    // must have control connection socket
00249   const char *pTxt = m_lastControlLine.data();
00250 
00251   // read the next line ...
00252   if(iOffset < 0)
00253   {
00254     int  iMore = 0;
00255     m_iRespCode = 0;
00256 
00257     // If the server sends a multiline response starting with
00258     // "nnn-text" we loop here until a final "nnn text" line is
00259     // reached. Only data from the final line will be stored.
00260     do {
00261       while (!m_control->canReadLine() && m_control->waitForReadyRead((readTimeout() * 1000))) {}
00262       m_lastControlLine = m_control->readLine();
00263       pTxt = m_lastControlLine.data();
00264       int iCode  = atoi(pTxt);
00265       if (iMore == 0) {
00266           // first line
00267           kDebug(7102) << "    > " << pTxt;
00268           if(iCode >= 100) {
00269               m_iRespCode = iCode;
00270               if (pTxt[3] == '-') {
00271                   // marker for a multiple line response
00272                   iMore = iCode;
00273               }
00274           } else {
00275               kWarning(7102) << "Cannot parse valid code from line" << pTxt;
00276           }
00277       } else {
00278           // multi-line
00279           kDebug(7102) << "    > " << pTxt;
00280           if (iCode >= 100 && iCode == iMore && pTxt[3] == ' ') {
00281               iMore = 0;
00282           }
00283       }
00284     } while(iMore != 0);
00285     kDebug(7102) << "resp> " << pTxt;
00286 
00287     m_iRespType = (m_iRespCode > 0) ? m_iRespCode / 100 : 0;
00288   }
00289 
00290   // return text with offset ...
00291   while(iOffset-- > 0 && pTxt[0])
00292     pTxt++;
00293   return pTxt;
00294 }
00295 
00296 
00297 void Ftp::closeConnection()
00298 {
00299   if(m_control != NULL || m_data != NULL)
00300     kDebug(7102) << "m_bLoggedOn=" << m_bLoggedOn << " m_bBusy=" << m_bBusy;
00301 
00302   if(m_bBusy)              // ftpCloseCommand not called
00303   {
00304     kWarning(7102) << "Abandoned data stream";
00305     ftpCloseDataConnection();
00306   }
00307 
00308   if(m_bLoggedOn)           // send quit
00309   {
00310     if( !ftpSendCmd( "quit", 0 ) || (m_iRespType != 2) )
00311       kWarning(7102) << "QUIT returned error: " << m_iRespCode;
00312   }
00313 
00314   // close the data and control connections ...
00315   ftpCloseDataConnection();
00316   ftpCloseControlConnection();
00317 }
00318 
00319 void Ftp::setHost( const QString& _host, quint16 _port, const QString& _user,
00320                    const QString& _pass )
00321 {
00322   kDebug(7102) << _host << "port=" << _port << "user=" << _user;
00323 
00324   m_proxyURL.clear();
00325   m_proxyUrls = config()->readEntry("ProxyUrls", QStringList());
00326   kDebug(7102) << "proxy urls:" << m_proxyUrls;
00327 
00328   if ( m_host != _host || m_port != _port ||
00329        m_user != _user || m_pass != _pass )
00330     closeConnection();
00331 
00332   m_host = _host;
00333   m_port = _port;
00334   m_user = _user;
00335   m_pass = _pass;
00336 }
00337 
00338 void Ftp::openConnection()
00339 {
00340   ftpOpenConnection(loginExplicit);
00341 }
00342 
00343 bool Ftp::ftpOpenConnection (LoginMode loginMode)
00344 {
00345   // check for implicit login if we are already logged on ...
00346   if(loginMode == loginImplicit && m_bLoggedOn)
00347   {
00348     Q_ASSERT(m_control != NULL);    // must have control connection socket
00349     return true;
00350   }
00351 
00352   kDebug(7102) << "host=" << m_host << ", port=" << m_port << ", user=" << m_user << "password= [password hidden]";
00353 
00354   infoMessage( i18n("Opening connection to host %1", m_host) );
00355 
00356   if ( m_host.isEmpty() )
00357   {
00358     error( ERR_UNKNOWN_HOST, QString() );
00359     return false;
00360   }
00361 
00362   Q_ASSERT( !m_bLoggedOn );
00363 
00364   m_initialPath.clear();
00365   m_currentPath.clear();
00366 
00367   if (!ftpOpenControlConnection() )
00368     return false;          // error emitted by ftpOpenControlConnection
00369   infoMessage( i18n("Connected to host %1", m_host) );
00370 
00371   bool userNameChanged = false;
00372   if(loginMode != loginDefered)
00373   {
00374     m_bLoggedOn = ftpLogin(&userNameChanged);
00375     if( !m_bLoggedOn )
00376       return false;       // error emitted by ftpLogin
00377   }
00378 
00379   m_bTextMode = config()->readEntry("textmode", false);
00380   connected();
00381 
00382   // Redirected due to credential change...
00383   if (userNameChanged && m_bLoggedOn)
00384   {
00385     KUrl realURL;
00386     realURL.setProtocol( "ftp" );
00387     if (m_user != FTP_LOGIN)
00388       realURL.setUser( m_user );
00389     if (m_pass != FTP_PASSWD)
00390       realURL.setPass( m_pass );
00391     realURL.setHost( m_host );
00392     if ( m_port > 0 && m_port != DEFAULT_FTP_PORT )
00393       realURL.setPort( m_port );
00394     if ( m_initialPath.isEmpty() )
00395       m_initialPath = '/';
00396     realURL.setPath( m_initialPath );
00397     kDebug(7102) << "User name changed! Redirecting to" << realURL.prettyUrl();
00398     redirection( realURL );
00399     finished();
00400     return false;
00401   }
00402 
00403   return true;
00404 }
00405 
00406 
00412 bool Ftp::ftpOpenControlConnection()
00413 {
00414   if (m_proxyUrls.isEmpty())
00415       return ftpOpenControlConnection(m_host, m_port);
00416 
00417   int errorCode = 0;
00418   QString errorMessage;
00419 
00420   Q_FOREACH (const QString& proxyUrl, m_proxyUrls) {
00421     const KUrl url (proxyUrl);
00422     const QString scheme (url.protocol());
00423 
00424     if (!supportedProxyScheme(scheme)) {
00425       // TODO: Need a new error code to indicate unsupported URL scheme.
00426       errorCode = ERR_COULD_NOT_CONNECT;
00427       errorMessage = url.url();
00428       continue;
00429     }
00430 
00431     if (scheme == QLatin1String("socks")) {
00432       kDebug(7102) << "Connecting to SOCKS proxy @" << url;
00433       const int proxyPort = url.port();
00434       QNetworkProxy proxy (QNetworkProxy::Socks5Proxy, url.host(), (proxyPort == -1 ? 0 : proxyPort));
00435       QNetworkProxy::setApplicationProxy(proxy);
00436       if (ftpOpenControlConnection(m_host, m_port)) {
00437         return true;
00438       }
00439       QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
00440     } else {
00441       if (ftpOpenControlConnection(url.host(), url.port())) {
00442         m_proxyURL = url;
00443         return true;
00444       }
00445     }
00446   }
00447 
00448   if (errorCode) {
00449     error(errorCode, errorMessage);
00450   }
00451 
00452   return false;
00453 }
00454 
00455 bool Ftp::ftpOpenControlConnection( const QString &host, int port )
00456 {
00457   // implicitly close, then try to open a new connection ...
00458   closeConnection();
00459   QString sErrorMsg;
00460 
00461   // now connect to the server and read the login message ...
00462   if (port == 0)
00463     port = 21;                  // default FTP port
00464   m_control = KSocketFactory::synchronousConnectToHost(QLatin1String("ftp"), host, port, connectTimeout() * 1000);
00465   connect(m_control, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)),
00466           this, SLOT(proxyAuthentication(QNetworkProxy,QAuthenticator*)));
00467   int iErrorCode = m_control->state() == QAbstractSocket::ConnectedState ? 0 : ERR_COULD_NOT_CONNECT;
00468 
00469   // on connect success try to read the server message...
00470   if(iErrorCode == 0)
00471   {
00472     const char* psz = ftpResponse(-1);
00473     if(m_iRespType != 2)
00474     { // login not successful, do we have an message text?
00475       if(psz[0])
00476         sErrorMsg = i18n("%1.\n\nReason: %2", host, psz);
00477       iErrorCode = ERR_COULD_NOT_CONNECT;
00478     }
00479   }
00480   else
00481   {
00482     if (m_control->error() == QAbstractSocket::HostNotFoundError)
00483       iErrorCode = ERR_UNKNOWN_HOST;
00484 
00485     sErrorMsg = QString("%1: %2").arg(host).arg(m_control->errorString());
00486   }
00487 
00488   // if there was a problem - report it ...
00489   if(iErrorCode == 0)             // OK, return success
00490     return true;
00491   closeConnection();              // clean-up on error
00492   error(iErrorCode, sErrorMsg);
00493   return false;
00494 }
00495 
00503 bool Ftp::ftpLogin(bool* userChanged)
00504 {
00505   infoMessage( i18n("Sending login information") );
00506 
00507   Q_ASSERT( !m_bLoggedOn );
00508 
00509   QString user (m_user);
00510   QString pass (m_pass);
00511 
00512   if ( config()->readEntry("EnableAutoLogin", false) )
00513   {
00514     QString au = config()->readEntry("autoLoginUser");
00515     if ( !au.isEmpty() )
00516     {
00517         user = au;
00518         pass = config()->readEntry("autoLoginPass");
00519     }
00520   }
00521 
00522   AuthInfo info;
00523   info.url.setProtocol( "ftp" );
00524   info.url.setHost( m_host );
00525   if ( m_port > 0 && m_port != DEFAULT_FTP_PORT )
00526       info.url.setPort( m_port );
00527   if (!user.isEmpty())
00528       info.url.setUser(user);
00529 
00530   // Check for cached authentication first and fallback to
00531   // anonymous login when no stored credentials are found.
00532   if (!config()->readEntry("TryAnonymousLoginFirst", false) &&
00533       pass.isEmpty() && checkCachedAuthentication(info))
00534   {
00535       user = info.username;
00536       pass = info.password;
00537   }
00538 
00539   // Try anonymous login if both username/password
00540   // information is blank.
00541   if (user.isEmpty() && pass.isEmpty())
00542   {
00543       user = FTP_LOGIN;
00544       pass = FTP_PASSWD;
00545   }
00546 
00547   QByteArray tempbuf;
00548   QString lastServerResponse;
00549   int failedAuth = 0;
00550   bool promptForRetry = false;
00551 
00552   // Give the user the option to login anonymously...
00553   info.setExtraField(QLatin1String("anonymous"), false);
00554 
00555   do
00556   {
00557     // Check the cache and/or prompt user for password if 1st
00558     // login attempt failed OR the user supplied a login name,
00559     // but no password.
00560     if ( failedAuth > 0 || (!user.isEmpty() && pass.isEmpty()) )
00561     {
00562       QString errorMsg;
00563       kDebug(7102) << "Prompting user for login info...";
00564 
00565       // Ask user if we should retry after when login fails!
00566       if( failedAuth > 0 && promptForRetry)
00567       {
00568         errorMsg = i18n("Message sent:\nLogin using username=%1 and "
00569                         "password=[hidden]\n\nServer replied:\n%2\n\n"
00570                         , user, lastServerResponse);
00571       }
00572 
00573       if ( user != FTP_LOGIN )
00574         info.username = user;
00575 
00576       info.prompt = i18n("You need to supply a username and a password "
00577                          "to access this site.");
00578       info.commentLabel = i18n( "Site:" );
00579       info.comment = i18n("<b>%1</b>",  m_host );
00580       info.keepPassword = true; // Prompt the user for persistence as well.
00581       info.setModified(false);  // Default the modified flag since we reuse authinfo.
00582 
00583       bool disablePassDlg = config()->readEntry( "DisablePassDlg", false );
00584       if ( disablePassDlg || !openPasswordDialog( info, errorMsg ) )
00585       {
00586         error( ERR_USER_CANCELED, m_host );
00587         return false;
00588       }
00589       else
00590       {
00591         // User can decide go anonymous using checkbox
00592         if( info.getExtraField( "anonymous" ).toBool() )
00593         {
00594           user = FTP_LOGIN;
00595           pass = FTP_PASSWD;
00596         }
00597         else
00598         {
00599           user = info.username;
00600           pass = info.password;
00601         }        
00602         promptForRetry = true;
00603       }
00604     }
00605 
00606     tempbuf = "USER ";
00607     tempbuf += user.toLatin1();
00608     if ( m_proxyURL.isValid() )
00609     {
00610       tempbuf += '@';
00611       tempbuf += m_host.toLatin1();
00612       if ( m_port > 0 && m_port != DEFAULT_FTP_PORT )
00613       {
00614         tempbuf += ':';
00615         tempbuf += QString::number(m_port).toLatin1();
00616       }
00617     }
00618 
00619     kDebug(7102) << "Sending Login name: " << tempbuf;
00620 
00621     bool loggedIn = ( ftpSendCmd(tempbuf) && (m_iRespCode == 230) );
00622     bool needPass = (m_iRespCode == 331);
00623     // Prompt user for login info if we do not
00624     // get back a "230" or "331".
00625     if ( !loggedIn && !needPass )
00626     {
00627       lastServerResponse = ftpResponse(0);
00628       kDebug(7102) << "Login failed: " << lastServerResponse;
00629       ++failedAuth;
00630       continue;  // Well we failed, prompt the user please!!
00631     }
00632 
00633     if( needPass )
00634     {
00635       tempbuf = "PASS ";
00636       tempbuf += pass.toLatin1();
00637       kDebug(7102) << "Sending Login password: " << "[protected]";
00638       loggedIn = ( ftpSendCmd(tempbuf) && (m_iRespCode == 230) );
00639     }
00640 
00641     if ( loggedIn )
00642     {
00643       // Make sure the user name changed flag is properly set.
00644       if (userChanged)
00645         *userChanged = (!m_user.isEmpty() && (m_user != user));
00646 
00647       // Do not cache the default login!!
00648       if( user != FTP_LOGIN && pass != FTP_PASSWD )
00649       {
00650         // Update the username in case it was changed during login.
00651         if (!m_user.isEmpty()) {
00652             info.url.setUser (user);
00653             m_user = user;
00654         }
00655 
00656         // Cache the password if the user requested it.
00657         if (info.keepPassword) {
00658             cacheAuthentication(info);
00659         }
00660       }
00661       failedAuth = -1;
00662     }
00663     else
00664     {
00665         // some servers don't let you login anymore
00666         // if you fail login once, so restart the connection here
00667         lastServerResponse = ftpResponse(0);
00668         if (!ftpOpenControlConnection())
00669         {
00670             return false;
00671         }
00672     }
00673   } while( ++failedAuth );
00674 
00675 
00676   kDebug(7102) << "Login OK";
00677   infoMessage( i18n("Login OK") );
00678 
00679   // Okay, we're logged in. If this is IIS 4, switch dir listing style to Unix:
00680   // Thanks to jk@soegaard.net (Jens Kristian Sgaard) for this hint
00681   if( ftpSendCmd("SYST") && (m_iRespType == 2) )
00682   {
00683     if( !strncmp( ftpResponse(0), "215 Windows_NT", 14 ) ) // should do for any version
00684     {
00685       ftpSendCmd( "site dirstyle" );
00686       // Check if it was already in Unix style
00687       // Patch from Keith Refson <Keith.Refson@earth.ox.ac.uk>
00688       if( !strncmp( ftpResponse(0), "200 MSDOS-like directory output is on", 37 ))
00689          //It was in Unix style already!
00690          ftpSendCmd( "site dirstyle" );
00691       // windows won't support chmod before KDE konquers their desktop...
00692       m_extControl |= chmodUnknown;
00693     }
00694   }
00695   else
00696     kWarning(7102) << "SYST failed";
00697 
00698   if ( config()->readEntry ("EnableAutoLoginMacro", false) )
00699     ftpAutoLoginMacro ();
00700 
00701   // Get the current working directory
00702   kDebug(7102) << "Searching for pwd";
00703   if( !ftpSendCmd("PWD") || (m_iRespType != 2) )
00704   {
00705     kDebug(7102) << "Couldn't issue pwd command";
00706     error( ERR_COULD_NOT_LOGIN, i18n("Could not login to %1.", m_host) ); // or anything better ?
00707     return false;
00708   }
00709 
00710   QString sTmp = remoteEncoding()->decode( ftpResponse(3) );
00711   int iBeg = sTmp.indexOf('"');
00712   int iEnd = sTmp.lastIndexOf('"');
00713   if(iBeg > 0 && iBeg < iEnd)
00714   {
00715     m_initialPath = sTmp.mid(iBeg+1, iEnd-iBeg-1);
00716     if(m_initialPath[0] != '/') m_initialPath.prepend('/');
00717     kDebug(7102) << "Initial path set to: " << m_initialPath;
00718     m_currentPath = m_initialPath;
00719   }
00720   return true;
00721 }
00722 
00723 void Ftp::ftpAutoLoginMacro ()
00724 {
00725   QString macro = metaData( "autoLoginMacro" );
00726 
00727   if ( macro.isEmpty() )
00728     return;
00729 
00730   const QStringList list = macro.split('\n',QString::SkipEmptyParts);
00731 
00732   for(QStringList::const_iterator it = list.begin() ; it != list.end() ; ++it )
00733   {
00734     if ( (*it).startsWith(QLatin1String("init")) )
00735     {
00736       const QStringList list2 = macro.split( '\\',QString::SkipEmptyParts);
00737       it = list2.begin();
00738       ++it;  // ignore the macro name
00739 
00740       for( ; it != list2.end() ; ++it )
00741       {
00742         // TODO: Add support for arbitrary commands
00743         // besides simply changing directory!!
00744         if ( (*it).startsWith( QLatin1String("cwd") ) )
00745           ftpFolder( (*it).mid(4), false );
00746       }
00747 
00748       break;
00749     }
00750   }
00751 }
00752 
00753 
00763 bool Ftp::ftpSendCmd( const QByteArray& cmd, int maxretries )
00764 {
00765   Q_ASSERT(m_control != NULL);    // must have control connection socket
00766 
00767   if ( cmd.indexOf( '\r' ) != -1 || cmd.indexOf( '\n' ) != -1)
00768   {
00769     kWarning(7102) << "Invalid command received (contains CR or LF):"
00770                     << cmd.data();
00771     error( ERR_UNSUPPORTED_ACTION, m_host );
00772     return false;
00773   }
00774 
00775   // Don't print out the password...
00776   bool isPassCmd = (cmd.left(4).toLower() == "pass");
00777   if ( !isPassCmd )
00778     kDebug(7102) << "send> " << cmd.data();
00779   else
00780     kDebug(7102) << "send> pass [protected]";
00781 
00782   // Send the message...
00783   QByteArray buf = cmd;
00784   buf += "\r\n";      // Yes, must use CR/LF - see http://cr.yp.to/ftp/request.html
00785   int num = m_control->write(buf);
00786   while (m_control->bytesToWrite() && m_control->waitForBytesWritten()) {}
00787 
00788   // If we were able to successfully send the command, then we will
00789   // attempt to read the response. Otherwise, take action to re-attempt
00790   // the login based on the maximum number of retries specified...
00791   if( num > 0 )
00792     ftpResponse(-1);
00793   else
00794   {
00795     m_iRespType = m_iRespCode = 0;
00796   }
00797 
00798   // If respCh is NULL or the response is 421 (Timed-out), we try to re-send
00799   // the command based on the value of maxretries.
00800   if( (m_iRespType <= 0) || (m_iRespCode == 421) )
00801   {
00802     // We have not yet logged on...
00803     if (!m_bLoggedOn)
00804     {
00805       // The command was sent from the ftpLogin function, i.e. we are actually
00806       // attempting to login in. NOTE: If we already sent the username, we
00807       // return false and let the user decide whether (s)he wants to start from
00808       // the beginning...
00809       if (maxretries > 0 && !isPassCmd)
00810       {
00811         closeConnection ();
00812         if( ftpOpenConnection(loginDefered) )
00813           ftpSendCmd ( cmd, maxretries - 1 );
00814       }
00815 
00816       return false;
00817     }
00818     else
00819     {
00820       if ( maxretries < 1 )
00821         return false;
00822       else
00823       {
00824         kDebug(7102) << "Was not able to communicate with " << m_host
00825                       << "Attempting to re-establish connection.";
00826 
00827         closeConnection(); // Close the old connection...
00828         openConnection();  // Attempt to re-establish a new connection...
00829 
00830         if (!m_bLoggedOn)
00831         {
00832           if (m_control != NULL)  // if openConnection succeeded ...
00833           {
00834             kDebug(7102) << "Login failure, aborting";
00835             error (ERR_COULD_NOT_LOGIN, m_host);
00836             closeConnection ();
00837           }
00838           return false;
00839         }
00840 
00841         kDebug(7102) << "Logged back in, re-issuing command";
00842 
00843         // If we were able to login, resend the command...
00844         if (maxretries)
00845           maxretries--;
00846 
00847         return ftpSendCmd( cmd, maxretries );
00848       }
00849     }
00850   }
00851 
00852   return true;
00853 }
00854 
00855 /*
00856  * ftpOpenPASVDataConnection - set up data connection, using PASV mode
00857  *
00858  * return 0 if successful, ERR_INTERNAL otherwise
00859  * doesn't set error message, since non-pasv mode will always be tried if
00860  * this one fails
00861  */
00862 int Ftp::ftpOpenPASVDataConnection()
00863 {
00864   Q_ASSERT(m_control != NULL);    // must have control connection socket
00865   Q_ASSERT(m_data == NULL);       // ... but no data connection
00866 
00867   // Check that we can do PASV
00868   QHostAddress address = m_control->peerAddress();
00869   if (address.protocol() != QAbstractSocket::IPv4Protocol)
00870     return ERR_INTERNAL;       // no PASV for non-PF_INET connections
00871 
00872  if (m_extControl & pasvUnknown)
00873     return ERR_INTERNAL;       // already tried and got "unknown command"
00874 
00875   m_bPasv = true;
00876 
00877   /* Let's PASsiVe*/
00878   if( !ftpSendCmd("PASV") || (m_iRespType != 2) )
00879   {
00880     kDebug(7102) << "PASV attempt failed";
00881     // unknown command?
00882     if( m_iRespType == 5 )
00883     {
00884         kDebug(7102) << "disabling use of PASV";
00885         m_extControl |= pasvUnknown;
00886     }
00887     return ERR_INTERNAL;
00888   }
00889 
00890   // The usual answer is '227 Entering Passive Mode. (160,39,200,55,6,245)'
00891   // but anonftpd gives '227 =160,39,200,55,6,245'
00892   int i[6];
00893   const char *start = strchr(ftpResponse(3), '(');
00894   if ( !start )
00895     start = strchr(ftpResponse(3), '=');
00896   if ( !start ||
00897        ( sscanf(start, "(%d,%d,%d,%d,%d,%d)",&i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 &&
00898          sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 ) )
00899   {
00900     kError(7102) << "parsing IP and port numbers failed. String parsed: " << start;
00901     return ERR_INTERNAL;
00902   }
00903 
00904   // we ignore the host part on purpose for two reasons
00905   // a) it might be wrong anyway
00906   // b) it would make us being suceptible to a port scanning attack
00907 
00908   // now connect the data socket ...
00909   quint16 port = i[4] << 8 | i[5];
00910   const QString host = (isSocksProxy() ? m_host : address.toString());
00911   m_data = KSocketFactory::synchronousConnectToHost("ftp-data", host, port, connectTimeout() * 1000);
00912 
00913   return m_data->state() == QAbstractSocket::ConnectedState ? 0 : ERR_INTERNAL;
00914 }
00915 
00916 /*
00917  * ftpOpenEPSVDataConnection - opens a data connection via EPSV
00918  */
00919 int Ftp::ftpOpenEPSVDataConnection()
00920 {
00921   Q_ASSERT(m_control != NULL);    // must have control connection socket
00922   Q_ASSERT(m_data == NULL);       // ... but no data connection
00923 
00924   QHostAddress address = m_control->peerAddress();
00925   int portnum;
00926 
00927   if (m_extControl & epsvUnknown)
00928     return ERR_INTERNAL;
00929 
00930   m_bPasv = true;
00931   if( !ftpSendCmd("EPSV") || (m_iRespType != 2) )
00932   {
00933     // unknown command?
00934     if( m_iRespType == 5 )
00935     {
00936        kDebug(7102) << "disabling use of EPSV";
00937        m_extControl |= epsvUnknown;
00938     }
00939     return ERR_INTERNAL;
00940   }
00941 
00942   const char *start = strchr(ftpResponse(3), '|');
00943   if ( !start || sscanf(start, "|||%d|", &portnum) != 1)
00944     return ERR_INTERNAL;
00945 
00946   const QString host = (isSocksProxy() ? m_host : address.toString());
00947   m_data = KSocketFactory::synchronousConnectToHost("ftp-data", host, portnum, connectTimeout() * 1000);
00948   return m_data->isOpen() ? 0 : ERR_INTERNAL;
00949 }
00950 
00951 /*
00952  * ftpOpenDataConnection - set up data connection
00953  *
00954  * The routine calls several ftpOpenXxxxConnection() helpers to find
00955  * the best connection mode. If a helper cannot connect if returns
00956  * ERR_INTERNAL - so this is not really an error! All other error
00957  * codes are treated as fatal, e.g. they are passed back to the caller
00958  * who is responsible for calling error(). ftpOpenPortDataConnection
00959  * can be called as last try and it does never return ERR_INTERNAL.
00960  *
00961  * @return 0 if successful, err code otherwise
00962  */
00963 int Ftp::ftpOpenDataConnection()
00964 {
00965   // make sure that we are logged on and have no data connection...
00966   Q_ASSERT( m_bLoggedOn );
00967   ftpCloseDataConnection();
00968 
00969   int  iErrCode = 0;
00970   int  iErrCodePASV = 0;  // Remember error code from PASV
00971 
00972   // First try passive (EPSV & PASV) modes
00973   if( !config()->readEntry("DisablePassiveMode", false) )
00974   {
00975     iErrCode = ftpOpenPASVDataConnection();
00976     if(iErrCode == 0)
00977       return 0; // success
00978     iErrCodePASV = iErrCode;
00979     ftpCloseDataConnection();
00980 
00981     if( !config()->readEntry("DisableEPSV", false) )
00982     {
00983       iErrCode = ftpOpenEPSVDataConnection();
00984       if(iErrCode == 0)
00985         return 0; // success
00986       ftpCloseDataConnection();
00987     }
00988 
00989     // if we sent EPSV ALL already and it was accepted, then we can't
00990     // use active connections any more
00991     if (m_extControl & epsvAllSent)
00992       return iErrCodePASV ? iErrCodePASV : iErrCode;
00993   }
00994 
00995   // fall back to port mode
00996   iErrCode = ftpOpenPortDataConnection();
00997   if(iErrCode == 0)
00998     return 0; // success
00999 
01000   ftpCloseDataConnection();
01001   // prefer to return the error code from PASV if any, since that's what should have worked in the first place
01002   return iErrCodePASV ? iErrCodePASV : iErrCode;
01003 }
01004 
01005 /*
01006  * ftpOpenPortDataConnection - set up data connection
01007  *
01008  * @return 0 if successful, err code otherwise (but never ERR_INTERNAL
01009  *         because this is the last connection mode that is tried)
01010  */
01011 int Ftp::ftpOpenPortDataConnection()
01012 {
01013   Q_ASSERT(m_control != NULL);    // must have control connection socket
01014   Q_ASSERT(m_data == NULL);       // ... but no data connection
01015 
01016   m_bPasv = false;
01017   if (m_extControl & eprtUnknown)
01018     return ERR_INTERNAL;
01019 
01020   if (!m_server)
01021     m_server = KSocketFactory::listen("ftp-data");
01022 
01023   if (!m_server->isListening()) {
01024     delete m_server;
01025     m_server = NULL;
01026     return ERR_COULD_NOT_LISTEN;
01027   }
01028 
01029   m_server->setMaxPendingConnections(1);
01030 
01031   QString command;
01032   QHostAddress localAddress = m_control->localAddress();
01033   if (localAddress.protocol() == QAbstractSocket::IPv4Protocol)
01034   {
01035     struct
01036     {
01037       quint32 ip4;
01038       quint16 port;
01039     } data;
01040     data.ip4 = localAddress.toIPv4Address();
01041     data.port = m_server->serverPort();
01042 
01043     unsigned char *pData = reinterpret_cast<unsigned char*>(&data);
01044     command.sprintf("PORT %d,%d,%d,%d,%d,%d",pData[3],pData[2],pData[1],pData[0],pData[5],pData[4]);
01045   }
01046   else if (localAddress.protocol() == QAbstractSocket::IPv6Protocol)
01047   {
01048     command = QString("EPRT |2|%2|%3|").arg(localAddress.toString()).arg(m_server->serverPort());
01049   }
01050 
01051   if( ftpSendCmd(command.toLatin1()) && (m_iRespType == 2) )
01052   {
01053     return 0;
01054   }
01055 
01056   delete m_server;
01057   m_server = NULL;
01058   return ERR_INTERNAL;
01059 }
01060 
01061 bool Ftp::ftpOpenCommand( const char *_command, const QString & _path, char _mode,
01062                           int errorcode, KIO::fileoffset_t _offset )
01063 {
01064   int errCode = 0;
01065   if( !ftpDataMode(ftpModeFromPath(_path, _mode)) )
01066     errCode = ERR_COULD_NOT_CONNECT;
01067   else
01068     errCode = ftpOpenDataConnection();
01069 
01070   if(errCode != 0)
01071   {
01072     error(errCode, m_host);
01073     return false;
01074   }
01075 
01076   if ( _offset > 0 ) {
01077     // send rest command if offset > 0, this applies to retr and stor commands
01078     char buf[100];
01079     sprintf(buf, "rest %lld", _offset);
01080     if ( !ftpSendCmd( buf ) )
01081        return false;
01082     if( m_iRespType != 3 )
01083     {
01084       error( ERR_CANNOT_RESUME, _path ); // should never happen
01085       return false;
01086     }
01087   }
01088 
01089   QByteArray tmp = _command;
01090   QString errormessage;
01091 
01092   if ( !_path.isEmpty() ) {
01093     tmp += ' ';
01094     tmp += remoteEncoding()->encode(ftpCleanPath(_path));
01095   }
01096 
01097   if( !ftpSendCmd( tmp ) || (m_iRespType != 1) )
01098   {
01099     if( _offset > 0 && strcmp(_command, "retr") == 0 && (m_iRespType == 4) )
01100       errorcode = ERR_CANNOT_RESUME;
01101     // The error here depends on the command
01102     errormessage = _path;
01103   }
01104 
01105   else
01106   {
01107     // Only now we know for sure that we can resume
01108     if ( _offset > 0 && strcmp(_command, "retr") == 0 )
01109       canResume();
01110 
01111     if(m_server && !m_data) {
01112       kDebug(7102) << "waiting for connection from remote.";
01113       m_server->waitForNewConnection(connectTimeout() * 1000);
01114       m_data = m_server->nextPendingConnection();
01115     }
01116 
01117     if(m_data) {
01118       kDebug(7102) << "connected with remote.";
01119       m_bBusy = true;              // cleared in ftpCloseCommand
01120       return true;
01121     }
01122 
01123     kDebug(7102) << "no connection received from remote.";
01124     errorcode=ERR_COULD_NOT_ACCEPT;
01125     errormessage=m_host;
01126     return false;
01127   }
01128 
01129   error(errorcode, errormessage);
01130   return false;
01131 }
01132 
01133 
01134 bool Ftp::ftpCloseCommand()
01135 {
01136   // first close data sockets (if opened), then read response that
01137   // we got for whatever was used in ftpOpenCommand ( should be 226 )
01138   ftpCloseDataConnection();
01139 
01140   if(!m_bBusy)
01141     return true;
01142 
01143   kDebug(7102) << "ftpCloseCommand: reading command result";
01144   m_bBusy = false;
01145 
01146   if(!ftpResponse(-1) || (m_iRespType != 2) )
01147   {
01148     kDebug(7102) << "ftpCloseCommand: no transfer complete message";
01149     return false;
01150   }
01151   return true;
01152 }
01153 
01154 void Ftp::mkdir( const KUrl & url, int permissions )
01155 {
01156   if( !ftpOpenConnection(loginImplicit) )
01157         return;
01158 
01159   const QByteArray encodedPath (remoteEncoding()->encode(url));
01160   const QString path = QString::fromLatin1(encodedPath.constData(), encodedPath.size());
01161 
01162   if( !ftpSendCmd( (QByteArray ("mkd ") + encodedPath) ) || (m_iRespType != 2) )
01163   {
01164     QString currentPath( m_currentPath );
01165 
01166     // Check whether or not mkdir failed because
01167     // the directory already exists...
01168     if( ftpFolder( path, false ) )
01169     {
01170       error( ERR_DIR_ALREADY_EXIST, path );
01171       // Change the directory back to what it was...
01172       (void) ftpFolder( currentPath, false );
01173       return;
01174     }
01175 
01176     error( ERR_COULD_NOT_MKDIR, path );
01177     return;
01178   }
01179 
01180   if ( permissions != -1 )
01181   {
01182     // chmod the dir we just created, ignoring errors.
01183     (void) ftpChmod( path, permissions );
01184   }
01185 
01186   finished();
01187 }
01188 
01189 void Ftp::rename( const KUrl& src, const KUrl& dst, KIO::JobFlags flags )
01190 {
01191   if( !ftpOpenConnection(loginImplicit) )
01192         return;
01193 
01194   // The actual functionality is in ftpRename because put needs it
01195   if ( ftpRename( src.path(), dst.path(), flags ) )
01196     finished();
01197 }
01198 
01199 bool Ftp::ftpRename(const QString & src, const QString & dst, KIO::JobFlags jobFlags)
01200 {
01201     Q_ASSERT(m_bLoggedOn);
01202 
01203     // Must check if dst already exists, RNFR+RNTO overwrites by default (#127793).
01204     if (!(jobFlags & KIO::Overwrite)) {
01205         if (ftpFileExists(dst)) {
01206             error(ERR_FILE_ALREADY_EXIST, dst);
01207             return false;
01208         }
01209     }
01210 
01211     if (ftpFolder(dst, false)) {
01212         error(ERR_DIR_ALREADY_EXIST, dst);
01213         return false;
01214     }
01215 
01216     // CD into parent folder
01217     const int pos = src.lastIndexOf('/');
01218     if (pos > 0) {
01219         if(!ftpFolder(src.left(pos+1), false))
01220             return false;
01221     }
01222 
01223     QByteArray from_cmd = "RNFR ";
01224     from_cmd += remoteEncoding()->encode(src.mid(pos+1));
01225     if (!ftpSendCmd(from_cmd) || (m_iRespType != 3)) {
01226         error( ERR_CANNOT_RENAME, src );
01227         return false;
01228     }
01229 
01230     QByteArray to_cmd = "RNTO ";
01231     to_cmd += remoteEncoding()->encode(dst);
01232     if (!ftpSendCmd(to_cmd) || (m_iRespType != 2)) {
01233         error( ERR_CANNOT_RENAME, src );
01234         return false;
01235     }
01236 
01237     return true;
01238 }
01239 
01240 void Ftp::del( const KUrl& url, bool isfile )
01241 {
01242   if( !ftpOpenConnection(loginImplicit) )
01243         return;
01244 
01245   // When deleting a directory, we must exit from it first
01246   // The last command probably went into it (to stat it)
01247   if ( !isfile )
01248     ftpFolder(remoteEncoding()->directory(url), false); // ignore errors
01249 
01250   QByteArray cmd = isfile ? "DELE " : "RMD ";
01251   cmd += remoteEncoding()->encode(url);
01252 
01253   if( !ftpSendCmd( cmd ) || (m_iRespType != 2) )
01254     error( ERR_CANNOT_DELETE, url.path() );
01255   else
01256     finished();
01257 }
01258 
01259 bool Ftp::ftpChmod( const QString & path, int permissions )
01260 {
01261   Q_ASSERT( m_bLoggedOn );
01262 
01263   if(m_extControl & chmodUnknown)      // previous errors?
01264     return false;
01265 
01266   // we need to do bit AND 777 to get permissions, in case
01267   // we were sent a full mode (unlikely)
01268   QString cmd = QString::fromLatin1("SITE CHMOD ") + QString::number( permissions & 511, 8 /*octal*/ ) + ' ';
01269   cmd += path;
01270 
01271   ftpSendCmd(remoteEncoding()->encode(cmd));
01272   if(m_iRespType == 2)
01273      return true;
01274 
01275   if(m_iRespCode == 500)
01276   {
01277     m_extControl |= chmodUnknown;
01278     kDebug(7102) << "ftpChmod: CHMOD not supported - disabling";
01279   }
01280   return false;
01281 }
01282 
01283 void Ftp::chmod( const KUrl & url, int permissions )
01284 {
01285   if( !ftpOpenConnection(loginImplicit) )
01286         return;
01287 
01288   if ( !ftpChmod( url.path(), permissions ) )
01289     error( ERR_CANNOT_CHMOD, url.path() );
01290   else
01291     finished();
01292 }
01293 
01294 void Ftp::ftpCreateUDSEntry( const QString & filename, FtpEntry& ftpEnt, UDSEntry& entry, bool isDir )
01295 {
01296   Q_ASSERT(entry.count() == 0); // by contract :-)
01297 
01298   entry.insert( KIO::UDSEntry::UDS_NAME, filename );
01299   entry.insert( KIO::UDSEntry::UDS_SIZE, ftpEnt.size );
01300   entry.insert( KIO::UDSEntry::UDS_MODIFICATION_TIME, ftpEnt.date );
01301   entry.insert( KIO::UDSEntry::UDS_ACCESS, ftpEnt.access );
01302   entry.insert( KIO::UDSEntry::UDS_USER, ftpEnt.owner );
01303   if ( !ftpEnt.group.isEmpty() )
01304   {
01305     entry.insert( KIO::UDSEntry::UDS_GROUP, ftpEnt.group );
01306   }
01307 
01308   if ( !ftpEnt.link.isEmpty() )
01309   {
01310     entry.insert( KIO::UDSEntry::UDS_LINK_DEST, ftpEnt.link );
01311 
01312     KMimeType::Ptr mime = KMimeType::findByUrl( KUrl("ftp://host/" + filename ) );
01313     // Links on ftp sites are often links to dirs, and we have no way to check
01314     // that. Let's do like Netscape : assume dirs generally.
01315     // But we do this only when the mimetype can't be known from the filename.
01316     // --> we do better than Netscape :-)
01317     if ( mime->name() == KMimeType::defaultMimeType() )
01318     {
01319       kDebug(7102) << "Setting guessed mime type to inode/directory for " << filename;
01320       entry.insert( KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, QString::fromLatin1( "inode/directory" ) );
01321       isDir = true;
01322     }
01323   }
01324 
01325   entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : ftpEnt.type );
01326   // entry.insert KIO::UDSEntry::UDS_ACCESS_TIME,buff.st_atime);
01327   // entry.insert KIO::UDSEntry::UDS_CREATION_TIME,buff.st_ctime);
01328 }
01329 
01330 
01331 void Ftp::ftpShortStatAnswer( const QString& filename, bool isDir )
01332 {
01333     UDSEntry entry;
01334 
01335 
01336     entry.insert( KIO::UDSEntry::UDS_NAME, filename );
01337     entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : S_IFREG );
01338     entry.insert( KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH );
01339     if (isDir) {
01340       entry.insert( KIO::UDSEntry::UDS_MIME_TYPE, QLatin1String("inode/directory"));
01341     }
01342     // No details about size, ownership, group, etc.
01343 
01344     statEntry(entry);
01345     finished();
01346 }
01347 
01348 void Ftp::ftpStatAnswerNotFound( const QString & path, const QString & filename )
01349 {
01350     // Only do the 'hack' below if we want to download an existing file (i.e. when looking at the "source")
01351     // When e.g. uploading a file, we still need stat() to return "not found"
01352     // when the file doesn't exist.
01353     QString statSide = metaData("statSide");
01354     kDebug(7102) << "statSide=" << statSide;
01355     if ( statSide == "source" )
01356     {
01357         kDebug(7102) << "Not found, but assuming found, because some servers don't allow listing";
01358         // MS Server is incapable of handling "list <blah>" in a case insensitive way
01359         // But "retr <blah>" works. So lie in stat(), to get going...
01360         //
01361         // There's also the case of ftp://ftp2.3ddownloads.com/90380/linuxgames/loki/patches/ut/ut-patch-436.run
01362         // where listing permissions are denied, but downloading is still possible.
01363         ftpShortStatAnswer( filename, false /*file, not dir*/ );
01364 
01365         return;
01366     }
01367 
01368     error( ERR_DOES_NOT_EXIST, path );
01369 }
01370 
01371 void Ftp::stat(const KUrl &url)
01372 {
01373   kDebug(7102) << "path=" << url.path();
01374   if( !ftpOpenConnection(loginImplicit) )
01375         return;
01376 
01377   const QString path = ftpCleanPath( QDir::cleanPath( url.path() ) );
01378   kDebug(7102) << "cleaned path=" << path;
01379 
01380   // We can't stat root, but we know it's a dir.
01381   if( path.isEmpty() || path == "/" )
01382   {
01383     UDSEntry entry;
01384     //entry.insert( KIO::UDSEntry::UDS_NAME, UDSField( QString() ) );
01385     entry.insert( KIO::UDSEntry::UDS_NAME, QString::fromLatin1( "." ) );
01386     entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR );
01387     entry.insert( KIO::UDSEntry::UDS_MIME_TYPE, QLatin1String("inode/directory"));
01388     entry.insert( KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH );
01389     entry.insert( KIO::UDSEntry::UDS_USER, QString::fromLatin1( "root" ) );
01390     entry.insert( KIO::UDSEntry::UDS_GROUP, QString::fromLatin1( "root" ) );
01391     // no size
01392 
01393     statEntry( entry );
01394     finished();
01395     return;
01396   }
01397 
01398   KUrl tempurl( url );
01399   tempurl.setPath( path ); // take the clean one
01400   QString listarg; // = tempurl.directory(KUrl::ObeyTrailingSlash);
01401   QString parentDir;
01402   QString filename = tempurl.fileName();
01403   Q_ASSERT(!filename.isEmpty());
01404   QString search = filename;
01405 
01406   // Try cwd into it, if it works it's a dir (and then we'll list the parent directory to get more info)
01407   // if it doesn't work, it's a file (and then we'll use dir filename)
01408   bool isDir = ftpFolder(path, false);
01409 
01410   // if we're only interested in "file or directory", we should stop here
01411   QString sDetails = metaData("details");
01412   int details = sDetails.isEmpty() ? 2 : sDetails.toInt();
01413   kDebug(7102) << "details=" << details;
01414   if ( details == 0 )
01415   {
01416      if ( !isDir && !ftpFileExists(path) ) // ok, not a dir -> is it a file ?
01417      {  // no -> it doesn't exist at all
01418         ftpStatAnswerNotFound( path, filename );
01419         return;
01420      }
01421      ftpShortStatAnswer( filename, isDir ); // successfully found a dir or a file -> done
01422      return;
01423   }
01424 
01425   if (!isDir)
01426   {
01427     // It is a file or it doesn't exist, try going to parent directory
01428     parentDir = tempurl.directory(KUrl::AppendTrailingSlash);
01429     // With files we can do "LIST <filename>" to avoid listing the whole dir
01430     listarg = filename;
01431   }
01432   else
01433   {
01434     // --- New implementation:
01435     // Don't list the parent dir. Too slow, might not show it, etc.
01436     // Just return that it's a dir.
01437     UDSEntry entry;
01438     entry.insert( KIO::UDSEntry::UDS_NAME, filename );
01439     entry.insert( KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR );
01440     entry.insert( KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH );
01441     // No clue about size, ownership, group, etc.
01442 
01443     statEntry(entry);
01444     finished();
01445     return;
01446   }
01447 
01448   // Now cwd the parent dir, to prepare for listing
01449   if( !ftpFolder(parentDir, true) )
01450     return;
01451 
01452   if( !ftpOpenCommand( "list", listarg, 'I', ERR_DOES_NOT_EXIST ) )
01453   {
01454     kError(7102) << "COULD NOT LIST";
01455     return;
01456   }
01457   kDebug(7102) << "Starting of list was ok";
01458 
01459   Q_ASSERT( !search.isEmpty() && search != "/" );
01460 
01461   bool bFound = false;
01462   KUrl      linkURL;
01463   FtpEntry  ftpEnt;
01464   while( ftpReadDir(ftpEnt) )
01465   {
01466     // We look for search or filename, since some servers (e.g. ftp.tuwien.ac.at)
01467     // return only the filename when doing "dir /full/path/to/file"
01468     if (!bFound) {
01469         if ( ( search == ftpEnt.name || filename == ftpEnt.name ) ) {
01470             if ( !filename.isEmpty() ) {
01471               bFound = true;
01472               UDSEntry entry;
01473               ftpCreateUDSEntry( filename, ftpEnt, entry, isDir );
01474               statEntry( entry );
01475             }
01476         }
01477     }
01478 
01479     // kDebug(7102) << ftpEnt.name;
01480   }
01481 
01482   ftpCloseCommand();        // closes the data connection only
01483 
01484   if ( !bFound )
01485   {
01486     ftpStatAnswerNotFound( path, filename );
01487     return;
01488   }
01489 
01490   if ( !linkURL.isEmpty() )
01491   {
01492       if ( linkURL == url || linkURL == tempurl )
01493       {
01494           error( ERR_CYCLIC_LINK, linkURL.prettyUrl() );
01495           return;
01496       }
01497       Ftp::stat( linkURL );
01498       return;
01499   }
01500 
01501   kDebug(7102) << "stat : finished successfully";
01502   finished();
01503 }
01504 
01505 
01506 void Ftp::listDir( const KUrl &url )
01507 {
01508   kDebug(7102) << url;
01509   if( !ftpOpenConnection(loginImplicit) )
01510         return;
01511 
01512   // No path specified ?
01513   QString path = url.path();
01514   if ( path.isEmpty() )
01515   {
01516     KUrl realURL;
01517     realURL.setProtocol( "ftp" );
01518     realURL.setUser( m_user );
01519     realURL.setPass( m_pass );
01520     realURL.setHost( m_host );
01521     if ( m_port > 0 && m_port != DEFAULT_FTP_PORT )
01522         realURL.setPort( m_port );
01523     if ( m_initialPath.isEmpty() )
01524         m_initialPath = '/';
01525     realURL.setPath( m_initialPath );
01526     kDebug(7102) << "REDIRECTION to " << realURL.prettyUrl();
01527     redirection( realURL );
01528     finished();
01529     return;
01530   }
01531 
01532     kDebug(7102) << "hunting for path" << path;
01533 
01534     if (!ftpOpenDir(path)) {
01535         if (ftpFileExists(path)) {
01536             error(ERR_IS_FILE, path);
01537         } else {
01538             // not sure which to emit
01539             //error( ERR_DOES_NOT_EXIST, path );
01540             error( ERR_CANNOT_ENTER_DIRECTORY, path );
01541         }
01542         return;
01543     }
01544 
01545   UDSEntry entry;
01546   FtpEntry  ftpEnt;
01547   while( ftpReadDir(ftpEnt) )
01548   {
01549     //kDebug(7102) << ftpEnt.name;
01550     //Q_ASSERT( !ftpEnt.name.isEmpty() );
01551     if ( !ftpEnt.name.isEmpty() )
01552     {
01553       //if ( S_ISDIR( (mode_t)ftpEnt.type ) )
01554       //   kDebug(7102) << "is a dir";
01555       //if ( !ftpEnt.link.isEmpty() )
01556       //   kDebug(7102) << "is a link to " << ftpEnt.link;
01557       entry.clear();
01558       ftpCreateUDSEntry( ftpEnt.name, ftpEnt, entry, false );
01559       listEntry( entry, false );
01560     }
01561   }
01562   listEntry( entry, true ); // ready
01563   ftpCloseCommand();        // closes the data connection only
01564   finished();
01565 }
01566 
01567 void Ftp::slave_status()
01568 {
01569   kDebug(7102) << "Got slave_status host = " << (!m_host.toAscii().isEmpty() ? m_host.toAscii() : "[None]") << " [" << (m_bLoggedOn ? "Connected" : "Not connected") << "]";
01570   slaveStatus( m_host, m_bLoggedOn );
01571 }
01572 
01573 bool Ftp::ftpOpenDir( const QString & path )
01574 {
01575   //QString path( _url.path(KUrl::RemoveTrailingSlash) );
01576 
01577   // We try to change to this directory first to see whether it really is a directory.
01578   // (And also to follow symlinks)
01579   QString tmp = path.isEmpty() ? QString("/") : path;
01580 
01581   // We get '550', whether it's a file or doesn't exist...
01582   if( !ftpFolder(tmp, false) )
01583       return false;
01584 
01585   // Don't use the path in the list command:
01586   // We changed into this directory anyway - so it's enough just to send "list".
01587   // We use '-a' because the application MAY be interested in dot files.
01588   // The only way to really know would be to have a metadata flag for this...
01589   // Since some windows ftp server seems not to support the -a argument, we use a fallback here.
01590   // In fact we have to use -la otherwise -a removes the default -l (e.g. ftp.trolltech.com)
01591   if( !ftpOpenCommand( "list -la", QString(), 'I', ERR_CANNOT_ENTER_DIRECTORY ) )
01592   {
01593     if ( !ftpOpenCommand( "list", QString(), 'I', ERR_CANNOT_ENTER_DIRECTORY ) )
01594     {
01595       kWarning(7102) << "Can't open for listing";
01596       return false;
01597     }
01598   }
01599   kDebug(7102) << "Starting of list was ok";
01600   return true;
01601 }
01602 
01603 bool Ftp::ftpReadDir(FtpEntry& de)
01604 {
01605   Q_ASSERT(m_data != NULL);
01606 
01607   // get a line from the data connecetion ...
01608   while( true )
01609   {
01610     while (!m_data->canReadLine() && m_data->waitForReadyRead((readTimeout() * 1000))) {}
01611     QByteArray data = m_data->readLine();
01612     if (data.size() == 0)
01613       break;
01614 
01615     const char* buffer = data.data();
01616     kDebug(7102) << "dir > " << buffer;
01617 
01618     //Normally the listing looks like
01619     // -rw-r--r--   1 dfaure   dfaure        102 Nov  9 12:30 log
01620     // but on Netware servers like ftp://ci-1.ci.pwr.wroc.pl/ it looks like (#76442)
01621     // d [RWCEAFMS] Admin                     512 Oct 13  2004 PSI
01622 
01623     // we should always get the following 5 fields ...
01624     const char *p_access, *p_junk, *p_owner, *p_group, *p_size;
01625     if( (p_access = strtok((char*)buffer," ")) == 0) continue;
01626     if( (p_junk  = strtok(NULL," ")) == 0) continue;
01627     if( (p_owner = strtok(NULL," ")) == 0) continue;
01628     if( (p_group = strtok(NULL," ")) == 0) continue;
01629     if( (p_size  = strtok(NULL," ")) == 0) continue;
01630 
01631     //kDebug(7102) << "p_access=" << p_access << " p_junk=" << p_junk << " p_owner=" << p_owner << " p_group=" << p_group << " p_size=" << p_size;
01632 
01633     de.access = 0;
01634     if ( strlen( p_access ) == 1 && p_junk[0] == '[' ) { // Netware
01635       de.access = S_IRWXU | S_IRWXG | S_IRWXO; // unknown -> give all permissions
01636     }
01637 
01638     const char *p_date_1, *p_date_2, *p_date_3, *p_name;
01639 
01640     // A special hack for "/dev". A listing may look like this:
01641     // crw-rw-rw-   1 root     root       1,   5 Jun 29  1997 zero
01642     // So we just ignore the number in front of the ",". Ok, it is a hack :-)
01643     if ( strchr( p_size, ',' ) != 0L )
01644     {
01645       //kDebug(7102) << "Size contains a ',' -> reading size again (/dev hack)";
01646       if ((p_size = strtok(NULL," ")) == 0)
01647         continue;
01648     }
01649 
01650     // Check whether the size we just read was really the size
01651     // or a month (this happens when the server lists no group)
01652     // Used to be the case on sunsite.uio.no, but not anymore
01653     // This is needed for the Netware case, too.
01654     if ( !isdigit( *p_size ) )
01655     {
01656       p_date_1 = p_size;
01657       p_size = p_group;
01658       p_group = 0;
01659       //kDebug(7102) << "Size didn't have a digit -> size=" << p_size << " date_1=" << p_date_1;
01660     }
01661     else
01662     {
01663       p_date_1 = strtok(NULL," ");
01664       //kDebug(7102) << "Size has a digit -> ok. p_date_1=" << p_date_1;
01665     }
01666 
01667     if ( p_date_1 != 0 &&
01668          (p_date_2 = strtok(NULL," ")) != 0 &&
01669          (p_date_3 = strtok(NULL," ")) != 0 &&
01670          (p_name = strtok(NULL,"\r\n")) != 0 )
01671     {
01672       {
01673         QByteArray tmp( p_name );
01674         if ( p_access[0] == 'l' )
01675         {
01676           int i = tmp.lastIndexOf( " -> " );
01677           if ( i != -1 ) {
01678             de.link = remoteEncoding()->decode(p_name + i + 4);
01679             tmp.truncate( i );
01680           }
01681           else
01682             de.link.clear();
01683         }
01684         else
01685           de.link.clear();
01686 
01687         if ( tmp[0] == '/' ) // listing on ftp://ftp.gnupg.org/ starts with '/'
01688           tmp.remove( 0, 1 );
01689 
01690         if (tmp.indexOf('/') != -1)
01691           continue; // Don't trick us!
01692 
01693         de.name     = remoteEncoding()->decode(tmp);
01694       }
01695 
01696       de.type = S_IFREG;
01697       switch ( p_access[0] ) {
01698       case 'd':
01699         de.type = S_IFDIR;
01700         break;
01701       case 's':
01702         de.type = S_IFSOCK;
01703         break;
01704       case 'b':
01705         de.type = S_IFBLK;
01706         break;
01707       case 'c':
01708         de.type = S_IFCHR;
01709         break;
01710       case 'l':
01711         de.type = S_IFREG;
01712         // we don't set S_IFLNK here.  de.link says it.
01713         break;
01714       default:
01715         break;
01716       }
01717 
01718       if ( p_access[1] == 'r' )
01719         de.access |= S_IRUSR;
01720       if ( p_access[2] == 'w' )
01721         de.access |= S_IWUSR;
01722       if ( p_access[3] == 'x' || p_access[3] == 's' )
01723         de.access |= S_IXUSR;
01724       if ( p_access[4] == 'r' )
01725         de.access |= S_IRGRP;
01726       if ( p_access[5] == 'w' )
01727         de.access |= S_IWGRP;
01728       if ( p_access[6] == 'x' || p_access[6] == 's' )
01729         de.access |= S_IXGRP;
01730       if ( p_access[7] == 'r' )
01731         de.access |= S_IROTH;
01732       if ( p_access[8] == 'w' )
01733         de.access |= S_IWOTH;
01734       if ( p_access[9] == 'x' || p_access[9] == 't' )
01735         de.access |= S_IXOTH;
01736       if ( p_access[3] == 's' || p_access[3] == 'S' )
01737         de.access |= S_ISUID;
01738       if ( p_access[6] == 's' || p_access[6] == 'S' )
01739         de.access |= S_ISGID;
01740       if ( p_access[9] == 't' || p_access[9] == 'T' )
01741         de.access |= S_ISVTX;
01742 
01743       de.owner    = remoteEncoding()->decode(p_owner);
01744       de.group    = remoteEncoding()->decode(p_group);
01745       de.size     = charToLongLong(p_size);
01746 
01747       // Parsing the date is somewhat tricky
01748       // Examples : "Oct  6 22:49", "May 13  1999"
01749 
01750       // First get current time - we need the current month and year
01751       time_t currentTime = time( 0L );
01752       struct tm * tmptr = gmtime( &currentTime );
01753       int currentMonth = tmptr->tm_mon;
01754       //kDebug(7102) << "Current time :" << asctime( tmptr );
01755       // Reset time fields
01756       tmptr->tm_isdst = -1; // We do not anything about day saving time
01757       tmptr->tm_sec = 0;
01758       tmptr->tm_min = 0;
01759       tmptr->tm_hour = 0;
01760       // Get day number (always second field)
01761       tmptr->tm_mday = atoi( p_date_2 );
01762       // Get month from first field
01763       // NOTE : no, we don't want to use KLocale here
01764       // It seems all FTP servers use the English way
01765       //kDebug(7102) << "Looking for month " << p_date_1;
01766       static const char * const s_months[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
01767                                                  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
01768       for ( int c = 0 ; c < 12 ; c ++ )
01769         if ( !strcmp( p_date_1, s_months[c]) )
01770         {
01771           //kDebug(7102) << "Found month " << c << " for " << p_date_1;
01772           tmptr->tm_mon = c;
01773           break;
01774         }
01775 
01776       // Parse third field
01777       if ( strlen( p_date_3 ) == 4 ) // 4 digits, looks like a year
01778         tmptr->tm_year = atoi( p_date_3 ) - 1900;
01779       else
01780       {
01781         // otherwise, the year is implicit
01782         // according to man ls, this happens when it is between than 6 months
01783         // old and 1 hour in the future.
01784         // So the year is : current year if tm_mon <= currentMonth+1
01785         // otherwise current year minus one
01786         // (The +1 is a security for the "+1 hour" at the end of the month issue)
01787         if ( tmptr->tm_mon > currentMonth + 1 )
01788           tmptr->tm_year--;
01789 
01790         // and p_date_3 contains probably a time
01791         char * semicolon;
01792         if ( ( semicolon = (char*)strchr( p_date_3, ':' ) ) )
01793         {
01794           *semicolon = '\0';
01795           tmptr->tm_min = atoi( semicolon + 1 );
01796           tmptr->tm_hour = atoi( p_date_3 );
01797         }
01798         else
01799           kWarning(7102) << "Can't parse third field " << p_date_3;
01800       }
01801 
01802       //kDebug(7102) << asctime( tmptr );
01803       de.date = mktime( tmptr );
01804       return true;
01805     }
01806   } // line invalid, loop to get another line
01807   return false;
01808 }
01809 
01810 //===============================================================================
01811 // public: get           download file from server
01812 // helper: ftpGet        called from get() and copy()
01813 //===============================================================================
01814 void Ftp::get( const KUrl & url )
01815 {
01816   kDebug(7102) << url;
01817 
01818   int iError = 0;
01819   const StatusCode cs = ftpGet(iError, -1, url, 0);
01820   ftpCloseCommand();                        // must close command!
01821 
01822   if (cs == statusSuccess) {
01823      finished();
01824      return;
01825   }
01826 
01827   if (iError) {                            // can have only server side errs
01828      error(iError, url.path());
01829   }
01830 }
01831 
01832 Ftp::StatusCode Ftp::ftpGet(int& iError, int iCopyFile, const KUrl& url, KIO::fileoffset_t llOffset)
01833 {
01834   // Calls error() by itself!
01835   if( !ftpOpenConnection(loginImplicit) )
01836     return statusServerError;
01837 
01838   // Try to find the size of the file (and check that it exists at
01839   // the same time). If we get back a 550, "File does not exist"
01840   // or "not a plain file", check if it is a directory. If it is a
01841   // directory, return an error; otherwise simply try to retrieve
01842   // the request...
01843   if ( !ftpSize( url.path(), '?' ) && (m_iRespCode == 550) &&
01844        ftpFolder(url.path(), false) )
01845   {
01846     // Ok it's a dir in fact
01847     kDebug(7102) << "it is a directory in fact";
01848     iError = ERR_IS_DIRECTORY;
01849     return statusServerError;
01850   }
01851 
01852   QString resumeOffset = metaData("resume");
01853   if ( !resumeOffset.isEmpty() )
01854   {
01855     llOffset = resumeOffset.toLongLong();
01856     kDebug(7102) << "got offset from metadata : " << llOffset;
01857   }
01858 
01859   if( !ftpOpenCommand("retr", url.path(), '?', ERR_CANNOT_OPEN_FOR_READING, llOffset) )
01860   {
01861     kWarning(7102) << "Can't open for reading";
01862     return statusServerError;
01863   }
01864 
01865   // Read the size from the response string
01866   if(m_size == UnknownSize)
01867   {
01868     const char* psz = strrchr( ftpResponse(4), '(' );
01869     if(psz) m_size = charToLongLong(psz+1);
01870     if (!m_size) m_size = UnknownSize;
01871   }
01872 
01873   // Send the mime-type...
01874   if (iCopyFile == -1) {
01875     StatusCode status = ftpSendMimeType(iError, url);
01876     if (status != statusSuccess) {
01877         return status;
01878     }
01879   }
01880 
01881   KIO::filesize_t bytesLeft = 0;
01882   if ( m_size != UnknownSize ) {
01883     bytesLeft = m_size - llOffset;
01884     totalSize( m_size );  // emit the total size...
01885   }
01886 
01887   kDebug(7102) << "starting with offset=" << llOffset;
01888   KIO::fileoffset_t processed_size = llOffset;
01889 
01890   QByteArray array;
01891   char buffer[maximumIpcSize];
01892   // start with small data chunks in case of a slow data source (modem)
01893   // - unfortunately this has a negative impact on performance for large
01894   // - files - so we will increase the block size after a while ...
01895   int iBlockSize = initialIpcSize;
01896   int iBufferCur = 0;
01897 
01898   while(m_size == UnknownSize || bytesLeft > 0)
01899   {  // let the buffer size grow if the file is larger 64kByte ...
01900     if(processed_size-llOffset > 1024 * 64)
01901       iBlockSize = maximumIpcSize;
01902 
01903     // read the data and detect EOF or error ...
01904     if(iBlockSize+iBufferCur > (int)sizeof(buffer))
01905       iBlockSize = sizeof(buffer) - iBufferCur;
01906     if (m_data->bytesAvailable() == 0)
01907       m_data->waitForReadyRead((readTimeout() * 1000));
01908     int n = m_data->read( buffer+iBufferCur, iBlockSize );
01909     if(n <= 0)
01910     {   // this is how we detect EOF in case of unknown size
01911       if( m_size == UnknownSize && n == 0 )
01912         break;
01913       // unexpected eof. Happens when the daemon gets killed.
01914       iError = ERR_COULD_NOT_READ;
01915       return statusServerError;
01916     }
01917     processed_size += n;
01918 
01919     // collect very small data chunks in buffer before processing ...
01920     if(m_size != UnknownSize)
01921     {
01922       bytesLeft -= n;
01923       iBufferCur += n;
01924       if(iBufferCur < minimumMimeSize && bytesLeft > 0)
01925       {
01926         processedSize( processed_size );
01927         continue;
01928       }
01929       n = iBufferCur;
01930       iBufferCur = 0;
01931     }
01932 
01933     // write output file or pass to data pump ...
01934     if(iCopyFile == -1)
01935     {
01936         array = QByteArray::fromRawData(buffer, n);
01937         data( array );
01938         array.clear();
01939     }
01940     else if( (iError = WriteToFile(iCopyFile, buffer, n)) != 0)
01941        return statusClientError;              // client side error
01942     processedSize( processed_size );
01943   }
01944 
01945   kDebug(7102) << "done";
01946   if(iCopyFile == -1)          // must signal EOF to data pump ...
01947     data(array);               // array is empty and must be empty!
01948 
01949   processedSize( m_size == UnknownSize ? processed_size : m_size );
01950   return statusSuccess;
01951 }
01952 
01953 #if 0
01954   void Ftp::mimetype( const KUrl& url )
01955   {
01956     if( !ftpOpenConnection(loginImplicit) )
01957           return;
01958 
01959     if ( !ftpOpenCommand( "retr", url.path(), 'I', ERR_CANNOT_OPEN_FOR_READING, 0 ) ) {
01960       kWarning(7102) << "Can't open for reading";
01961       return;
01962     }
01963     char buffer[ 2048 ];
01964     QByteArray array;
01965     // Get one chunk of data only and send it, KIO::Job will determine the
01966     // mimetype from it using KMimeMagic
01967     int n = m_data->read( buffer, 2048 );
01968     array.setRawData(buffer, n);
01969     data( array );
01970     array.resetRawData(buffer, n);
01971 
01972     kDebug(7102) << "aborting";
01973     ftpAbortTransfer();
01974 
01975     kDebug(7102) << "finished";
01976     finished();
01977     kDebug(7102) << "after finished";
01978   }
01979 
01980   void Ftp::ftpAbortTransfer()
01981   {
01982     // RFC 959, page 34-35
01983     // IAC (interpret as command) = 255 ; IP (interrupt process) = 254
01984     // DM = 242 (data mark)
01985      char msg[4];
01986      // 1. User system inserts the Telnet "Interrupt Process" (IP) signal
01987      //   in the Telnet stream.
01988      msg[0] = (char) 255; //IAC
01989      msg[1] = (char) 254; //IP
01990      (void) send(sControl, msg, 2, 0);
01991      // 2. User system sends the Telnet "Sync" signal.
01992      msg[0] = (char) 255; //IAC
01993      msg[1] = (char) 242; //DM
01994      if (send(sControl, msg, 2, MSG_OOB) != 2)
01995        ; // error...
01996 
01997      // Send ABOR
01998      kDebug(7102) << "send ABOR";
01999      QCString buf = "ABOR\r\n";
02000      if ( KSocks::self()->write( sControl, buf.data(), buf.length() ) <= 0 )  {
02001        error( ERR_COULD_NOT_WRITE, QString() );
02002        return;
02003      }
02004 
02005      //
02006      kDebug(7102) << "read resp";
02007      if ( readresp() != '2' )
02008      {
02009        error( ERR_COULD_NOT_READ, QString() );
02010        return;
02011      }
02012 
02013     kDebug(7102) << "close sockets";
02014     closeSockets();
02015   }
02016 #endif
02017 
02018 //===============================================================================
02019 // public: put           upload file to server
02020 // helper: ftpPut        called from put() and copy()
02021 //===============================================================================
02022 void Ftp::put(const KUrl& url, int permissions, KIO::JobFlags flags)
02023 {
02024   kDebug(7102) << url;
02025 
02026   int iError = 0;                           // iError gets status
02027   const StatusCode cs = ftpPut(iError, -1, url, permissions, flags);
02028   ftpCloseCommand();                        // must close command!
02029 
02030   if (cs == statusSuccess) {
02031     finished();
02032     return;
02033   }
02034 
02035   if (iError) {                             // can have only server side errs
02036     error(iError, url.path());
02037   }
02038 }
02039 
02040 Ftp::StatusCode Ftp::ftpPut(int& iError, int iCopyFile, const KUrl& dest_url,
02041                             int permissions, KIO::JobFlags flags)
02042 {
02043   if( !ftpOpenConnection(loginImplicit) )
02044     return statusServerError;
02045 
02046   // Don't use mark partial over anonymous FTP.
02047   // My incoming dir allows put but not rename...
02048   bool bMarkPartial;
02049   if (m_user.isEmpty () || m_user == FTP_LOGIN)
02050     bMarkPartial = false;
02051   else
02052     bMarkPartial = config()->readEntry("MarkPartial", true);
02053 
02054   QString dest_orig = dest_url.path();
02055   QString dest_part( dest_orig );
02056   dest_part += ".part";
02057 
02058   if ( ftpSize( dest_orig, 'I' ) )
02059   {
02060     if ( m_size == 0 )
02061     { // delete files with zero size
02062       QByteArray cmd = "DELE ";
02063       cmd += remoteEncoding()->encode(dest_orig);
02064       if( !ftpSendCmd( cmd ) || (m_iRespType != 2) )
02065       {
02066         iError = ERR_CANNOT_DELETE_PARTIAL;
02067         return statusServerError;
02068       }
02069     }
02070     else if ( !(flags & KIO::Overwrite) && !(flags & KIO::Resume) )
02071     {
02072        iError = ERR_FILE_ALREADY_EXIST;
02073        return statusServerError;
02074     }
02075     else if ( bMarkPartial )
02076     { // when using mark partial, append .part extension
02077       if ( !ftpRename( dest_orig, dest_part, KIO::Overwrite ) )
02078       {
02079         iError = ERR_CANNOT_RENAME_PARTIAL;
02080         return statusServerError;
02081       }
02082     }
02083     // Don't chmod an existing file
02084     permissions = -1;
02085   }
02086   else if ( bMarkPartial && ftpSize( dest_part, 'I' ) )
02087   { // file with extension .part exists
02088     if ( m_size == 0 )
02089     {  // delete files with zero size
02090       QByteArray cmd = "DELE ";
02091       cmd += remoteEncoding()->encode(dest_part);
02092       if ( !ftpSendCmd( cmd ) || (m_iRespType != 2) )
02093       {
02094         iError = ERR_CANNOT_DELETE_PARTIAL;
02095         return statusServerError;
02096       }
02097     }
02098     else if ( !(flags & KIO::Overwrite) && !(flags & KIO::Resume) )
02099     {
02100       flags |= canResume (m_size) ? KIO::Resume : KIO::DefaultFlags;
02101       if (!(flags & KIO::Resume))
02102       {
02103         iError = ERR_FILE_ALREADY_EXIST;
02104         return statusServerError;
02105       }
02106     }
02107   }
02108   else
02109     m_size = 0;
02110 
02111   QString dest;
02112 
02113   // if we are using marking of partial downloads -> add .part extension
02114   if ( bMarkPartial ) {
02115     kDebug(7102) << "Adding .part extension to " << dest_orig;
02116     dest = dest_part;
02117   } else
02118     dest = dest_orig;
02119 
02120   KIO::fileoffset_t offset = 0;
02121 
02122   // set the mode according to offset
02123   if( (flags & KIO::Resume) && m_size > 0 )
02124   {
02125     offset = m_size;
02126     if(iCopyFile != -1)
02127     {
02128       if( KDE_lseek(iCopyFile, offset, SEEK_SET) < 0 )
02129       {
02130         iError = ERR_CANNOT_RESUME;
02131         return statusClientError;
02132       }
02133     }
02134   }
02135 
02136   if (! ftpOpenCommand( "stor", dest, '?', ERR_COULD_NOT_WRITE, offset ) )
02137      return statusServerError;
02138 
02139   kDebug(7102) << "ftpPut: starting with offset=" << offset;
02140   KIO::fileoffset_t processed_size = offset;
02141 
02142   QByteArray buffer;
02143   int result;
02144   int iBlockSize = initialIpcSize;
02145   // Loop until we got 'dataEnd'
02146   do
02147   {
02148     if(iCopyFile == -1)
02149     {
02150       dataReq(); // Request for data
02151       result = readData( buffer );
02152     }
02153     else
02154     { // let the buffer size grow if the file is larger 64kByte ...
02155       if(processed_size-offset > 1024 * 64)
02156         iBlockSize = maximumIpcSize;
02157       buffer.resize(iBlockSize);
02158       result = ::read(iCopyFile, buffer.data(), buffer.size());
02159       if(result < 0)
02160         iError = ERR_COULD_NOT_WRITE;
02161       else
02162         buffer.resize(result);
02163     }
02164 
02165     if (result > 0)
02166     {
02167       m_data->write( buffer );
02168       while (m_data->bytesToWrite() && m_data->waitForBytesWritten()) {}
02169       processed_size += result;
02170       processedSize (processed_size);
02171     }
02172   }
02173   while ( result > 0 );
02174 
02175   if (result != 0) // error
02176   {
02177     ftpCloseCommand();               // don't care about errors
02178     kDebug(7102) << "Error during 'put'. Aborting.";
02179     if (bMarkPartial)
02180     {
02181       // Remove if smaller than minimum size
02182       if ( ftpSize( dest, 'I' ) &&
02183            ( processed_size < config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE) ) )
02184       {
02185         QByteArray cmd = "DELE ";
02186         cmd += remoteEncoding()->encode(dest);
02187         (void) ftpSendCmd( cmd );
02188       }
02189     }
02190     return statusServerError;
02191   }
02192 
02193   if ( !ftpCloseCommand() )
02194   {
02195     iError = ERR_COULD_NOT_WRITE;
02196     return statusServerError;
02197   }
02198 
02199   // after full download rename the file back to original name
02200   if ( bMarkPartial )
02201   {
02202     kDebug(7102) << "renaming dest (" << dest << ") back to dest_orig (" << dest_orig << ")";
02203     if ( !ftpRename( dest, dest_orig, KIO::Overwrite ) )
02204     {
02205       iError = ERR_CANNOT_RENAME_PARTIAL;
02206       return statusServerError;
02207     }
02208   }
02209 
02210   // set final permissions
02211   if ( permissions != -1 )
02212   {
02213     if ( m_user == FTP_LOGIN )
02214       kDebug(7102) << "Trying to chmod over anonymous FTP ???";
02215     // chmod the file we just put
02216     if ( ! ftpChmod( dest_orig, permissions ) )
02217     {
02218         // To be tested
02219         //if ( m_user != FTP_LOGIN )
02220         //    warning( i18n( "Could not change permissions for\n%1" ).arg( dest_orig ) );
02221     }
02222   }
02223 
02224   return statusSuccess;
02225 }
02226 
02229 bool Ftp::ftpSize( const QString & path, char mode )
02230 {
02231   m_size = UnknownSize;
02232   if( !ftpDataMode(mode) )
02233       return false;
02234 
02235   QByteArray buf;
02236   buf = "SIZE ";
02237   buf += remoteEncoding()->encode(path);
02238   if( !ftpSendCmd( buf ) || (m_iRespType != 2) )
02239     return false;
02240 
02241   // skip leading "213 " (response code)
02242   QByteArray psz (ftpResponse(4));
02243   if(psz.isEmpty())
02244     return false;
02245   bool ok = false;
02246   m_size = psz.trimmed().toLongLong(&ok);
02247   if (!ok) m_size = UnknownSize;
02248   return true;
02249 }
02250 
02251 bool Ftp::ftpFileExists(const QString& path)
02252 {
02253   QByteArray buf;
02254   buf = "SIZE ";
02255   buf += remoteEncoding()->encode(path);
02256   if( !ftpSendCmd( buf ) || (m_iRespType != 2) )
02257     return false;
02258 
02259   // skip leading "213 " (response code)
02260   const char* psz = ftpResponse(4);
02261   return psz != 0;
02262 }
02263 
02264 // Today the differences between ASCII and BINARY are limited to
02265 // CR or CR/LF line terminators. Many servers ignore ASCII (like
02266 // win2003 -or- vsftp with default config). In the early days of
02267 // computing, when even text-files had structure, this stuff was
02268 // more important.
02269 // Theoretically "list" could return different results in ASCII
02270 // and BINARY mode. But again, most servers ignore ASCII here.
02271 bool Ftp::ftpDataMode(char cMode)
02272 {
02273   if(cMode == '?') cMode = m_bTextMode ? 'A' : 'I';
02274   else if(cMode == 'a') cMode = 'A';
02275   else if(cMode != 'A') cMode = 'I';
02276 
02277   kDebug(7102) << "want" << cMode << "has" << m_cDataMode;
02278   if(m_cDataMode == cMode)
02279     return true;
02280 
02281   QByteArray buf = "TYPE ";
02282   buf += cMode;
02283   if( !ftpSendCmd(buf) || (m_iRespType != 2) )
02284       return false;
02285   m_cDataMode = cMode;
02286   return true;
02287 }
02288 
02289 
02290 bool Ftp::ftpFolder(const QString& path, bool bReportError)
02291 {
02292   QString newPath = path;
02293   int iLen = newPath.length();
02294   if(iLen > 1 && newPath[iLen-1] == '/') newPath.truncate(iLen-1);
02295 
02296   //kDebug(7102) << "want" << newPath << "has" << m_currentPath;
02297   if(m_currentPath == newPath)
02298     return true;
02299 
02300   QByteArray tmp = "cwd ";
02301   tmp += remoteEncoding()->encode(newPath);
02302   if( !ftpSendCmd(tmp) )
02303     return false;                  // connection failure
02304   if(m_iRespType != 2)
02305   {
02306     if(bReportError)
02307       error(ERR_CANNOT_ENTER_DIRECTORY, path);
02308     return false;                  // not a folder
02309   }
02310   m_currentPath = newPath;
02311   return true;
02312 }
02313 
02314 
02315 //===============================================================================
02316 // public: copy          don't use kio data pump if one side is a local file
02317 // helper: ftpCopyPut    called from copy() on upload
02318 // helper: ftpCopyGet    called from copy() on download
02319 //===============================================================================
02320 void Ftp::copy( const KUrl &src, const KUrl &dest, int permissions, KIO::JobFlags flags )
02321 {
02322   int iError = 0;
02323   int iCopyFile = -1;
02324   StatusCode cs = statusSuccess;
02325   bool bSrcLocal = src.isLocalFile();
02326   bool bDestLocal = dest.isLocalFile();
02327   QString  sCopyFile;
02328 
02329   if(bSrcLocal && !bDestLocal)                    // File -> Ftp
02330   {
02331     sCopyFile = src.toLocalFile();
02332     kDebug(7102) << "local file" << sCopyFile << "-> ftp" << dest.path();
02333     cs = ftpCopyPut(iError, iCopyFile, sCopyFile, dest, permissions, flags);
02334     if( cs == statusServerError) sCopyFile = dest.url();
02335   }
02336   else if(!bSrcLocal && bDestLocal)               // Ftp -> File
02337   {
02338     sCopyFile = dest.toLocalFile();
02339     kDebug(7102) << "ftp" << src.path() << "-> local file" << sCopyFile;
02340     cs = ftpCopyGet(iError, iCopyFile, sCopyFile, src, permissions, flags);
02341     if( cs == statusServerError ) sCopyFile = src.url();
02342   }
02343   else {
02344     error( ERR_UNSUPPORTED_ACTION, QString() );
02345     return;
02346   }
02347 
02348   // perform clean-ups and report error (if any)
02349   if(iCopyFile != -1)
02350     ::close(iCopyFile);
02351   ftpCloseCommand();                        // must close command!
02352   if(iError)
02353     error(iError, sCopyFile);
02354   else
02355     finished();
02356 }
02357 
02358 
02359 Ftp::StatusCode Ftp::ftpCopyPut(int& iError, int& iCopyFile, const QString &sCopyFile,
02360                                 const KUrl& url, int permissions, KIO::JobFlags flags)
02361 {
02362   // check if source is ok ...
02363   KDE_struct_stat buff;
02364   bool bSrcExists = (KDE::stat( sCopyFile, &buff ) != -1);
02365   if(bSrcExists)
02366   { if(S_ISDIR(buff.st_mode))
02367     {
02368       iError = ERR_IS_DIRECTORY;
02369       return statusClientError;
02370     }
02371   }
02372   else
02373   {
02374     iError = ERR_DOES_NOT_EXIST;
02375     return statusClientError;
02376   }
02377 
02378   iCopyFile = KDE::open( sCopyFile, O_RDONLY );
02379   if(iCopyFile == -1)
02380   {
02381     iError = ERR_CANNOT_OPEN_FOR_READING;
02382     return statusClientError;
02383   }
02384 
02385   // delegate the real work (iError gets status) ...
02386   totalSize(buff.st_size);
02387 #ifdef  ENABLE_CAN_RESUME
02388   return ftpPut(iError, iCopyFile, url, permissions, flags & ~KIO::Resume);
02389 #else
02390   return ftpPut(iError, iCopyFile, url, permissions, flags | KIO::Resume);
02391 #endif
02392 }
02393 
02394 
02395 Ftp::StatusCode Ftp::ftpCopyGet(int& iError, int& iCopyFile, const QString &sCopyFile,
02396                                 const KUrl& url, int permissions, KIO::JobFlags flags)
02397 {
02398   // check if destination is ok ...
02399   KDE_struct_stat buff;
02400   const bool bDestExists = (KDE::stat( sCopyFile, &buff ) != -1);
02401   if(bDestExists)
02402   { if(S_ISDIR(buff.st_mode))
02403     {
02404       iError = ERR_IS_DIRECTORY;
02405       return statusClientError;
02406     }
02407     if(!(flags & KIO::Overwrite))
02408     {
02409       iError = ERR_FILE_ALREADY_EXIST;
02410       return statusClientError;
02411     }
02412   }
02413 
02414   // do we have a ".part" file?
02415   const QString sPart = sCopyFile + QLatin1String(".part");
02416   bool bResume = false;
02417   const bool bPartExists = (KDE::stat( sPart, &buff ) != -1);
02418   const bool bMarkPartial = config()->readEntry("MarkPartial", true);
02419   const QString dest = bMarkPartial ? sPart : sCopyFile;
02420   if (bMarkPartial && bPartExists && buff.st_size > 0)
02421   { // must not be a folder! please fix a similar bug in kio_file!!
02422     if(S_ISDIR(buff.st_mode))
02423     {
02424       iError = ERR_DIR_ALREADY_EXIST;
02425       return statusClientError;                            // client side error
02426     }
02427     //doesn't work for copy? -> design flaw?
02428 #ifdef  ENABLE_CAN_RESUME
02429     bResume = canResume( buff.st_size );
02430 #else
02431     bResume = true;
02432 #endif
02433   }
02434 
02435   if (bPartExists && !bResume)                  // get rid of an unwanted ".part" file
02436     QFile::remove(sPart);
02437 
02438   // WABA: Make sure that we keep writing permissions ourselves,
02439   // otherwise we can be in for a surprise on NFS.
02440   mode_t initialMode;
02441   if (permissions != -1)
02442     initialMode = permissions | S_IWUSR;
02443   else
02444     initialMode = 0666;
02445 
02446   // open the output file ...
02447   KIO::fileoffset_t hCopyOffset = 0;
02448   if (bResume) {
02449     iCopyFile = KDE::open( sPart, O_RDWR );  // append if resuming
02450     hCopyOffset = KDE_lseek(iCopyFile, 0, SEEK_END);
02451     if(hCopyOffset < 0)
02452     {
02453       iError = ERR_CANNOT_RESUME;
02454       return statusClientError;                            // client side error
02455     }
02456     kDebug(7102) << "resuming at " << hCopyOffset;
02457   }
02458   else {
02459     iCopyFile = KDE::open(dest, O_CREAT | O_TRUNC | O_WRONLY, initialMode);
02460   }
02461 
02462   if(iCopyFile == -1)
02463   {
02464     kDebug(7102) << "### COULD NOT WRITE " << sCopyFile;
02465     iError = (errno == EACCES) ? ERR_WRITE_ACCESS_DENIED
02466                                : ERR_CANNOT_OPEN_FOR_WRITING;
02467     return statusClientError;
02468   }
02469 
02470   // delegate the real work (iError gets status) ...
02471   StatusCode iRes = ftpGet(iError, iCopyFile, url, hCopyOffset);
02472   if( ::close(iCopyFile) && iRes == statusSuccess )
02473   {
02474     iError = ERR_COULD_NOT_WRITE;
02475     iRes = statusClientError;
02476   }
02477   iCopyFile = -1;
02478 
02479   // handle renaming or deletion of a partial file ...
02480   if(bMarkPartial)
02481   {
02482     if(iRes == statusSuccess)
02483     { // rename ".part" on success
02484       if ( KDE::rename( sPart, sCopyFile ) )
02485       {
02486         // If rename fails, try removing the destination first if it exists.
02487         if (!bDestExists || !(QFile::remove(sCopyFile) && KDE::rename(sPart, sCopyFile) == 0)) {
02488             kDebug(7102) << "cannot rename " << sPart << " to " << sCopyFile;
02489             iError = ERR_CANNOT_RENAME_PARTIAL;
02490             iRes = statusClientError;
02491         }
02492       }
02493     }
02494     else if(KDE::stat( sPart, &buff ) == 0)
02495     { // should a very small ".part" be deleted?
02496       int size = config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE);
02497       if (buff.st_size <  size)
02498         QFile::remove(sPart);
02499     }
02500   }
02501   return iRes;
02502 }
02503 
02504 Ftp::StatusCode Ftp::ftpSendMimeType(int& iError, const KUrl& url)
02505 {
02506   const int totalSize = ((m_size == UnknownSize || m_size > 1024) ? 1024 : m_size);
02507   QByteArray buffer(totalSize, '\0');
02508 
02509   while (true) {
02510       // Wait for content to be available...
02511       if (m_data->bytesAvailable() == 0 && !m_data->waitForReadyRead((readTimeout() * 1000))) {
02512           iError = ERR_COULD_NOT_READ;
02513           return statusServerError;
02514       }
02515 
02516       const int bytesRead = m_data->peek(buffer.data(), totalSize);
02517 
02518       // If we got a -1, it must be an error so return an error.
02519       if (bytesRead == -1) {
02520           iError = ERR_COULD_NOT_READ;
02521           return statusServerError;
02522       }
02523 
02524       // If m_size is unknown, peek returns 0 (0 sized file ??), or peek returns size
02525       // equal to the size we want, then break.
02526       if (bytesRead == 0 || bytesRead == totalSize || m_size == UnknownSize) {
02527           break;
02528       }
02529   }
02530 
02531   if (!buffer.isEmpty()) {
02532       KMimeType::Ptr mime = KMimeType::findByNameAndContent(url.fileName(), buffer);
02533       kDebug(7102) << "Emitting mimetype" << mime->name();
02534       mimeType( mime->name() ); // emit the mime type...
02535   }
02536 
02537   return statusSuccess;
02538 }
02539 
02540 void Ftp::proxyAuthentication(const QNetworkProxy& proxy, QAuthenticator* authenticator)
02541 {
02542     Q_UNUSED(proxy);
02543     kDebug(7102) << "Authenticator received -- realm:" << authenticator->realm() << "user:"
02544                  << authenticator->user();
02545 
02546     AuthInfo info;
02547     info.url = m_proxyURL;
02548     info.realmValue = authenticator->realm();
02549     info.verifyPath = true;    //### whatever
02550     info.username = authenticator->user();
02551 
02552     const bool haveCachedCredentials = checkCachedAuthentication(info);
02553 
02554     // if m_socketProxyAuth is a valid pointer then authentication has been attempted before,
02555     // and it was not successful. see below and saveProxyAuthenticationForSocket().
02556     if (!haveCachedCredentials || m_socketProxyAuth) {
02557         // Save authentication info if the connection succeeds. We need to disconnect
02558         // this after saving the auth data (or an error) so we won't save garbage afterwards!
02559         connect(m_control, SIGNAL(connected()), this, SLOT(saveProxyAuthentication()));
02560         //### fillPromptInfo(&info);
02561         info.prompt = i18n("You need to supply a username and a password for "
02562                            "the proxy server listed below before you are allowed "
02563                            "to access any sites.");
02564         info.keepPassword = true;
02565         info.commentLabel = i18n("Proxy:");
02566         info.comment = i18n("<b>%1</b> at <b>%2</b>", info.realmValue, m_proxyURL.host());
02567         const bool dataEntered = openPasswordDialog(info, i18n("Proxy Authentication Failed."));
02568         if (!dataEntered) {
02569             kDebug(7102) << "looks like the user canceled proxy authentication.";
02570             error(ERR_USER_CANCELED, m_proxyURL.host());
02571             return;
02572         }
02573     }
02574     authenticator->setUser(info.username);
02575     authenticator->setPassword(info.password);
02576     authenticator->setOption(QLatin1String("keepalive"), info.keepPassword);
02577 
02578     if (m_socketProxyAuth) {
02579         *m_socketProxyAuth = *authenticator;
02580     } else {
02581         m_socketProxyAuth = new QAuthenticator(*authenticator);
02582     }
02583 
02584     m_proxyURL.setUser(info.username);
02585     m_proxyURL.setPassword(info.password);
02586 }
02587 
02588 void Ftp::saveProxyAuthentication()
02589 {
02590     kDebug(7102);
02591     disconnect(m_control, SIGNAL(connected()), this, SLOT(saveProxyAuthentication()));
02592     Q_ASSERT(m_socketProxyAuth);
02593     if (m_socketProxyAuth) {
02594         kDebug(7102) << "-- realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user();
02595         KIO::AuthInfo a;
02596         a.verifyPath = true;
02597         a.url = m_proxyURL;
02598         a.realmValue = m_socketProxyAuth->realm();
02599         a.username = m_socketProxyAuth->user();
02600         a.password = m_socketProxyAuth->password();
02601         a.keepPassword = m_socketProxyAuth->option(QLatin1String("keepalive")).toBool();
02602         cacheAuthentication(a);
02603     }
02604     delete m_socketProxyAuth;
02605     m_socketProxyAuth = 0;
02606 }

KIOSlave

Skip menu "KIOSlave"
  • Main Page
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • File Members
  • Related Pages

kdelibs

Skip menu "kdelibs"
  • DNSSD
  • Interfaces
  •   KHexEdit
  •   KMediaPlayer
  •   KSpeech
  •   KTextEditor
  • kconf_update
  • KDE3Support
  •   KUnitTest
  • KDECore
  • KDED
  • KDEsu
  • KDEUI
  • KDEWebKit
  • KDocTools
  • KFile
  • KHTML
  • KImgIO
  • KInit
  • kio
  • KIOSlave
  • KJS
  •   KJS-API
  •   WTF
  • kjsembed
  • KNewStuff
  • KParts
  • KPty
  • Kross
  • KUnitConversion
  • KUtils
  • Nepomuk
  • Plasma
  • Solid
  • Sonnet
  • ThreadWeaver
Generated for kdelibs by doxygen 1.7.6.1
This website is maintained by Adriaan de Groot and Allen Winter.
KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal