/*
 * This file is part of the KFTPGrabber project
 *
 * Copyright (C) 2003-2006 by the KFTPGrabber developers
 * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
 * NON-INFRINGEMENT.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.  If you modify
 * file(s) with this exception, you may extend this exception to your
 * version of the file(s), but you are not obligated to do so.  If you
 * do not wish to do so, delete this exception statement from your
 * version.  If you delete this exception statement from all source
 * files in the program, then also delete it here.
 */

#include "ftpsocket.h"
#include "thread.h"
#include "ftpdirectoryparser.h"
#include "cache.h"
#include "speedlimiter.h"
#include "ssl.h"

#include "misc/kftpotpgenerator.h"
#include "misc/config.h"

#include <qdir.h>

#include <klocale.h>
#include <kstandarddirs.h>
#include <ksocketdevice.h>

#include <utime.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>

namespace KFTPEngine {

FtpSocket::FtpSocket(Thread *thread)
 : KNetwork::KStreamSocket(),
   Socket(thread, "ftp"),
   SpeedLimiterItem(),
   m_login(false),
   m_transferSocket(0),
   m_directoryParser(0),
   m_controlConnecting(false),
   m_controlSsl(0),
   m_dataSsl(0),
   m_clientCert(0)
{
  enableRead(false);
  setBlocking(false);
}

FtpSocket::~FtpSocket()
{
  protoDisconnect();
}

void FtpSocket::poll()
{
  if (m_controlConnecting) {
    if (isFatalError(error())) {
      slotError();
      resetError();
      m_controlConnecting = false;
      return;
    }
    
    if (state() == Connected) {
      m_controlConnecting = false;
      slotConnected();
    }
    
    return;
  }
  
  slotControlTryRead();
    
  if (!m_buffer.isEmpty())
    processBuffer();

  if (m_transferSocket) {
    if (m_transferConnecting && m_transferSocket->state() == Connected) {
      m_transferConnecting = false;
      slotDataConnected();
    } else if (!m_transferConnecting) {
      if (getCurrentCommand() == Commands::CmdPut) {
        if (m_transferStart >= 2)
          slotDataTryWrite();
      } else {
        bool input;
        m_transferSocket->socketDevice()->poll(&input, 0, 0, 0);
        
        if (input)
          slotDataTryRead();
      }
    }
  } else if (m_serverSocket) {
    bool input;
    m_serverSocket->socketDevice()->poll(&input, 0, 0, 0);
    
    if (input) {
      KNetwork::KActiveSocketBase *socket = m_serverSocket->accept();
      
      if (socket) {
        slotDataAccept(static_cast<KNetwork::KStreamSocket*>(socket));
        m_transferConnecting = false;
      }
    }
  }
  
  // Check for timeouts
  // NOTE This should be moved to a QTimer's slot when ported to Qt 4
  timeoutCheck();
  keepaliveCheck();
}

void FtpSocket::slotControlTryRead()
{
  QString tmpStr;
  Q_LONG size = 0;
  
  // Read what we can
  if (getConfigInt("ssl") && m_controlSsl) {
    size = m_controlSsl->read(m_controlBuffer, sizeof(m_controlBuffer) - 1);
    
    if (size == -1) {
      protoDisconnect();
      return;
    }
  } else
    size = readBlock(m_controlBuffer, sizeof(m_controlBuffer) - 1);
  
  if (error() != NoError) {
    // Have we been disconnected ?
    if (error() != WouldBlock)
      protoDisconnect();

    return;
  }
  
  if (size == 0)
    return;
  
  for (int i = 0; i < size; i++)
    if (m_controlBuffer[i] == 0)
      m_controlBuffer[i] = '!';
  
  memset(m_controlBuffer + size, 0, sizeof(m_controlBuffer) - size);
  m_buffer.append(m_controlBuffer);
}

void FtpSocket::processBuffer()
{
  // Parse any lines we might have
  int pos;
  while ((pos = m_buffer.find('\n')) > -1) {
    QString line = m_buffer.mid(0, pos);
    line = m_remoteEncoding->decode(QCString(line.ascii()));
    parseLine(line);
    
    // Remove what we just parsed
    m_buffer.remove(0, pos + 1);
  }
}

void FtpSocket::parseLine(const QString &line)
{
  // Is this the end of multiline response ?
  if (!m_multiLineCode.isEmpty() && line.left(4) == m_multiLineCode) {
    m_multiLineCode = "";
    emitEvent(Event::EventResponse, line);
  } else if (line[3] == '-' && m_multiLineCode.isEmpty()) {
    m_multiLineCode = line.left(3) + " ";
    emitEvent(Event::EventMultiline, line);
  } else if (!m_multiLineCode.isEmpty()) {
    emitEvent(Event::EventMultiline, line);
  } else {
    // Normal response
    emitEvent(Event::EventResponse, line);
  }
  
  timeoutWait(false);
  
  // Parse our response
  m_response = line;
  nextCommand();
}

bool FtpSocket::isResponse(const QString &code)
{
  QString ref;
  
  if (isMultiline())
    ref = m_multiLineCode;
  else
    ref = m_response;
    
  return ref.left(code.length()) == code;
}

void FtpSocket::sendCommand(const QString &command)
{
  emitEvent(Event::EventCommand, command);
  QCString buffer(m_remoteEncoding->encode(command) + "\r\n");
  
  if (getConfigInt("ssl") && m_controlSsl)
    m_controlSsl->write(buffer.data(), buffer.length());
  else
    writeBlock(buffer.data(), buffer.length());
    
  timeoutWait(true);
}

void FtpSocket::resetCommandClass(ResetCode code)
{
  timeoutWait(false);
  
  if (m_transferSocket && code != Ok) {
    // Invalidate the socket
    closeDataTransferSocket();
    
    // Close the file that failed transfer
    if (getTransferFile()->isOpen()) {
      getTransferFile()->close();
      
      if (getCurrentCommand() == Commands::CmdGet && getTransferFile()->size() == 0)
        getTransferFile()->remove();
    }
  }
  
  if (m_serverSocket && code != Ok)
    delete m_serverSocket;
  
  Socket::resetCommandClass(code);
}

// *******************************************************************************************
// ***************************************** CONNECT *****************************************
// *******************************************************************************************

class FtpCommandConnect : public Commands::Base {
public:
    enum State {
      None,
      SentAuthTls,
      SentUser,
      SentPass,
      SentPbsz,
      SentProt,
      DoingSyst,
      DoingFeat,
      SentPwd
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandConnect, FtpSocket, CmdNone)
    
    void process()
    {
      switch (currentState) {
        case None: {
          if (!socket()->isMultiline()) {
            if (socket()->isResponse("2")) {
              // Negotiate a SSL connection if configured
              if (socket()->getConfigInt("ssl.use_tls")) {
                currentState = SentAuthTls;
                socket()->sendCommand("AUTH TLS");
              } else {
                // Send username
                currentState = SentUser;
                socket()->sendCommand("USER " + socket()->getCurrentUrl().user());
              }
            } else {
              socket()->emitEvent(Event::EventMessage, i18n("Connection has failed."));
              
              socket()->protoAbort();
              socket()->emitError(ConnectFailed);
            }
          }
          break;
        }
        case SentAuthTls: {
          if (socket()->isResponse("2")) {
            socket()->m_controlSsl = new Ssl(socket());
            
            // Setup client certificate if one was provided
            if (socket()->m_clientCert)
              socket()->m_controlSsl->setClientCertificate(socket()->m_clientCert);
              
            if (socket()->m_controlSsl->connect()) {
              socket()->emitEvent(Event::EventMessage, i18n("SSL negotiation successful. Connection is secured with %1 bit cipher %2.").arg(socket()->m_controlSsl->connectionInfo().getCipherUsedBits()).arg(socket()->m_controlSsl->connectionInfo().getCipher()));
              socket()->setConfig("ssl", 1);
              
              // Now send the username
              currentState = SentUser;
              socket()->sendCommand("USER " + socket()->getCurrentUrl().user());
            } else {
              delete socket()->m_controlSsl;
              socket()->m_controlSsl = 0;
              
              socket()->emitEvent(Event::EventMessage, i18n("SSL negotiation failed. Login aborted."));
              socket()->resetCommandClass(Failed);
              
              socket()->protoAbort();
            }
          } else {
            socket()->emitEvent(Event::EventMessage, i18n("SSL negotiation request failed. Login aborted."));
            socket()->resetCommandClass(Failed);
            
            socket()->protoAbort();
          }
          break;
        }
        case SentUser: {
          if (socket()->isResponse("331")) {
            // Send password
            if (socket()->isResponse("331 Response to otp-") ||
                socket()->isResponse("331 Response to s/key")) {
              // OTP: 331 Response to otp-md5 41 or4828 ext required for foo.
              QString tmp = socket()->getResponse();
              tmp = tmp.section(' ', 3, 5);
              
              KFTPOTPGenerator otp(tmp, socket()->getCurrentUrl().pass());
              currentState = SentPass;
              socket()->sendCommand("PASS " + otp.generateOTP());
            } else {
              socket()->sendCommand("PASS " + socket()->getCurrentUrl().pass());
              currentState = SentPass;
            }
          } else if (socket()->isResponse("230")) {
            // Some servers imediately send the 230 response for anonymous accounts
            if (!socket()->isMultiline()) {
              if (socket()->getConfigInt("ssl")) {
                currentState = SentPbsz;
                socket()->sendCommand("PBSZ 0");
              } else {
                // Do SYST
                socket()->sendCommand("SYST");
                currentState = DoingSyst;
              }
            }
          } else {
            socket()->emitEvent(Event::EventMessage, i18n("Login has failed."));
            
            socket()->protoAbort();
            socket()->emitError(LoginFailed);
          }
          break;
        }
        case SentPass: {
          if (socket()->isResponse("230")) {
            if (!socket()->isMultiline()) {
              if (socket()->getConfigInt("ssl")) {
                currentState = SentPbsz;
                socket()->sendCommand("PBSZ 0");
              } else {
                // Do SYST
                socket()->sendCommand("SYST");
                currentState = DoingSyst;
              }
            }
          } else {
            socket()->emitEvent(Event::EventMessage, i18n("Login has failed."));
            
            socket()->protoAbort();
            socket()->emitError(LoginFailed);
          }
          break;
        }
        case SentPbsz: {
          currentState = SentProt;
          QString prot = "PROT ";
          
          if (socket()->getConfigInt("ssl.prot_mode") == 0) 
            prot.append('P');
          else
            prot.append('C');

          socket()->sendCommand(prot);
          break;
        }
        case SentProt: {
          if (socket()->isResponse("5")) {
            // Fallback to unencrypted data channel
            socket()->setConfig("ssl.prot_mode", 2);
          }
          
          currentState = DoingSyst;
          socket()->sendCommand("SYST");
          break;
        }
        case DoingSyst: {
          socket()->sendCommand("FEAT");
          currentState = DoingFeat;
          break;
        }
        case DoingFeat: {
          if (socket()->isMultiline()) {
            parseFeat();
          } else {
            socket()->sendCommand("PWD");
            currentState = SentPwd;
          }
          break;
        }
        case SentPwd: {
          // Parse the current working directory
          if (socket()->isResponse("2")) {
            // 257 "/home/default/path"
            QString tmp = socket()->getResponse();
            int first = tmp.find('"') + 1;
            tmp = tmp.mid(first, tmp.findRev('"') - first);
            
            socket()->setDefaultDirectory(tmp);
            socket()->setCurrentDirectory(tmp);
          }
          
          // Enable transmission of keepalive events
          socket()->keepaliveStart();
          
          currentState = None;
          socket()->emitEvent(Event::EventMessage, i18n("Connected."));
          socket()->emitEvent(Event::EventConnect);
          socket()->m_login = true;
          socket()->resetCommandClass();
          break;
        }
      }
    }
    
