<?php
/**
 * An FTP transfer wrapper using the OSInet Finite_State_Machine
 *
 * @copyright  (c) 2007 OSI
 * @author     Frédéric G. MARAND
 * @license    Licensed under the CeCILL 2.0
 * @version    CVS: $Id: Ftp_Client.php,v 1.3 2007-06-10 19:39:54 marand Exp $
 * @link
 * @since      Not applicable yet
 * @package    osinetoffice
 * @subpackage bo_up_ingram
 *
 */

/**
 * Save current reporting level while we set it
 */
$_ftpEr = error_reporting(E_ALL | E_STRICT);

/**
 * Class implements a finite-state-machine-based
 * FTP client with *very* limited functionality,
 * but that can display progress information
 * states are:
 * - init: not yet set up
 * - offline: no link established
 * - online: client connected to server
 * - live: client connected and logged in
 * - active: a data transfer operation is under way
 * - unsafe: something failed, disconnect can happen out of our control
 *
 * @package    osinetoffice
 * @subpackage bo_up_ingram
 */
class Ftp_Client extends Finite_State_Machine
  {
  /**
   * Code currently depends on the actual FSM location. Future versions should
   * probably locate it on their own.
   */
  const FTP_FSM = 'e:/src/osinetoffice/lib/Ftp.xml';

  /**
   * remote properties
   */
  private $fRemoteFile;
  private $fRemoteHost;
  private $fRemotePass;
  private $fRemoteUser;
  private $fRemoteWd;

  /**
   * local properties
   */
  private $fLocalWd;
  private $fLocalFile;
  private $fLocalFp;       // file pointer to local file, used in get/put/continue

  /**
   * FTP properties
   */
  private $fFtpCallback;   // name of callback function for progress information
  private $fFtpGoal;       // bytes to be transferred
  private $fFtpConnection; // connection

  /**
   * Initialize parameters from an array
   *
   * @todo should really check names and values.
   * @param array $params
   * @return Fsm_Result
   */
  public function setParameters($params)
    {
    foreach ($params as $name => $value)
      {
      $field_name = "f$name";
      $this->$field_name = $value;
      }
    $ret = $this->apply_event('CheckParameters');
    return $ret;
    }

  /**
   * ============ utility functions ============
   */

  /**
   * format a boolean value as a string
   *
   * @param boolean $val
   * @return string
   */
  static function stringFromBoolean($val)
    {
    return $val ? 'true' : 'false';
    }

  /**
   * Enter description here...
   *
   * @param unknown_type $status
   * @return unknown
   */
  static function stringFromFtp($status)
    {
    switch ($status)
      {
      case FTP_FINISHED: $ret = 'FTP_FINISHED'; break;
      case FTP_MOREDATA: $ret = 'FTP_MOREDATA'; break;
      case FTP_FAILED:   $ret = 'FTP_MOREDATA'; break;
      default:           $ret = 'FTP_INVALID_STATUS'; break;
      }
    return $ret;
    }

  /**
   * ============ event handlers ============
   */

  /**
   * implement change remote directory
   * @return string (boolean)
   */
  protected function eventChdir()
    {
    $ret = ftp_chdir($this->fFtpConnection, $this->fRemoteWd);
    return ftp_client::stringFromBoolean($ret);
    }

  /**
   * does the instance have all necessary info for a FTP transfer ?
   *
   * @return string (boolean)
   */
  public function eventCheckParameters()
    {
    $ret = isset($this->fRemoteHost)
      && isset($this->fRemoteUser)
      && isset($this->fRemotePass)
      && isset($this->fRemoteWd)
      && isset($this->fRemoteFile)
      && isset($this->fLocalWd)
      && isset($this->fLocalFile)
      && isset($this->fFtpCallback)
      ;

    $ret = ftp_client::stringFromBoolean($ret);
    // echo func_name() . ", ret = $ret\n";
    return $ret;
    }

  /**
   * implementation of Close event
   *
   * @return string (boolean)
   */
  protected function eventClose()
    {
    $ret = 'true';
    // echo "Trying to close socket...";
    if (is_resource($this->fFtpConnection))
      {
      try
        {
        // echo "Trying to close connection...";
        ftp_close($this->fFtpConnection);
        // echo "Done.";
        }
      catch (Exception $e)
        {
        print_r($e);
        $ret = 'false';
        }
      }
    // echo "Socket closed\n";
    return $ret;
    }

  /**
   * implementation of Connect event
   *
   * @return string (boolean)
   */
  protected function eventConnect()
    {
    // echo func_name() . "\n";
    $this->fFtpConnection = ftp_connect($this->fRemoteHost); // default port, default timeout
    $ret = is_resource($this->fFtpConnection);
    return Ftp_Client::stringFromBoolean($ret);
    }

  /**
   * implementation of continue
   * @return string FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
   */
  protected function eventContinue()
    {
    // echo func_name();
    if ($this->fFtpCallback)
      {
      call_user_func($this->fFtpCallback, $this, 'pre');
      }
    $ret = ftp_nb_continue($this->fFtpConnection);
    if ($ret == FTP_FINISHED)
      {
      fclose($this->fLocalFp);
      }
    if ($this->fFtpCallback)
      call_user_func($this->fFtpCallback, $this, 'post');
    $ret = Ftp_Client::stringFromFtp($ret);
    return $ret;
    }

  /**
   * implementation of get
   *
   * @return string FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
   * @throws Exception fail creating local file
   */
  protected function eventGet()
    {
    // echo func_name() . "\n";
    $this->fLocalFp = fopen($this->fLocalFile, "wb");
    if (!is_resource($this->fLocalFp))
      {
      $ret = Ftp_Client::stringFromFtp(FTP_FAILED);
      // throw new Exception(func_name() . ": could not create local file $this->fLocalFile");
      return $ret;
      }

    $this->fFtpGoal = ftp_size($this->fFtpConnection, $this->fRemoteFile);

    $ret = ftp_nb_fget($this->fFtpConnection, $this->fLocalFp,
      $this->fRemoteFile, FTP_BINARY);
    if ($ret == FTP_FINISHED)
      {
      fclose($this->fLocalFp);
      }
    call_user_func($this->fFtpCallback, $this, 'post');
    // echo func_name() . " => $ret\n"; flush();
    return Ftp_Client::stringFromFtp($ret);
    }

  /**
   * implementation of login
   * @return string (boolean)
   */
  protected function eventLogin()
    {
    // echo func_name() . "\n";
    $ret = ftp_login($this->fFtpConnection, $this->fRemoteUser, $this->fRemotePass);
    return Ftp_Client::stringFromBoolean($ret);
    }

  /**
   * handler must be implemented, but does nothing
   * @return void
   */
  protected function eventProgress()
    {
    return; // the FSM needs nothing for this
    }

  /**
   * @return void
   */
  public function __construct()
    {
    $this->load_fsm(Ftp_Client::FTP_FSM);
    // print_r($this->f_transitions);
    parent::__construct();
    // print_r($this);die();
    }

  /**
   * close connection if it hasn't been done, to prevent connection
   * lingering on the server if avoidable
   * @return void
   */
  public function __destruct()
    {
    // echo func_name() . PHP_EOL;
    if ($this->is_event_allowed('Close'))
      {
      $this->apply_event('Close');
      }

    if(is_resource($this->fLocalFp))
      try
        {
        echo "Trying to close file...";
        fclose($this->fLocalFp);
        echo "Done\n";
        }
      catch (Exception $e)
        {
        print_r($e);
        }
    // echo "End of " . func_name() . "\n";
    }

  /**
   * Make sure name can be resolved
   * ignore hosts that don't resolve in DNS or hosts file
   *
   * @param string $host
   * @return boolean
   */
  public function isHostValid($host = null)
    {
    $ret = is_array(gethostbynamel($host));
    return $ret;
    }

  /**
   * If a transfer is under way, return the progress percentile, otherwise 0.0
   *
   * @return float
   */
  public function getProgress()
    {
    if ((!$this->f_state == 'active') || (!is_resource($this->fLocalFp)))
      {
      $ret = 0.0;
      }
    else
      {
      $pos = ftell($this->fLocalFp);
      $ret = $pos / $this->fFtpGoal;
      }
    return $ret;
    }
  }

/**
 * Restore previous reporting level
 */
error_reporting($_ftpEr);