<?php
//
// $Id: class.ftp.inc,v1.0 2004-12-18 12:27a EST Onion Exp $
//

/*
 * FTP Class
 */
class FTP {

 /*
  * Public Properties
  */
 public $host = '';
 public $port = 21;
 public $user = '';
 public $pass = '';
 public $show_pass = false;
 public $welcome_msg = '';

 /*
  * Private Properties
  */
 private $ftp_sock;
 private $data_sock;
 private $command_log  = array ();
 private $response_log = array ();
 private $full_log     = array ();

 /*
  * Constructor
  *
  * void __construct ( string host [, int port [, string user [, string pass [, bool show_pass ]]]] )
  */
 public function __construct ( $host, $port = 21, $user = 'anonymous', $pass = 'root@localhost', $show_pass = false ) {
  $this->host = $host;
  $this->port = (int) $port;
  $this->user = $user;
  $this->pass = $pass;
  $this->show_pass = $show_pass;

  $this->ftp_sock = new Socket ($host, $port);

  $this->welcome_msg = implode ('', $this->get_log (2));
  $this->write ('USER '.$user);
  $this->write ('PASS '.$pass);
 }

 /*
  * Destructor
  *
  * void __destruct ( void )
  */
 public function __destruct ( ) {
  $this->write ('QUIT');
 }

 /*
  * Public Methods
  */
 /*
  * FTP::is_ok ()
  *
  * Returns true if an error has not occured.
  *
  * bool is_ok ( void )
  */
 public function is_ok ( ) {
  return ($this->get_last_response_num () < 500);
 }

 //
 // XXX NOTE: Refer to http://faqs.org/rfcs/rfc959.html for explanation of various FTP commands. XXX
 //
 /*
  * FTP::allo ()
  *
  * bool allo ( int size )
  */ 
 public function allo ( $size ) {
  $this->write ('ALLO '.$size);
  return $this->is_ok ();
 }

 /*
  * FTP::cwd ()
  *
  * bool cwd ( string dir )
  */
 public function cwd ( $dir ) {
  $this->write ('CWD '.$dir);
  return $this->is_ok ();
 }

 /*
  * FTP::cdup ()
  *
  * bool cdup ( void )
  */
 public function cdup ( ) {
  $this->write ('CDUP');
  return $this->is_ok ();
 }

 /*
  * FTP::dele ()
  *
  * bool dele ( string file_path )
  */
 public function dele ( $file_path ) {
  $this->write ('DELE '.$file_path);
  return $this->is_ok ();
 }

 /*
  * FTP::ls ()
  *
  * Refer to "LIST" in the RFC.
  *
  * mixed ls ( [ string dir ] )
  */
 public function ls ( $dir = null ) {
  $this->pasv (&$addr, &$port);
  $this->type ('A');
  $this->write ('LIST'.($dir === null ? '' : ' '.$dir));
  return $this->is_ok () ? $this->data_sock->get_contents () : false;
 }

 /*
  * FTP::mkd ()
  *
  * bool mkd ( string dir_name )
  */
 public function mkd ( $dir_name ) {
  $this->write ('MKD '.$dir_name);
  return $this->is_ok ();
 }

 /*
  * FTP::nlst ()
  *
  * mixed nlst ( [ string dir ] )
  */
 public function nlst ( $dir = null ) {
  $this->pasv (&$addr, &$port);
  $this->type ('A');
  $this->write ('NLST'.($dir === null ? '' : ' '.$dir));
  return $this->is_ok () ? $this->data_sock->get_contents () : false;
 }

 /*
  * FTP::pasv ()
  *
  * Currently the only way this thing can transfer data with a server.
  *
  * bool pasv ( string &addr, int &port )
  */
 public function pasv ( &$addr, &$port ) {
  $this->write ('PASV');
  if ($this->is_ok ()) {
   if (preg_match ('#(\d{1,3}),(\d{1,3}),(\d{1,3}),(\d{1,3}),(\d{1,3}),(\d{1,3})#', $this->get_last_response_str (), $p)) {
    $addr = vsprintf ('%d.%d.%d.%d', array ($p[1], $p[2], $p[3], $p[4]));
    $port = (int) ((int) $p[5] << 8) + $p[6];
    $this->data_sock = new Socket ($addr, $port);
    return true;
   } else {
    // debugger crap
    $debug = new Debugger (__FILE__, __CLASS__, end (explode ('::', __METHOD__)), __LINE__, '$addr should be an IP. $port should be a valid port.', 'something else', array ('this' => $this, 'this->get_last_response_str ()' => $this->get_last_response_str (), 'this->full_log' => $this->full_log));
    foot ('<pre>'.htmlentities ($debug->__toString ()).'</pre>');
   }
  }
  return false;
 }

 /*
  * FTP::port ()
  *
  * This would be the other way, but I can't do it right now (read the "TODO" file for why).
  *
  * void port ( void )
  */
 public function port ( ) {
  // define this later
 }