    void parseFeat()
    {
      QString feat = socket()->getResponse().stripWhiteSpace().upper();
      
      if (feat.left(3).toInt() > 0 && feat[3] == '-')
        feat.remove(0, 4);
      
      if (feat.left(4) == "MDTM") {
        // Server has MDTM (MoDification TiMe) support
        socket()->setConfig("feat.mdtm", 1);
      } else if (feat.left(4) == "PRET") {
        // Server is a distributed ftp server and requires PRET for transfers
        socket()->setConfig("feat.pret", 1);
      } else if (feat.left(4) == "MLSD") {
        // Server supports machine-friendly directory listings
        socket()->setConfig("feat.mlsd", 1);
      } else if (feat.left(4) == "REST") {
        // Server supports resume operations
        socket()->setConfig("feat.rest", 1);
      } else if (feat.left(4) == "SSCN") {
        // Server supports SSCN for secure site-to-site transfers
        socket()->setConfig("feat.sscn", 1);
        socket()->setConfig("feat.cpsv", 0);
      } else if (feat.left(4) == "CPSV" && !socket()->getConfigInt("feat.sscn")) {
        // Server supports CPSV for secure site-to-site transfers
        socket()->setConfig("feat.cpsv", 1);
      }
    }
};

void FtpSocket::protoConnect(const KURL &url)
{
  emitEvent(Event::EventState, i18n("Connecting..."));
  emitEvent(Event::EventMessage, i18n("Connecting to %1:%2...").arg(url.host()).arg(url.port()));
  
  if (!getConfig("encoding").isEmpty())
    changeEncoding(getConfig("encoding"));
  
  // Start the connect procedure
  m_controlConnecting = true;
  setCurrentUrl(url);
  KNetwork::KStreamSocket::connect(url.host(), QString::number(url.port()));
}

void FtpSocket::slotConnected()
{
  if (getConfigInt("ssl.use_implicit")) {
    m_controlSsl = new Ssl(this);
    
    // Setup client certificate if one was provided
    if (m_clientCert)
      m_controlSsl->setClientCertificate(m_clientCert);

    if (m_controlSsl->connect()) {
      emitEvent(Event::EventMessage, i18n("SSL negotiation successful. Connection is secured with %1 bit cipher %2.").arg(m_controlSsl->connectionInfo().getCipherUsedBits()).arg(m_controlSsl->connectionInfo().getCipher()));
      setConfig("ssl", 1);
    } else {
      delete m_controlSsl;
      m_controlSsl = 0;
      
      emitEvent(Event::EventMessage, i18n("SSL negotiation failed. Connect aborted."));
      resetCommandClass(Failed);
      
      protoAbort();
    }
  }
  
  timeoutWait(true);

  emitEvent(Event::EventState, i18n("Logging in..."));
  emitEvent(Event::EventMessage, i18n("Connected with server, waiting for welcome message..."));
  setupCommandClass(FtpCommandConnect);
}

void FtpSocket::slotError()
{
  if (isFatalError(error())) {
    emitEvent(Event::EventMessage, i18n("Failed to connect (%1)").arg(errorString(error())));
    emitError(ConnectFailed);
    
    resetCommandClass(FailedSilently);
  }
}

// *******************************************************************************************
// **************************************** DISCONNECT ***************************************
// *******************************************************************************************

void FtpSocket::protoDisconnect()
{
  Socket::protoDisconnect();
  
  // Close SSL
  if (getConfigInt("ssl") && m_controlSsl) {
    m_controlSsl->close();
    delete m_controlSsl;
    m_controlSsl = 0;
    
    if (m_clientCert) {
      delete m_clientCert;
      m_clientCert = 0;
    }
  }
  
  // Terminate the connection
  m_login = false;
  KNetwork::KStreamSocket::close();
}

void FtpSocket::protoAbort()
{
  Socket::protoAbort();
  
  if (getCurrentCommand() != Commands::CmdNone) {
    // Abort current command
    if (getCurrentCommand() == Commands::CmdConnect)
      protoDisconnect();
      
    if (m_cmdData)
      resetCommandClass(UserAbort);
    
    emitEvent(Event::EventMessage, i18n("Aborted."));
  }
}

// *******************************************************************************************
// ********************************* NEGOTIATE DATA CONNECTION *******************************
// *******************************************************************************************

class FtpCommandNegotiateData : public Commands::Base {
public:
    enum State {
      None,
      SentSscnOff,
      SentType,
      SentProt,
      SentPret,
      NegotiateActive,
      NegotiatePasv,
      NegotiateEpsv,
      HaveConnection,
      SentRest,
      SentDataCmd,
      WaitTransfer
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandNegotiateData, FtpSocket, CmdNone)
    
    void process()
    {
      switch (currentState) {
        case None: {
          if (socket()->getConfigInt("sscn.activated")) {
            // First disable SSCN
            currentState = SentSscnOff;
            socket()->sendCommand("SSCN OFF");
            return;
          }
        }
        case SentSscnOff: {
          if (currentState == SentSscnOff)
            socket()->setConfig("sscn.activated", 0);
          
          // Change type
          currentState = SentType;
          socket()->resetTransferStart();
          
          QString type = "TYPE ";
          type.append(socket()->getConfigInt("params.data_type"));
          socket()->sendCommand(type);
          break;
        }
        case SentType: {
          if (socket()->getConfigInt("ssl") && socket()->getConfigInt("ssl.prot_mode") == 1) {
            currentState = SentProt;
            
            if (socket()->getPreviousCommand() == Commands::CmdList)
              socket()->sendCommand("PROT P");
            else
              socket()->sendCommand("PROT C"); 
          } else if (socket()->getConfigInt("feat.pret")) {
            currentState = SentPret;
            socket()->sendCommand("PRET " + socket()->getConfig("params.data_command"));
          } else {
            negotiateDataConnection();
          }
          break;
        }
        case SentProt: {
          if (socket()->getConfigInt("feat.pret")) {
            currentState = SentPret;
            socket()->sendCommand("PRET " + socket()->getConfig("params.data_command"));
          } else {
            negotiateDataConnection();
          }
          break;
        }
        case SentPret: {
          // PRET failed because of filesystem problems, abort right away!
          if (socket()->isResponse("530")) {
            socket()->emitError(PermissionDenied);
            socket()->resetCommandClass(Failed);
            return;
          } else if (socket()->isResponse("550")) {
            socket()->emitError(FileNotFound);
            socket()->resetCommandClass(Failed);
            return;
          } else if (socket()->isResponse("5")) {
            // PRET is not supported, disable for future use
            socket()->setConfig("feat.pret", 0);
          }
          
          negotiateDataConnection();
          break;
        }
        case NegotiateActive: negotiateActive(); break;
        case NegotiateEpsv: negotiateEpsv(); break;
        case NegotiatePasv: negotiatePasv(); break;
        case HaveConnection: {
          // We have the connection
          if (socket()->getConfigInt("params.data_rest_do")) {
            currentState = SentRest;
            socket()->sendCommand("REST " + QString::number(socket()->getConfigFs("params.data_rest")));
          } else {
            currentState = SentDataCmd;
            socket()->sendCommand(socket()->getConfig("params.data_command"));
          }
          break;
        }
        case SentRest: {
          if (!socket()->isResponse("2") && !socket()->isResponse("3")) {
            socket()->setConfig("feat.rest", 0);
            socket()->getTransferFile()->close();
            
            bool ok;
            
            if (socket()->getPreviousCommand() == Commands::CmdGet)
              ok = socket()->getTransferFile()->open(IO_WriteOnly | IO_Truncate);
            else
              ok = socket()->getTransferFile()->open(IO_ReadOnly);
            
            // Check if there was a problem opening the file
            if (!ok) {
              socket()->emitError(FileOpenFailed);
              socket()->resetCommandClass(Failed);
              return;
            }
          }
          
          // We have sent REST, now send the data command
          currentState = SentDataCmd;
          socket()->sendCommand(socket()->getConfig("params.data_command"));
          break;
        }
        case SentDataCmd: {
          if (!socket()->isResponse("1")) {
            // Some problems while executing the data command
            socket()->resetCommandClass(Failed);
            return;
          }
          
          if (!socket()->isMultiline()) {
            socket()->checkTransferStart();
            currentState = WaitTransfer;
          }
          break;
        }
        case WaitTransfer: {
          if (!socket()->isResponse("2")) {
            // Transfer has failed
            socket()->resetCommandClass(Failed);
            return;
          }
          
          if (!socket()->isMultiline()) {
            // Transfer has been completed
            socket()->checkTransferEnd();
          }
          break;
        }
      }
    }
    
    void negotiateDataConnection()
    {
      if (socket()->getConfigInt("feat.epsv")) {
        negotiateEpsv();
      } else if (socket()->getConfigInt("feat.pasv")) {
        negotiatePasv();
      } else {
        negotiateActive();
      }
    }
    
    void negotiateEpsv()
    {
      if (currentState == NegotiateEpsv) {
        if (!socket()->isResponse("2")) {
          // Negotiation failed
          socket()->setConfig("feat.epsv", "0");
          
          // Try the next thing
          negotiateDataConnection();
          return;
        }
        
        // 229 Entering Extended Passive Mode (|||55016|)
        char *begin = strchr(socket()->getResponse().ascii(), '(');
        int port;
      
        if (!begin || sscanf(begin, "(|||%d|)", &port) != 1) {
          // Unable to parse, try the next thing
          socket()->setConfig("feat.epsv", "0");
          negotiateDataConnection();
          return;
        }
        
        // We have the address, let's setup the transfer socket and then
        // we are done.
        currentState = HaveConnection;
        socket()->setupPassiveTransferSocket(QString::null, port);
      } else {
        // Just send the EPSV command
        currentState = NegotiateEpsv;
        socket()->sendCommand("EPSV");
      }
    }
    
    void negotiatePasv()
    {
      if (currentState == NegotiatePasv) {
        if (!socket()->isResponse("2")) {
          // Negotiation failed
          socket()->setConfig("feat.pasv", "0");
          
          // Try the next thing
          negotiateDataConnection();
          return;
        }
        
        // Ok PASV command successfull - let's parse the result
        int ip[6];
        char *begin = strchr(socket()->getResponse().ascii(), '(');
      
        // Some stinky servers don't respect RFC and do it on their own
        if (!begin)
          begin = strchr(socket()->getResponse().ascii(), '=');
      
        if (!begin || (sscanf(begin, "(%d,%d,%d,%d,%d,%d)",&ip[0], &ip[1], &ip[2], &ip[3], &ip[4], &ip[5]) != 6 &&
                       sscanf(begin, "=%d,%d,%d,%d,%d,%d",&ip[0], &ip[1], &ip[2], &ip[3], &ip[4], &ip[5]) != 6)) {
          // Unable to parse, try the next thing
          socket()->setConfig("feat.pasv", "0");
          negotiateDataConnection();
          return;
        }
      
        // Convert to string
        QString host;
        int port;
      
        host.sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
        port = ip[4] << 8 | ip[5];
        
        // If the reported IP address is from a private IP range, this might be because the
        // remote server is not properly configured. So we just use the server's real IP instead
        // of the one we got (if the host is really local, then this should work as well).
        if (!socket()->getConfigInt("feat.pret")) {
          if (host.startsWith("192.168.") || host.startsWith("10.") || host.startsWith("172.16."))
            host = socket()->peerAddress().nodeName();
        }
        
        // We have the address, let's setup the transfer socket and then
        // we are done.
        currentState = HaveConnection;
        socket()->setupPassiveTransferSocket(host, port);
      } else {
        // Just send the PASV command
        currentState = NegotiatePasv;
        socket()->sendCommand("PASV");
      }
    }
    
    void negotiateActive()
    {
      if (currentState == NegotiateActive) {
        if (!socket()->isResponse("2")) {
          if (socket()->getConfigInt("feat.eprt")) {
            socket()->setConfig("feat.eprt", 0);
          } else {
            // Negotiation failed, reset since active is the last fallback
            socket()->resetCommandClass(Failed);
            return;
          }
        } else {
          currentState = HaveConnection;
          socket()->nextCommandAsync();
          return;
        }
      }
      
      // Setup the socket and set the apropriate port command
      currentState = NegotiateActive;
      
      KNetwork::KSocketAddress address = socket()->setupActiveTransferSocket();
      if (address.address()) {
        if (socket()->getConfigInt("feat.eprt")) {
          QString ianaFamily = QString::number(address.ianaFamily());
          
          socket()->sendCommand("EPRT |" + ianaFamily + "|" + address.nodeName() + "|" + address.serviceName() + "|"); 
        } else if (address.ianaFamily() == 1) {
          QString format = address.nodeName().replace(".", ",");
          
          format.append(",");
          format.append(QString::number((unsigned char) address.address()->sa_data[0]));
          format.append(",");
          format.append(QString::number((unsigned char) address.address()->sa_data[1]));
          
          socket()->sendCommand("PORT " + format);
        } else {
          socket()->emitEvent(Event::EventMessage, i18n("Incompatible address family for PORT, but EPRT not supported, aborting!"));
          socket()->resetCommandClass(Failed);
        }
      }
    }
};

void FtpSocket::initializeTransferSocket()
{
  m_transferConnecting = true;
  m_transferEnd = 0;
  m_transferBytes = 0;
  m_transferBufferSize = 4096;
  m_transferBuffer = (char*) malloc(m_transferBufferSize);
  
  m_speedLastTime = time(0);
  m_speedLastBytes = 0;
  
  // Setup the speed limiter
  switch (getPreviousCommand()) {
    case Commands::CmdGet: SpeedLimiter::self()->append(this, SpeedLimiter::Download); break;
    case Commands::CmdPut: SpeedLimiter::self()->append(this, SpeedLimiter::Upload); break;
    default: break;
  }
  
  m_transferSocket->enableRead(false);
  m_transferSocket->setBlocking(false);
  m_transferSocket->setAddressReuseable(true);
}

void FtpSocket::setupPassiveTransferSocket(const QString &host, int port)
{
  // Use the host from control connection if empty
  QString realHost = host;
  if (host.isEmpty() || getConfigInt("pasv.use_site_ip"))
    realHost = peerAddress().nodeName();
    
  // Let's connect
  emitEvent(Event::EventMessage, i18n("Establishing data connection with %1:%2...").arg(realHost).arg(port));
    
  if (!m_transferSocket)
    m_transferSocket = new KNetwork::KStreamSocket();
    
  initializeTransferSocket();
  m_transferSocket->connect(realHost, QString::number(port));
}

KNetwork::KSocketAddress FtpSocket::setupActiveTransferSocket()
{
  if (!m_serverSocket)
    m_serverSocket = new KNetwork::KServerSocket();
    
  m_serverSocket->setAcceptBuffered(false);
  m_serverSocket->setFamily(KNetwork::KResolver::InetFamily);
    
  if (KFTPCore::Config::activeForcePort()) {
    // Bind only to ports in a specified portrange
    bool found = false;
    unsigned int max = KFTPCore::Config::activeMaxPort();
    unsigned int min = KFTPCore::Config::activeMinPort();

    for (unsigned int port = min + rand() % (max - min + 1); port <= max; port++) {
      m_serverSocket->setAddress(QString::number(port));
      bool success = m_serverSocket->listen();
      
      if (found = (success && m_serverSocket->error() == KSocketBase::NoError))
        break;
        
      m_serverSocket->close();
    }
    
    if (!found) {
      emitEvent(Event::EventMessage, i18n("Unable to establish a listening socket."));
      resetCommandClass(Failed);
      return KNetwork::KSocketAddress();
    }
  } else {
    m_serverSocket->setAddress("0");
    
    if (!m_serverSocket->listen()) {
      emitEvent(Event::EventMessage, i18n("Unable to establish a listening socket."));
      resetCommandClass(Failed);
      return KNetwork::KSocketAddress();
    }
  }
  
  KNetwork::KSocketAddress serverAddr = m_serverSocket->localAddress();
  KNetwork::KSocketAddress controlAddr = localAddress();
  KNetwork::KSocketAddress request;
  
  if (KFTPCore::Config::portForceIp() && !getConfigInt("active.no_force_ip")) {
    QString remoteIp = peerAddress().nodeName();
    
    if (KFTPCore::Config::ignoreExternalIpForLan() &&
        (remoteIp.startsWith("192.168.") || remoteIp.startsWith("10.") || remoteIp.startsWith("172.16."))) {
      request = controlAddr;
    } else {
      // Force a specified IP/hostname to be used in PORT
      KNetwork::KResolverResults resolverResults;
  
      resolverResults = KNetwork::KResolver::resolve(KFTPCore::Config::portIp(), "21");
      if (resolverResults.error() < 0) {
        // Well, we are unable to resolve the name, so we should use what we got
        // from control socket
        request = controlAddr;
      } else {
        // The name has been resolved and we have the address, so we should
        // use it
        request = resolverResults[0].address();
      }
    }
  } else {
    // Just use our IP we bound to when connecting to the remote server
    request = controlAddr;
  }
  
  // Set the proper port
  request.address()->sa_data[0] = serverAddr.address()->sa_data[0];
  request.address()->sa_data[1] = serverAddr.address()->sa_data[1];
  
  emitEvent(Event::EventMessage, i18n("Waiting for data connection on port %1...").arg(serverAddr.serviceName()));
  
  return request;
}

void FtpSocket::slotDataAccept(KNetwork::KStreamSocket *socket)
{
  m_transferSocket = socket;
  initializeTransferSocket();
  
  // Socket has been accepted so the server is not needed anymore
  delete m_serverSocket;
  
  emitEvent(Event::EventMessage, i18n("Data connection established."));
  checkTransferStart();
}

void FtpSocket::closeDataTransferSocket()
{
  if (m_dataSsl) {
    m_dataSsl->close();
    delete m_dataSsl;
    m_dataSsl = 0;
  }
  
  // Free the buffer and invalidate the socket
  free(m_transferBuffer);
  
  m_transferSocket->close();
  delete m_transferSocket;
  m_transferBytes = 0;
  
  SpeedLimiter::self()->remove(this);
}

void FtpSocket::transferCompleted()
{
  // Transfer has been completed, cleanup
  closeDataTransferSocket();
  checkTransferEnd();
}

void FtpSocket::checkTransferStart()
{
  if (++m_transferStart >= 2) {
    // Setup SSL data connection
    if (getConfigInt("ssl") && (getConfigInt("ssl.prot_mode") == 0 ||
      (getConfigInt("ssl.prot_mode") == 1 && getToplevelCommand() == Commands::CmdList)) && !m_dataSsl) {
      m_dataSsl = new Ssl(m_transferSocket);
      
      if (m_dataSsl->connect()) {
        emitEvent(Event::EventMessage, i18n("Data channel secured with %1 bit SSL.").arg(m_dataSsl->connectionInfo().getCipherUsedBits()));
      } else {
        emitEvent(Event::EventMessage, i18n("SSL negotiation for the data channel has failed. Aborting transfer."));
        resetCommandClass(Failed);
        return;
      }
    }
  }
}

void FtpSocket::checkTransferEnd()
{
  if (++m_transferEnd >= 2) {
    emitEvent(Event::EventMessage, i18n("Transfer completed."));
    resetCommandClass();
  }
}

void FtpSocket::slotDataConnected()
{
  emitEvent(Event::EventMessage, i18n("Data connection established."));
  
  checkTransferStart();
  nextCommand();
}

void FtpSocket::variableBufferUpdate(Q_LONG size)
{
  if (size > m_transferBufferSize - 64) {
    if (m_transferBufferSize + 512 <= 32768) {
      m_transferBufferSize += 512;
      m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize);
    }
  } else if (size < m_transferBufferSize - 65) {
    if (m_transferBufferSize - 512 >= 4096) {
      m_transferBufferSize -= 512;
      m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize);
    }
  }
}