 /*
  * FTP::pwd ()
  *
  * string pwd ( void )
  */
 public function pwd ( ) {
  $this->write ('PWD');
  if (preg_match ('#"(.*?)"#', $this->get_last_response_str (), $pwd_match)) {
   return $pwd_match[1];
  } else {
   // debugger crap
   $debug = new Debugger (__FILE__, __CLASS__, end (explode ('::', __METHOD__)), __LINE__, 'current directory', print_r ($pwd_match, 1), array ('this' => $this, 'this->get_last_response_str ()' => $this->get_last_response_str (), 'this->full_log' => $this->full_log, 'pwd_match' => $pwd_match));
   foot ('<pre>'.htmlentities ($debug->__toString ()).'</pre>');
  }
 }

 /*
  * FTP::rename ()
  *
  * Refer to "RNFR" and "RNTO" in the RFC.
  *
  * bool rename ( string from, string to )
  */
 public function rename ( $from, $to ) {
  $this->write ('RNFR '.$from);
  $this->write ('RNTO '.$to);
  return $this->is_ok ();
 }

 /*
  * FTP::retr ()
  *
  * mixed retr ( string file_name )
  */
 public function retr ( $file_name ) {
  $this->pasv (&$addr, &$port);
  $this->write ('RETR '.$file_name);
  return $this->is_ok () ? $this->data_sock->get_contents () : false;
 }

 /*
  * FTP::rmd ()
  *
  * bool rmd ( string dir_name )
  */
 public function rmd ( $dir_name ) {
  $this->write ('RMD '.$dir_name);
  return $this->is_ok ();
 }

 /*
  * FTP::site ()
  *
  * bool site ( string cmd )
  */
 public function site ( $cmd ) {
  $this->write ('SITE '.$cmd);
  return $this->is_ok ();
 }

 /*
  * FTP::stor ()
  *
  * bool stor ( string local_filename [, string remote_filename ] )
  */
 public function stor ( $local_filename, $remote_filename = null ) {
  $remote_filename === null ? $remote_filename = $local_filename : '';
  $this->pasv (&$addr, &$port);
  $this->write ('STOR '.$remote_filename);
  if ($this->is_ok ()) {
   $this->data_sock->write (file_get_contents ($local_filename));
  }
  return $this->is_ok ();
 }

 /*
  * FTP::syst ()
  *
  * mixed syst ( void )
  */
 public function syst ( ) {
  $this->write ('SYST');
  return $this->is_ok () ? $this->get_last_response_str () : false;
 }

 /*
  * FTP::type ()
  *
  * bool type ( string type )
  */
 public function type ( $type ) {
  $this->write ('TYPE '.$type);
  return $this->is_ok ();
 }
 
 /*
  * FTP::get_log ()
  *
  * Returns one of the three logs.
  *
  * mixed get_log ( int type )
  */
 public function get_log ( $type = 0 ) {
  switch ($type) {
   case 0:
    return $this->full_log;
   case 1:
    return $this->command_log;
   case 2:
    return $this->response_log;
   default:
    return false;
  }
 }

 /*
  * FTP::get_last_command ()
  *
  * Returns the last command sent to the server.
  *
  * string get_last_command ( void )
  */
 public function get_last_command ( ) {
  return end ($this->command_log);
 }

 /*
  * FTP::get_last_response ()
  *
  * Returns the last reponse given from the server.
  *
  * string get_last_response ( void )
  */
 public function get_last_response ( ) {
  return end ($this->response_log);
 }

 /*
  * FTP::get_last_response_num ()
  *
  * Returns the last reponse number given from the server.
  *
  * int get_last_response_num ( void )
  */
 public function get_last_response_num ( ) {
  return preg_match ('#^(\d{1,3})#', $this->get_last_response (), $matches) ? (int) $matches[1] : 0;
 }

 /*
  * FTP::get_last_response_str ()
  *
  * Returns the last reponse message given from the server.
  *
  * string get_last_response_str ( void )
  */
 public function get_last_response_str ( ) {
  return preg_match ('#^\d+(.*)$#', $this->get_last_response (), $matches) ? trim ($matches[1]) : '';
 }

 /*
  * Private Methods
  */
 /*
  * FTP::write ()
  *
  * Sends a command to the server and logs stuff.
  *
  * void write ( string cmd )
  */
 private function write ( $cmd ) {
  $this->ftp_sock->write ("$cmd\r\n");

  $cmd_str             = (!$this->show_pass && preg_match ('#^PASS#', $cmd) ? 'PASS xxx' : $cmd)."\r\n";
  $this->command_log[] = $cmd_str;
  $this->full_log[]    = $cmd_str;

  for (;$line = $this->ftp_sock->get_line (); $this->response_log[] = $line, $this->full_log[] = $line);
 }
}