void FtpSocket::slotDataTryWrite()
{
  bool updateVariableBuffer = true;
  
  // Enforce speed limits
  if (allowedBytes() > -1) {
    m_transferBufferSize = allowedBytes();
    
    if (m_transferBufferSize > 32768)
      m_transferBufferSize = 32768;
    else if (m_transferBufferSize == 0)
      return;
      
    m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize);
    updateVariableBuffer = false;
  } else if (m_transferBufferSize == 0) {
    m_transferBufferSize = 4096;
    m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize);
  }
  
  if (!getTransferFile()->isOpen())
    return;
  
  // If there is nothing to upload, just close the connection right away
  if (getTransferFile()->size() == 0) {
    transferCompleted();
    return;
  }
  
  QFile::Offset tmpOffset = getTransferFile()->at();
  Q_LONG readSize = getTransferFile()->readBlock(m_transferBuffer, m_transferBufferSize);
  
  Q_LONG size = 0;
  
  if (m_dataSsl)
    size = m_dataSsl->write(m_transferBuffer, readSize);
  else
    size = m_transferSocket->writeBlock(m_transferBuffer, readSize);
  
  if (size < 0) {
    getTransferFile()->at(tmpOffset);
    return;
  } else if (size < readSize)
    getTransferFile()->at(tmpOffset + size);
    
  m_transferBytes += size;
  updateUsage(size);
  timeoutPing();
  
  if (getTransferFile()->atEnd()) {
    // We have reached the end of file, so we should terminate the connection
    transferCompleted();
    return;
  }
  
  if (updateVariableBuffer)
    variableBufferUpdate(size);
}

void FtpSocket::slotDataTryRead()
{
  bool updateVariableBuffer = true;
  
  // Enforce speed limits
  if (allowedBytes() > -1) {
    m_transferBufferSize = allowedBytes();
    
    if (m_transferBufferSize > 32768)
      m_transferBufferSize = 32768;
    else if (m_transferBufferSize == 0)
      return;
    
    m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize);
    updateVariableBuffer = false;
  } else if (m_transferBufferSize == 0) {
    m_transferBufferSize = 4096;
    m_transferBuffer = (char*) realloc(m_transferBuffer, m_transferBufferSize);
  }
  
  Q_LONG size = 0;
  
  if (m_dataSsl) {
    size = m_dataSsl->read(m_transferBuffer, m_transferBufferSize);
    
    if (size == -1) {
      transferCompleted();
      return;
    }
  } else {
    size = m_transferSocket->readBlock(m_transferBuffer, m_transferBufferSize);
  
    // Check if the connection has been closed
    if (m_transferSocket->error() != NoError) {
      if (m_transferSocket->error() != WouldBlock) {
        transferCompleted();
        return;
      }
    }
  }
  
  if (size <= 0) {
    if (!m_dataSsl)
      transferCompleted();
    
    return;
  }
  
  updateUsage(size);
  timeoutPing();

  switch (getPreviousCommand()) {
    case Commands::CmdList: {
      // Feed the data to the directory listing parser
      if (m_directoryParser)
        m_directoryParser->addData(m_transferBuffer, size);
      break;
    }
    case Commands::CmdGet: {
      // Write to file
      getTransferFile()->writeBlock(m_transferBuffer, size);
      m_transferBytes += size;
      break;
    }
    default: {
      qDebug("WARNING: slotDataReadActivity called for an invalid command!");
      return;
    }
  }
  
  if (updateVariableBuffer)
    variableBufferUpdate(size);
}

// *******************************************************************************************
// ******************************************* LIST ******************************************
// *******************************************************************************************

class FtpCommandList : public Commands::Base {
public:
    enum State {
      None,
      SentCwd,
      SentStat,
      WaitList
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandList, FtpSocket, CmdList)
    
    QString path;
    
    void process()
    {
      switch (currentState) {
        case None: {
          path = socket()->getConfig("params.list.path");
          
          if (socket()->isChained())
            socket()->m_lastDirectoryListing = DirectoryListing();
          
          // Change working directory
          currentState = SentCwd;
          socket()->changeWorkingDirectory(path);
          break;
        }
        case SentCwd: {
          // Check the directory listing cache
          DirectoryListing cached = Cache::self()->findCached(socket(), socket()->getCurrentDirectory());
          if (cached.isValid()) {
            socket()->emitEvent(Event::EventMessage, i18n("Using cached directory listing."));
            
            if (socket()->isChained()) {
              // We don't emit an event, because this list has been called from another
              // command. Just save the listing.
              socket()->m_lastDirectoryListing = cached;
            } else
              socket()->emitEvent(Event::EventDirectoryListing, cached);
              
            socket()->resetCommandClass();
            return;
          }
          
          socket()->m_directoryParser = new FtpDirectoryParser(socket());
          
          // Support for faster stat directory listings over the control connection
          if (socket()->getConfigInt("stat_listings")) {
            currentState = SentStat;
            socket()->sendCommand("STAT .");
            return;
          }
          
          // First we have to initialize the data connection, another class will
          // do this for us, so we just add it to the command chain
          socket()->setConfig("params.data_rest_do", 0);
          socket()->setConfig("params.data_type", 'A');
          
          if (socket()->getConfigInt("feat.mlsd"))
            socket()->setConfig("params.data_command", "MLSD");
          else
            socket()->setConfig("params.data_command", "LIST -a");
          
          currentState = WaitList;
          chainCommandClass(FtpCommandNegotiateData);
          break;
        }
        case SentStat: {
          if (!socket()->isResponse("2")) {
            // The server doesn't support STAT, disable it and fallback
            socket()->setConfig("stat_listings", 0);
            
            socket()->setConfig("params.data_rest_do", 0);
            socket()->setConfig("params.data_type", 'A');
            
            if (socket()->getConfigInt("feat.mlsd"))
              socket()->setConfig("params.data_command", "MLSD");
            else
              socket()->setConfig("params.data_command", "LIST -a");
            
            currentState = WaitList;
            chainCommandClass(FtpCommandNegotiateData);
            return;
          } else if (socket()->isMultiline()) {
            // Some servers put the response code into the multiline reply
            QString response = socket()->getResponse();
            if (response.left(3) == "211")
              response = response.mid(4);
              
            socket()->m_directoryParser->addDataLine(response);
            return;
          }
          
          // If we are done, just go on and emit the listing
        }
        case WaitList: {
          // List has been received
          if (socket()->isChained()) {
            // We don't emit an event, because this list has been called from another
            // command. Just save the listing.
            socket()->m_lastDirectoryListing = socket()->m_directoryParser->getListing();
          } else
            socket()->emitEvent(Event::EventDirectoryListing, socket()->m_directoryParser->getListing());
          
          // Cache the directory listing
          Cache::self()->addDirectory(socket(), socket()->m_directoryParser->getListing());
          
          delete socket()->m_directoryParser;
          socket()->m_directoryParser = 0;

          socket()->resetCommandClass();
          break;
        }
      }
    }
};

void FtpSocket::protoList(const KURL &path)
{
  emitEvent(Event::EventState, i18n("Fetching directory listing..."));
  emitEvent(Event::EventMessage, i18n("Fetching directory listing..."));
  
  // Set the directory that should be listed
  setConfig("params.list.path", path.path());
  
  activateCommandClass(FtpCommandList);
}

// *******************************************************************************************
// ******************************************* GET *******************************************
// *******************************************************************************************

class FtpCommandGet : public Commands::Base {
public:
    enum State {
      None,
      SentCwd,
      SentMdtm,
      StatDone,
      DestChecked,
      WaitTransfer
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandGet, FtpSocket, CmdGet)
    
    KURL sourceFile;
    KURL destinationFile;
    time_t modificationTime;
    
    void process()
    {
      switch (currentState) {
        case None: {
          modificationTime = 0;
          sourceFile.setPath(socket()->getConfig("params.get.source"));
          destinationFile.setPath(socket()->getConfig("params.get.destination"));
          
          // Attempt to CWD to the parent directory
          currentState = SentCwd;
          socket()->changeWorkingDirectory(sourceFile.directory());
          break;
        }
        case SentCwd: {
          // Send MDTM
          if (socket()->getConfigInt("feat.mdtm")) {
            currentState = SentMdtm;
            socket()->sendCommand("MDTM " + sourceFile.path());
            break;
          } else {
            // Don't break so we will get on to checking for file existance
          }
        }
        case SentMdtm: {
          if (currentState == SentMdtm) {
            if (socket()->isResponse("550")) {
              // The file probably doesn't exist, just ignore it
            } else if (!socket()->isResponse("213")) {
              socket()->setConfig("feat.mdtm", 0);
            } else {
              // Parse MDTM response
              struct tm dt = {0,0,0,0,0,0,0,0,0,0,0};
              QString tmp(socket()->getResponse());
        
              tmp.remove(0, 4);
              dt.tm_year = tmp.left(4).toInt() - 1900;
              dt.tm_mon = tmp.mid(4, 2).toInt() - 1;
              dt.tm_mday = tmp.mid(6, 2).toInt();
              dt.tm_hour = tmp.mid(8, 2).toInt();
              dt.tm_min = tmp.mid(10, 2).toInt();
              dt.tm_sec = tmp.mid(12, 2).toInt();
              modificationTime = mktime(&dt);
            }
          }

          // Check if the local file exists and stat the remote file if so
          if (QDir::root().exists(destinationFile.path())) {
            socket()->protoStat(sourceFile);
            currentState = StatDone;
            return;
          } else {
            KStandardDirs::makeDir(destinationFile.directory());
            
            // Don't break so we will get on to initiating the data connection
          }
        }
        case StatDone: {
          if (currentState == StatDone) {
            DirectoryListing list;
            list.addEntry(socket()->getStatResponse());
            
            currentState = DestChecked;
            socket()->emitEvent(Event::EventFileExists, list);
            return;
          }
        }
        case DestChecked: {
          socket()->setConfig("params.data_rest_do", 0);
          
          if (isWakeup()) {
            // We have been waken up because a decision has been made
            FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent);
            
            if (!socket()->getConfigInt("feat.rest") && event->action == FileExistsWakeupEvent::Resume)
              event->action = FileExistsWakeupEvent::Overwrite;
            
            switch (event->action) {
              case FileExistsWakeupEvent::Rename: {
                // Change the destination filename, otherwise it is the same as overwrite
                destinationFile.setPath(event->newFileName);
              }
              case FileExistsWakeupEvent::Overwrite: {
                socket()->getTransferFile()->setName(destinationFile.path());
                socket()->getTransferFile()->open(IO_WriteOnly | IO_Truncate);
                
                if (socket()->getConfigInt("feat.rest")) {
                  socket()->setConfig("params.data_rest_do", 1);
                  socket()->setConfig("params.data_rest", 0);
                }
                break;
              }
              case FileExistsWakeupEvent::Resume: {
                socket()->getTransferFile()->setName(destinationFile.path());
                socket()->getTransferFile()->open(IO_WriteOnly | IO_Append);
                
                // Signal resume
                socket()->emitEvent(Event::EventResumeOffset, socket()->getTransferFile()->size());
                
                socket()->setConfig("params.data_rest_do", 1);
                socket()->setConfig("params.data_rest", (filesize_t) socket()->getTransferFile()->size());
                break;
              }
              case FileExistsWakeupEvent::Skip: {
                // Transfer should be aborted
                socket()->emitEvent(Event::EventTransferComplete);
                socket()->resetCommandClass();
                return;
              }
            }
          } else {
            // The file doesn't exist so we are free to overwrite
            socket()->getTransferFile()->setName(destinationFile.path());
            socket()->getTransferFile()->open(IO_WriteOnly | IO_Truncate);
          }
          
          // Check if there was a problem opening the file
          if (!socket()->getTransferFile()->isOpen()) {
            socket()->emitError(FileOpenFailed);
            socket()->resetCommandClass(Failed);
            return;
          }
          
          // First we have to initialize the data connection, another class will
          // do this for us, so we just add it to the command chain
          socket()->setConfig("params.data_type", KFTPCore::Config::self()->ftpMode(sourceFile.path()));
          socket()->setConfig("params.data_command", "RETR " + sourceFile.filename());
          
          currentState = WaitTransfer;
          chainCommandClass(FtpCommandNegotiateData);
          break;
        }
        case WaitTransfer: {
          // Transfer has been completed
          socket()->getTransferFile()->close();
          
          if (modificationTime != 0) {
            // Use the modification time we got from MDTM
            utimbuf tmp;
            tmp.actime = time(0);
            tmp.modtime = modificationTime;
            utime(destinationFile.path().latin1(), &tmp);
          }
          
          socket()->emitEvent(Event::EventTransferComplete);
          socket()->emitEvent(Event::EventReloadNeeded);
          socket()->resetCommandClass();
          break;
        }
      }
    }
};

void FtpSocket::protoGet(const KURL &source, const KURL &destination)
{
  emitEvent(Event::EventState, i18n("Transfering..."));
  emitEvent(Event::EventMessage, i18n("Downloading file '%1'...").arg(source.fileName()));
  
  // Set the source and destination
  setConfig("params.get.source", source.path());
  setConfig("params.get.destination", destination.path());
  
  activateCommandClass(FtpCommandGet);
}

// *******************************************************************************************
// ******************************************* CWD *******************************************
// *******************************************************************************************

class FtpCommandCwd : public Commands::Base {
public:
    enum State {
      None,
      SentCwd,
      SentPwd,
      SentMkd,
      SentCwdEnd
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandCwd, FtpSocket, CmdNone)
    
    QString targetDirectory;
    QString currentPathPart;
    QString cached;
    int currentPart;
    int numParts;
    bool shouldCreate;
    
    void process()
    {
      switch (currentState) {
        case None: {
          targetDirectory = socket()->getConfig("params.cwd.path");
          
          // If we are already there, no need to CWD
          if (socket()->getCurrentDirectory() == targetDirectory) {
            socket()->resetCommandClass();
            return;
          }
          
          cached = Cache::self()->findCachedPath(socket(), targetDirectory);
          if (!cached.isEmpty()) {
            if (socket()->getCurrentDirectory() == cached) {
              // We are already there
              socket()->resetCommandClass();
              return;
            }
          }
          
          // First check the toplevel directory and if it exists we are done
          currentState = SentCwd;
          currentPart = 0;
          numParts = targetDirectory.contains('/');
          shouldCreate = socket()->getConfigInt("params.cwd.create");
          
          socket()->sendCommand("CWD " + targetDirectory);
          break;
        }
        case SentCwd: {
          if (socket()->isMultiline())
            return;
          
          if (socket()->isResponse("250") && currentPart == 0) {
            if (!cached.isEmpty()) {
              socket()->setCurrentDirectory(cached);
              socket()->resetCommandClass();
            } else {
              // Directory exists, check where we are
              currentState = SentPwd;
              socket()->sendCommand("PWD");
            }
          } else {
            // Changing the working directory has failed
            if (shouldCreate) {
              currentPathPart = targetDirectory.section('/', 0, ++currentPart);
              currentState = SentMkd;
              socket()->sendCommand("MKD " + currentPathPart);
            } else if (socket()->errorReporting()) {
              socket()->emitError(socket()->getPreviousCommand() == Commands::CmdList ? ListFailed : FileNotFound);
              socket()->resetCommandClass(Failed);
            } else {
              socket()->resetCommandClass();
            }
          }
          break;
        }
        case SentPwd: {
          // Parse the current working directory
          if (socket()->isResponse("2")) {
            QString tmp = socket()->getResponse();
            int first = tmp.find('"') + 1;
            tmp = tmp.mid(first, tmp.findRev('"') - first);
            
            // Set the current directory and cache it
            socket()->setCurrentDirectory(tmp);
            Cache::self()->addPath(socket(), tmp);
            
            socket()->resetCommandClass();
          } else if (socket()->errorReporting()) {
            socket()->emitError(socket()->getPreviousCommand() == Commands::CmdList ? ListFailed : FileNotFound);
            socket()->resetCommandClass(Failed);
          } else {
            socket()->resetCommandClass();
          }
          break;
        }
        case SentMkd: {
          // Invalidate parent cache
          if (socket()->isResponse("2")) {
            Cache::self()->invalidateEntry(socket(), KURL(currentPathPart).directory());
          }
            
          if (currentPart == numParts) {
            // We are done, since all directories have been created
            currentState = SentCwdEnd;
            socket()->sendCommand("CWD " + targetDirectory);
          } else {
            currentPathPart = targetDirectory.section('/', 0, ++currentPart);
            currentState = SentMkd;
            socket()->sendCommand("MKD " + currentPathPart);
          }
          break;
        }
        case SentCwdEnd: {
          if (socket()->isMultiline())
            return;
          
          // See where we are and set current working directory
          currentState = SentPwd;
          socket()->sendCommand("PWD");
          break;
        }
      }
    }
};

void FtpSocket::changeWorkingDirectory(const QString &path, bool shouldCreate)
{
  // Set the path to cwd to
  setConfig("params.cwd.path", path);
  setConfig("params.cwd.create", shouldCreate);
  
  activateCommandClass(FtpCommandCwd);
}

// *******************************************************************************************
// ******************************************* PUT *******************************************
// *******************************************************************************************

class FtpCommandPut : public Commands::Base {
public:
    enum State {
      None,
      WaitCwd,
      SentSize,
      StatDone,
      DestChecked,
      WaitTransfer
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandPut, FtpSocket, CmdPut)
    
    KURL sourceFile;
    KURL destinationFile;
    
    bool fetchedSize;
    filesize_t destinationSize;
    
    void cleanup()
    {
      // Unclean upload termination, be sure to erase the cached stat infos
      Cache::self()->invalidateEntry(socket(), destinationFile.directory());
    }
    
    void process()
    {
      switch (currentState) {
        case None: {
          sourceFile.setPath(socket()->getConfig("params.get.source"));
          destinationFile.setPath(socket()->getConfig("params.get.destination"));
          fetchedSize = false;
          
          // Check if the local file exists
          if (!QDir::root().exists(sourceFile.path())) {
            socket()->emitError(FileNotFound);
            socket()->resetCommandClass(Failed);
            return;
          }
          
          // Change to the current working directory, creating any directories that are
          // still missing
          currentState = WaitCwd;
          socket()->changeWorkingDirectory(destinationFile.directory(), true);
          break;
        }
        case WaitCwd: {
          // Check if the remote file exists
          if (socket()->getConfigInt("feat.size")) {
            currentState = SentSize;
            socket()->sendCommand("SIZE " + destinationFile.path());
          } else {
            // SIZE is not available, try stat directly
            currentState = StatDone;
            socket()->protoStat(destinationFile);
          }
          break;
        }
        case SentSize: {
          if (socket()->isResponse("213")) {
            destinationSize = socket()->getResponse().mid(4).toULongLong();
            fetchedSize = true;
            
            // File exists, we have to stat to get more data
            currentState = StatDone;
            socket()->protoStat(destinationFile);
          } else if (socket()->isResponse("500") || socket()->getResponse().contains("Operation not permitted", false)) {
            // Yes, some servers don't support the SIZE command :/
            socket()->setConfig("feat.size", 0);
            
            currentState = StatDone;
            socket()->protoStat(destinationFile);
          } else {
            currentState = DestChecked;
            process();
          }
          break;
        }
        case StatDone: {
          if (!socket()->getStatResponse().filename().isEmpty()) {
            if (fetchedSize) {
              if (socket()->getStatResponse().size() != destinationSize) {
                // It would seem that the size has changed, cached data is invalid
                Cache::self()->invalidateEntry(socket(), destinationFile.directory());
                
                currentState = StatDone;
                socket()->protoStat(destinationFile);
                return;
              }
            }
            
            // Remote file exists, emit a request for action
            DirectoryListing list;
            list.addEntry(socket()->getStatResponse());
            
            currentState = DestChecked;
            socket()->emitEvent(Event::EventFileExists, list);
            return;
          }
          
          // Don't break here
        }
        case DestChecked: {
          socket()->setConfig("params.data_rest_do", 0);
          
          if (isWakeup()) {
            // We have been waken up because a decision has been made
            FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent);
            
            if (!socket()->getConfigInt("feat.rest") && event->action == FileExistsWakeupEvent::Resume)
              event->action = FileExistsWakeupEvent::Overwrite;
            
            switch (event->action) {
              case FileExistsWakeupEvent::Rename: {
                // Change the destination filename, otherwise it is the same as overwrite
                destinationFile.setPath(event->newFileName);
              }
              case FileExistsWakeupEvent::Overwrite: {
                socket()->getTransferFile()->setName(sourceFile.path());
                socket()->getTransferFile()->open(IO_ReadOnly);
                
                if (socket()->getConfigInt("feat.rest")) {
                  socket()->setConfig("params.data_rest_do", 1);
                  socket()->setConfig("params.data_rest", 0);
                }
                break;
              }
              case FileExistsWakeupEvent::Resume: {
                socket()->getTransferFile()->setName(sourceFile.path());
                socket()->getTransferFile()->open(IO_ReadOnly);
                socket()->getTransferFile()->at(socket()->getStatResponse().size());
                
                // Signal resume
                socket()->emitEvent(Event::EventResumeOffset, socket()->getStatResponse().size());
                
                socket()->setConfig("params.data_rest_do", 1);
                socket()->setConfig("params.data_rest", (filesize_t) socket()->getStatResponse().size());
                break;
              }
              case FileExistsWakeupEvent::Skip: {
                // Transfer should be aborted
                markClean();
                
                socket()->resetCommandClass(UserAbort);
                socket()->emitEvent(Event::EventTransferComplete);
                return;
              }
            }
          } else {
            // The file doesn't exist so we are free to overwrite
            socket()->getTransferFile()->setName(sourceFile.path());
            socket()->getTransferFile()->open(IO_ReadOnly);
          }
          
          // Check if there was a problem opening the file
          if (!socket()->getTransferFile()->isOpen()) {
            socket()->emitError(FileOpenFailed);
            socket()->resetCommandClass(Failed);
            return;
          }
          
          // First we have to initialize the data connection, another class will
          // do this for us, so we just add it to the command chain
          socket()->setConfig("params.data_type", KFTPCore::Config::self()->ftpMode(destinationFile.path()));
          socket()->setConfig("params.data_command", "STOR " + destinationFile.filename());
          
          currentState = WaitTransfer;
          chainCommandClass(FtpCommandNegotiateData);
          break;
        }
        case WaitTransfer: {
          // Transfer has been completed
          Cache::self()->updateDirectoryEntry(socket(), destinationFile, socket()->getTransferFile()->size());
          socket()->getTransferFile()->close();
          markClean();
          
          socket()->emitEvent(Event::EventTransferComplete);
          socket()->emitEvent(Event::EventReloadNeeded);
          socket()->resetCommandClass();
          break;
        }
      }
    }
};

void FtpSocket::protoPut(const KURL &source, const KURL &destination)
{
  emitEvent(Event::EventState, i18n("Transfering..."));
  emitEvent(Event::EventMessage, i18n("Uploading file '%1'...").arg(source.fileName()));
  
  // Set the source and destination
  setConfig("params.get.source", source.path());
  setConfig("params.get.destination", destination.path());
  
  activateCommandClass(FtpCommandPut);
}

// *******************************************************************************************
// **************************************** REMOVE *******************************************
// *******************************************************************************************

class FtpCommandRemove : public Commands::Base {
public:
    enum State {
      None,
      SentCwd,
      SentRemove
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRemove, FtpSocket, CmdNone)
    
    QString destinationPath;
    QString parentDirectory;
    
    void process()
    {
      switch (currentState) {
        case None: {
          destinationPath = socket()->getConfig("params.remove.path");
          parentDirectory = socket()->getConfig("params.remove.parent");
          
          currentState = SentRemove;
          
          if (socket()->getConfigInt("params.remove.directory")) {
            if (socket()->getCurrentDirectory() != parentDirectory) {
              // We should change working directory to parent directory before removing
              currentState = SentCwd;
              socket()->sendCommand("CWD " + parentDirectory);
            } else {
              socket()->sendCommand("RMD " + destinationPath);
            }
          } else {
            socket()->sendCommand("DELE " + destinationPath);
          }
          break;
        }
        case SentCwd: {
          if (socket()->isMultiline())
            return;
          
          if (socket()->isResponse("2")) {
            // CWD was successful
            socket()->setCurrentDirectory(parentDirectory);
          }
          
          currentState = SentRemove;
          socket()->sendCommand("RMD " + destinationPath);
          break;
        }
        case SentRemove: {
          if (socket()->isMultiline())
            return;
          
          if (!socket()->isResponse("2")) {
            socket()->resetCommandClass(Failed);
          } else {
            // Invalidate cached parent entry (if any)
            Cache::self()->invalidateEntry(socket(), parentDirectory);
            Cache::self()->invalidatePath(socket(), destinationPath);
            
            if (!socket()->isChained())
              socket()->emitEvent(Event::EventReloadNeeded);
            socket()->resetCommandClass();
          }
          break;
        }
      }
    }
};

void FtpSocket::protoRemove(const KURL &path)
{
  emitEvent(Event::EventState, i18n("Removing..."));
  
  // Set the file to remove
  setConfig("params.remove.parent", path.directory());
  setConfig("params.remove.path", path.path());
  
  activateCommandClass(FtpCommandRemove);
}

// *******************************************************************************************
// **************************************** RENAME *******************************************
// *******************************************************************************************

class FtpCommandRename : public Commands::Base {
public:
    enum State {
      None,
      SentRnfr,
      SentRnto
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRename, FtpSocket, CmdRename)
    
    QString sourcePath;
    QString destinationPath;
    
    void process()
    {
      switch (currentState) {
        case None: {
          sourcePath = socket()->getConfig("params.rename.source");
          destinationPath = socket()->getConfig("params.rename.destination");
          
          currentState = SentRnfr;
          socket()->sendCommand("RNFR " + sourcePath);
          break;
        }
        case SentRnfr: {
          if (socket()->isResponse("3")) {
            currentState = SentRnto;
            socket()->sendCommand("RNTO " + destinationPath);
          } else
            socket()->resetCommandClass(Failed);
          break;
        }
        case SentRnto: {
          if (socket()->isResponse("2")) {
            // Invalidate cached parent entry (if any)
            Cache::self()->invalidateEntry(socket(), KURL(sourcePath).directory());
            Cache::self()->invalidateEntry(socket(), KURL(destinationPath).directory());
            
            Cache::self()->invalidatePath(socket(), sourcePath);
            Cache::self()->invalidatePath(socket(), destinationPath);
            
            socket()->emitEvent(Event::EventReloadNeeded);
            socket()->resetCommandClass();
          } else
            socket()->resetCommandClass(Failed);
          break;
        }
      }
    }
};

void FtpSocket::protoRename(const KURL &source, const KURL &destination)
{
  emitEvent(Event::EventState, i18n("Renaming..."));
  
  // Set rename options
  setConfig("params.rename.source", source.path());
  setConfig("params.rename.destination", destination.path());
  
  activateCommandClass(FtpCommandRename);
}

// *******************************************************************************************
// **************************************** CHMOD ********************************************
// *******************************************************************************************

class FtpCommandChmod : public Commands::Base {
public:
    enum State {
      None,
      SentChmod
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandChmod, FtpSocket, CmdChmod)
    
    void process()
    {
      switch (currentState) {
        case None: {
          currentState = SentChmod;
          
          QString chmod;
          chmod.sprintf("SITE CHMOD %.3d %s", socket()->getConfigInt("params.chmod.mode"),
                                              socket()->getConfig("params.chmod.path").ascii());
          socket()->sendCommand(chmod);
          break;
        }
        case SentChmod: {
          if (!socket()->isResponse("2"))
            socket()->resetCommandClass(Failed);
          else {
            // Invalidate cached parent entry (if any)
            Cache::self()->invalidateEntry(socket(), KURL(socket()->getConfig("params.chmod.path")).directory());
            
            socket()->emitEvent(Event::EventReloadNeeded);
            socket()->resetCommandClass();
          }
          break;
        }
      }
    }
};

void FtpSocket::protoChmodSingle(const KURL &path, int mode)
{
  emitEvent(Event::EventState, i18n("Changing mode..."));
  
  // Set chmod options
  setConfig("params.chmod.path", path.path());
  setConfig("params.chmod.mode", mode);
  
  activateCommandClass(FtpCommandChmod);
}

// *******************************************************************************************
// **************************************** MKDIR ********************************************
// *******************************************************************************************

class FtpCommandMkdir : public Commands::Base {
public:
    enum State {
      None,
      SentMkdir
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandMkdir, FtpSocket, CmdMkdir)
    
    void process()
    {
      switch (currentState) {
        case None: {
          currentState = SentMkdir;
          socket()->changeWorkingDirectory(socket()->getConfig("params.mkdir.path"), true);
          break;
        }
        case SentMkdir: {
          // Invalidate cached parent entry (if any)
          Cache::self()->invalidateEntry(socket(), KURL(socket()->getCurrentDirectory()).directory());
          
          socket()->emitEvent(Event::EventReloadNeeded);
          socket()->resetCommandClass();
          break;
        }
      }
    }
};

void FtpSocket::protoMkdir(const KURL &path)
{
  emitEvent(Event::EventState, i18n("Making directory..."));
  
  setConfig("params.mkdir.path", path.path());
  activateCommandClass(FtpCommandMkdir);
}

// *******************************************************************************************
// ******************************************* RAW *******************************************
// *******************************************************************************************

class FtpCommandRaw : public Commands::Base {
public:
    enum State {
      None,
      SentRaw
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandRaw, FtpSocket, CmdRaw)
    
    QString response;
    
    void process()
    {
      switch (currentState) {
        case None: {
          currentState = SentRaw;
          socket()->sendCommand(socket()->getConfig("params.raw.command"));
          break;
        }
        case SentRaw: {
          response.append(socket()->getResponse());
          
          if (!socket()->isMultiline()) {
            socket()->emitEvent(Event::EventRaw, response);
            socket()->resetCommandClass();
          }
          break;
        }
      }
    }
};

void FtpSocket::protoRaw(const QString &raw)
{
  setConfig("params.raw.command", raw);
  activateCommandClass(FtpCommandRaw);
}

// *******************************************************************************************
// ******************************************* FXP *******************************************
// *******************************************************************************************

class FtpCommandFxp : public Commands::Base {
public:
    enum State {
      None,
      
      // Source socket
      SourceSentCwd,
      SourceSentStat,
      SourceDestVerified,
      SourceSentType,
      SourceSentSscn,
      SourceSentProt,
      SourceWaitType,
      SourceSentPret,
      SourceSentPasv,
      SourceDoRest,
      SourceSentRest,
      SourceDoRetr,
      SourceSentRetr,
      SourceWaitTransfer,
      SourceResetProt,
      
      // Destination socket
      DestSentStat,
      DestWaitCwd,
      DestDoType,
      DestSentType,
      DestSentSscn,
      DestSentProt,
      DestDoPort,
      DestSentPort,
      DestSentRest,
      DestDoStor,
      DestSentStor,
      DestWaitTransfer,
      DestResetProt
    };
    
    enum ProtectionMode {
      ProtClear = 0,
      ProtPrivate = 1,
      ProtSSCN = 2
    };
    
    enum TransferMode {
      TransferPASV = 0,
      TransferCPSV = 1
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandFxp, FtpSocket, CmdFxp)
    
    FtpSocket *companion;
    
    KURL sourceFile;
    KURL destinationFile;
    filesize_t resumeOffset;
    
    void cleanup()
    {
      // We have been interrupted, so we have to abort the companion as well
      if (!socket()->getConfigInt("params.fxp.abort")) {
        companion->setConfig("params.fxp.abort", 1);
        companion->protoAbort();
      }
      
      // Unclean upload termination, be sure to erase the cached stat infos
      if (!socket()->getConfigInt("params.fxp.keep_cache"))
        Cache::self()->invalidateEntry(socket(), destinationFile.directory());
    }
    
    void process()
    {
      switch (currentState) {
        case None: {
          sourceFile.setPath(socket()->getConfig("params.fxp.source"));
          destinationFile.setPath(socket()->getConfig("params.fxp.destination"));
          socket()->setConfig("params.fxp.keep_cache", 0);
          
          // Who are we ? Where shall we begin ?
          if (socket()->getConfigInt("params.fxp.companion")) {
            // We are the companion, so we should check the destination
            socket()->setConfig("params.fxp.companion", 0);
            
            currentState = DestSentStat;
            socket()->protoStat(destinationFile);
            return;
          } else {
            socket()->setConfig("params.transfer.mode", TransferPASV);
            
            if (socket()->getCurrentDirectory() != sourceFile.directory()) {
              // Attempt to CWD to the parent directory
              currentState = SourceSentCwd;
              socket()->sendCommand("CWD " + sourceFile.directory());
              return;
            }
          }
        }
        
        // ***************************************************************************
        // ***************************** Source socket *******************************
        // ***************************************************************************
        case SourceSentCwd: {
          if (currentState == SourceSentCwd) {
            if (!socket()->isResponse("250")) {
              socket()->emitError(FileNotFound);
              socket()->resetCommandClass(Failed);
              return;
            }
            
            if (socket()->isMultiline())
              return;
            else
              socket()->setCurrentDirectory(sourceFile.directory());
          }
          
          // We are the source socket, let's stat
          currentState = SourceSentStat;
          socket()->protoStat(sourceFile);
          break;
        }
        case SourceSentStat: {
          if (socket()->getStatResponse().filename().isEmpty()) {
            socket()->emitError(FileNotFound);
            socket()->resetCommandClass(Failed);
          } else {
            // File exists, invoke the companion
            companion->setConfig("params.fxp.companion", 1);
            companion->thread()->siteToSite(socket()->thread(), sourceFile, destinationFile);
            currentState = SourceDestVerified;
          }
          break;
        }
        case SourceDestVerified: {
          if (isWakeup()) {
            // We have been waken up because a decision has been made
            FileExistsWakeupEvent *event = static_cast<FileExistsWakeupEvent*>(m_wakeupEvent);
            
            if (!socket()->getConfigInt("feat.rest") && event->action == FileExistsWakeupEvent::Resume)
              event->action = FileExistsWakeupEvent::Overwrite;
            
            switch (event->action) {
              case FileExistsWakeupEvent::Rename: {
                // Change the destination filename, otherwise it is the same as overwrite
                destinationFile.setPath(event->newFileName);
              }
              case FileExistsWakeupEvent::Overwrite: {
                companion->setConfig("params.fxp.rest", 0);
                resumeOffset = 0;
                break;
              }
              case FileExistsWakeupEvent::Resume: {
                companion->setConfig("params.fxp.rest", companion->getStatResponse().size());
                resumeOffset = companion->getStatResponse().size();
                break;
              }
              case FileExistsWakeupEvent::Skip: {
                // Transfer should be aborted
                companion->setConfig("params.fxp.keep_cache", 1);
                socket()->setConfig("params.fxp.keep_cache", 1);
                
                socket()->resetCommandClass(UserAbort);
                socket()->emitEvent(Event::EventTransferComplete);
                return;
              }
            }
          } else {
            companion->setConfig("params.fxp.rest", 0);
            resumeOffset = 0;
          }
          
          // Change type
          currentState = SourceSentType;
          
          QString type = "TYPE ";
          type.append(KFTPCore::Config::self()->ftpMode(sourceFile.path()));
          socket()->sendCommand(type);
          break;
        }
        case SourceSentType: {
          if (socket()->getConfigInt("ssl") && socket()->getConfigInt("ssl.prot_mode") != 2 && !socket()->getConfigInt("sscn.activated")) {
            if (socket()->getConfigInt("ssl.prot_mode") == 0) {
              if (socket()->getConfigInt("feat.sscn")) {
                // We support SSCN
                currentState = SourceSentSscn;
                socket()->sendCommand("SSCN ON");
                companion->setConfig("params.ssl.mode", ProtPrivate);
              } else if (companion->getConfigInt("feat.sscn")) {
                // Companion supports SSCN
                currentState = SourceWaitType;
                companion->setConfig("params.ssl.mode", ProtSSCN);
                companion->nextCommandAsync();
              } else if (socket()->getConfigInt("feat.cpsv")) {
                // We support CPSV
                currentState = SourceWaitType;
                socket()->setConfig("params.transfer.mode", TransferCPSV);
                companion->setConfig("params.ssl.mode", ProtPrivate);
                companion->nextCommandAsync();
              } else {
                // Neither support SSCN, can't do SSL transfer
                socket()->emitEvent(Event::EventMessage, i18n("Neither server supports SSCN/CPSV but SSL data connection requested, aborting transfer!"));
                socket()->resetCommandClass(Failed);
                return;
              }
            } else {
              currentState = SourceSentProt;
              socket()->sendCommand("PROT C");
              companion->setConfig("params.ssl.mode", ProtClear);
            }
          } else {
            currentState = SourceWaitType;
            companion->nextCommandAsync();
          }
          break;
        }
        case SourceSentSscn: {
          if (!socket()->isResponse("2")) {
            socket()->resetCommandClass(Failed);
          } else {
            socket()->setConfig("sscn.activated", 1);
            socket()->setConfig("params.fxp.changed_prot", 0);
            
            currentState = SourceWaitType;
            companion->nextCommandAsync();
          }
          break;
        }
        case SourceSentProt: {
          if (!socket()->isResponse("2")) {
            socket()->resetCommandClass(Failed);
          } else {
            socket()->setConfig("params.fxp.changed_prot", 1);
            
            currentState = SourceWaitType;
            companion->nextCommandAsync();
          }
          break;
        }
        case SourceWaitType: {
          // We are ready to invoke file transfer, do PASV
          if (socket()->getConfigInt("feat.pret")) {
            currentState = SourceSentPret;
            socket()->sendCommand("PRET RETR " + sourceFile.filename());
          } else {
            currentState = SourceSentPasv;
            
            switch (socket()->getConfigInt("params.transfer.mode")) {
              case TransferPASV: socket()->sendCommand("PASV"); break;
              case TransferCPSV: socket()->sendCommand("CPSV"); break;
            }
          }
          break;
        }
        case SourceSentPret: {
          if (!socket()->isResponse("2")) {
            if (socket()->isResponse("550")) {
              socket()->emitError(PermissionDenied);
              socket()->resetCommandClass(Failed);
              return;
            } else if (socket()->isResponse("530")) {
              socket()->emitError(FileNotFound);
              socket()->resetCommandClass(Failed);
            }
            
            socket()->setConfig("feat.pret", 0);
          }
          
          currentState = SourceSentPasv;
            
          switch (socket()->getConfigInt("params.transfer.mode")) {
            case TransferPASV: socket()->sendCommand("PASV"); break;
            case TransferCPSV: socket()->sendCommand("CPSV"); break;
          }
          break;
        }
        case SourceSentPasv: {
          // Parse the PASV response and get it to the companion to issue PORT
          if (!socket()->isResponse("2")) {
            socket()->resetCommandClass(Failed);
          } else {
            QString tmp = socket()->getResponse();
            int pos = tmp.find('(') + 1;
            tmp = tmp.mid(pos, tmp.find(')') - pos);
            
            currentState = SourceDoRest;
            companion->setConfig("params.fxp.ip", tmp);
            companion->nextCommandAsync();
          }
          break;
        }
        case SourceDoRest: {
          currentState = SourceSentRest;
          socket()->sendCommand("REST " + QString::number(resumeOffset));
          break;
        }
        case SourceSentRest: {
          if (!socket()->isResponse("2") && !socket()->isResponse("3")) {
            socket()->setConfig("feat.rest", 0);
            companion->setConfig("params.fxp.rest", 0);
          } else {
            // Signal resume
            socket()->emitEvent(Event::EventResumeOffset, resumeOffset);
          }
          
          currentState = SourceDoRetr;
          companion->nextCommandAsync();
          break;
        }
        case SourceDoRetr: {
          currentState = SourceSentRetr;
          socket()->sendCommand("RETR " + sourceFile.filename());
          break;
        }
        case SourceSentRetr: {
          if (!socket()->isResponse("1")) {
            socket()->resetCommandClass(Failed);
          } else {
            currentState = SourceWaitTransfer;
          }
          break;
        }
        case SourceWaitTransfer: {
          if (!socket()->isMultiline()) {
            // Transfer has been completed
            if (socket()->getConfigInt("params.fxp.changed_prot")) {
              currentState = SourceResetProt;
              
              QString prot = "PROT ";
              
              if (socket()->getConfigInt("ssl.prot_mode") == 0) 
                prot.append('P');
              else
                prot.append('C');
              
              socket()->sendCommand(prot);
            } else {
              markClean();
              
              socket()->emitEvent(Event::EventMessage, i18n("Transfer completed."));
              socket()->emitEvent(Event::EventTransferComplete);
              socket()->resetCommandClass();
            }
          }
          break;
        }
        case SourceResetProt: {
          markClean();
          
          socket()->emitEvent(Event::EventMessage, i18n("Transfer completed."));
          socket()->emitEvent(Event::EventTransferComplete);
          socket()->resetCommandClass();
          break;
        }
        
        // ***************************************************************************
        // *************************** Destination socket ****************************
        // ***************************************************************************
        case DestSentStat: {
          if (socket()->getStatResponse().filename().isEmpty()) {
            // Change the working directory
            currentState = DestWaitCwd;
            socket()->changeWorkingDirectory(destinationFile.directory(), true);
          } else {
            // The file already exists, request action
            DirectoryListing list;
            list.addEntry(companion->getStatResponse());
            list.addEntry(socket()->getStatResponse());
            
            currentState = DestDoType;
            socket()->emitEvent(Event::EventFileExists, list);
          }
          break;
        }
        case DestWaitCwd: {
          // Directory has been changed/created, call back the companion
          currentState = DestDoType;
          companion->nextCommandAsync();
          break;
        }
        case DestDoType: {
          currentState = DestSentType;
            
          QString type = "TYPE ";
          type.append(KFTPCore::Config::self()->ftpMode(sourceFile.path()));
          socket()->sendCommand(type);
          break;
        }
        case DestSentType: {
          if (socket()->getConfigInt("ssl")) {
            // Check what the source socket has instructed us to do
            switch (socket()->getConfigInt("params.ssl.mode")) {
              case ProtClear: {
                // We should use cleartext data channel
                if (socket()->getConfigInt("ssl.prot_mode") != 2) {
                  currentState = DestSentProt;
                  socket()->sendCommand("PROT C");
                } else {
                  currentState = DestDoPort;
                  companion->nextCommandAsync();
                }
                break;
              }
              case ProtPrivate: {
                // We should use private data channel
                if (socket()->getConfigInt("ssl.prot_mode") != 0) {
                  currentState = DestSentProt;
                  socket()->sendCommand("PROT P");
                } else {
                  currentState = DestDoPort;
                  companion->nextCommandAsync();
                }
                break;
              }
              case ProtSSCN: {
                // We should initialize SSCN mode
                if (!socket()->getConfigInt("sscn.activated")) {
                  currentState = DestSentSscn;
                  socket()->sendCommand("SSCN ON");
                } else {
                  currentState = DestDoPort;
                  companion->nextCommandAsync();
                }
                break;
              }
            }
          } else {
            currentState = DestDoPort;
            companion->nextCommandAsync();
          }
          break;
        }
        case DestSentSscn: {
          if (!socket()->isResponse("2")) {
            socket()->resetCommandClass(Failed);
          } else {
            socket()->setConfig("sscn.activated", 1);
            socket()->setConfig("params.fxp.changed_prot", 0);
            
            currentState = DestDoPort;
            companion->nextCommandAsync();
          }
          break;
        }
        case DestSentProt: {
          if (!socket()->isResponse("2")) {
            socket()->resetCommandClass(Failed);
          } else {
            socket()->setConfig("params.fxp.changed_prot", 1);
            
            currentState = DestDoPort;
            companion->nextCommandAsync();
          }
          break;
        }
        case DestDoPort: {
          currentState = DestSentPort;
          socket()->sendCommand("PORT " + socket()->getConfig("params.fxp.ip"));
          break;
        }
        case DestSentPort: {
          if (!socket()->isResponse("2")) {
            socket()->resetCommandClass(Failed);
          } else {
            currentState = DestSentRest;
            socket()->sendCommand("REST " + socket()->getConfig("params.fxp.rest"));
          }
          break;
        }
        case DestSentRest: {
          // We are ready for file transfer
          currentState = DestDoStor;
          companion->nextCommandAsync();
          break;
        }
        case DestDoStor: {
          currentState = DestSentStor;
          socket()->sendCommand("STOR " + destinationFile.filename());
          break;
        }
        case DestSentStor: {
          if (!socket()->isResponse("1")) {
            socket()->resetCommandClass(Failed);
          } else {
            currentState = DestWaitTransfer;
            companion->nextCommandAsync();
          }
          break;
        }
        case DestWaitTransfer: {
          if (!socket()->isMultiline()) {
            // Transfer has been completed
            if (socket()->getConfigInt("params.fxp.changed_prot")) {
              currentState = DestResetProt;
              
              QString prot = "PROT ";
              
              if (socket()->getConfigInt("ssl.prot_mode") == 0) 
                prot.append('P');
              else
                prot.append('C');
              
              socket()->sendCommand(prot);
            } else {
              markClean();
              
              socket()->emitEvent(Event::EventMessage, i18n("Transfer completed."));
              socket()->emitEvent(Event::EventReloadNeeded);
              socket()->resetCommandClass();
            }
          }
          break;
        }
        case DestResetProt: {
          markClean();
          
          socket()->emitEvent(Event::EventMessage, i18n("Transfer completed."));
          socket()->emitEvent(Event::EventReloadNeeded);
          socket()->resetCommandClass();
          break;
        }
      }
    }
};

void FtpSocket::protoSiteToSite(Socket *socket, const KURL &source, const KURL &destination)
{
  emitEvent(Event::EventState, i18n("Transfering..."));
  emitEvent(Event::EventMessage, i18n("Transfering file '%1'...").arg(source.fileName()));
  
  // Set the source and destination
  setConfig("params.fxp.abort", 0);
  setConfig("params.fxp.source", source.path());
  setConfig("params.fxp.destination", destination.path());
  
  FtpCommandFxp *fxp = new FtpCommandFxp(this);
  fxp->companion = static_cast<FtpSocket*>(socket);
  m_cmdData = fxp;
  m_cmdData->process();
}

// *******************************************************************************************
// ******************************************* NOOP ******************************************
// *******************************************************************************************

class FtpCommandKeepAlive : public Commands::Base {
public:
    enum State {
      None,
      SentNoop
    };
    
    ENGINE_STANDARD_COMMAND_CONSTRUCTOR(FtpCommandKeepAlive, FtpSocket, CmdKeepAlive)
    
    void process()
    {
      switch (currentState) {
        case None: {
          currentState = SentNoop;
          socket()->sendCommand("NOOP");
          break;
        }
        case SentNoop: {
          socket()->resetCommandClass();
          break;
        }
      }
    }
};

void FtpSocket::protoKeepAlive()
{
  emitEvent(Event::EventState, i18n("Transmitting keep-alive..."));
  setCurrentCommand(Commands::CmdKeepAlive);
  activateCommandClass(FtpCommandKeepAlive);
}

}
