<?php
/**
 * An FTP transfer wrapper using asynchronous transfer
 * (c) 2006 Ouest Systèmes Informatiques (OSI)
 * Licensed under the CeCILL 2.0 license
 *
 * $Id: u_ftp.php,v 1.3 2007-04-28 20:03:40 marand Exp $
 *
 * @todo redo to take advantage of the new FSM 1.2 features to reduce the code
 */

require_once('u_fsm.php');

/**
 * 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
 */
class ftp_client extends fsm {
  private $f_host;
  private $f_user;
  private $f_pass;
  private $f_pwd;
  private $f_file;
  private $f_localwd;
  private $f_localfile;
  private $f_fp;       // file pointer to local file, used in get/put/continue
  private $f_goal;     // bytes to be transferred
  private $f_conn;     // connection
  private $f_callback; // name of callback function for progress information

  /**
   * @param string $url
   * @return void
   */
  public function __construct(string $url = null) {
    if ($url)
      $this->set_url($url);
    $this->fTransitions = array(
      'init' => array(
        'check_params' => array(
          true     => 'offline',
          false    => 'init',
          ),
        'set_progress'  => array(
          true     => 'offline',
          false    => 'offline',
          ),
        ),
      'offline' => array(
        'check_params' => array(
          true     => 'offline',
          false    => 'init',
          ),
        'connect' => array
          (
          true     => 'online',
          false    => 'unsafe',
          ),
        'set_progress'  => array(
          true     => 'offline',
          false    => 'offline',
          ),
        ),
      'online' => array(
        'close'  => array(
          true     => 'offline',
          false    => 'unsafe',
          ),
        'login'  => array(
          true     => 'live',
          false    => 'unsafe',
          ),
        ),
      'live' => array(
        'close'  => array(
          true     => 'offline',
          false    => 'unsafe',
          ),
        'chdir'  => array(
          true     => 'live',
          false    => 'unsafe',
          ),
        'get'    => array(
          FTP_FINISHED => 'live',
          FTP_FAILED   => 'unsafe',
          FTP_MOREDATA => 'active',
          ),
        'put' => array(
          FTP_FINISHED => 'live',
          FTP_FAILED   => 'unsafe',
          FTP_MOREDATA => 'active',
          ),
        ),
      'active' => array(
        'close'  => array(
          true     => 'offline',
          false    => 'unsafe',
          ),
        'continue' => array(
          FTP_FINISHED => 'live',
          FTP_FAILED   => 'unsafe',
          FTP_MOREDATA => 'active',
          ),
        ),
      'unsafe' => array(),
        /**
         * no transition allowed
         * Even retrying connect would not be safe
         * Must destruct
         */
      );
    parent::__construct();
  }

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

    if (is_resource($this->f_fp)) {
      try {
        fclose($this->f_fp);
      }
      catch (Exception $e) {
        print_r($e);
      }
    }
  }

  /**
   * setter for f_host. Make sur name can be resolved
   *
   * @param string $host
   * @return ftp_client
   */
  public function set_host($host = null) {
    /**
     * ignore hosts that don't resolve in DNS
     */
    if (is_array(gethostbynamel($host)))
      $this->f_host = $host;
    else
      throw new Exception(func_name() . ": cannot resolve host name \"$host\"");

    $this->applyEvent('check_params');
    return $this;
  }

  /**
   * setter for f_user
   *
   * @param string $user
   * @return ftp_client
   */
  public function set_user($user = 'anonymous') {
    $this->f_user = $user;
    $this->applyEvent('check_params');
    return $this;
  }

  /**
   * setter for f_pass
   *
   * @param string $pass
   * @return ftp_client
   */
  public function set_pass($pass = null) {
    $this->f_pass = $pass;
    $this->applyEvent('check_params');
    return $this;
  }

  /**
   * callback is invoked at the end of get, put
   * and before and after continue
   *
   * @param string $callback
   */
  public function set_callback($callback) {
    if ($callback && !function_exists($callback))
      throw new Exception(func_name() . ": cannot use undefined function $callback as callback");
    else
      $this->f_callback = $callback;
    /**
     * this setter does not cause a state change, so no call to applyEvent
     */

    return $this;
  }

  /**
   * implement change remote directory
   * @return boolean
   */
  protected function f_chdir() {
    $ret = ftp_chdir($this->f_conn, $this->f_pwd);
    return $ret;
  }

  /**
   * change remote directory
   *
   * @param string $pwd
   * @return boolean
   */
  public function chdir($pwd = null) {
    $this->f_pwd = $pwd;
    $ret = $this->applyEvent('chdir');
    return $ret;
  }

  /**
   * setter for f_file
   *
   * @param string $file
   * @return ftp_client
   */
  public function set_file($file = null) {
    $this->f_file = $file;
    if (!isset($this->f_localfile))
      $this->f_localfile = $file;

    $this->applyEvent('check_params');

    return $this;
  }

  /**
   * setter for f_localfile
   *
   * @param string $file
   * @return ftp_client
   */
  public function set_localfile($file = null) {
    $this->f_localfile = $file;
    $this->applyEvent('check_params');

    return $this;
  }

  /**
   * does the instance have all necessary info for a FTP transfer ?
   *
   * @return boolean
   */
  protected function f_check_params() {
    $ret = isset($this->f_host)
      && isset($this->f_user)
      && isset($this->f_pass)
      && isset($this->f_file)
      && isset($this->f_localfile)
      ;
    // echo func_name() . ", ret = " . ($ret ? 'TRUE' : 'FALSE') . PHP_EOL;
    return $ret;
  }

  /**
   * implementation of connect
   *
   * @return boolean
   */
  protected function f_connect() {
    // echo func_name() . "\n";
    $this->f_conn = ftp_connect($this->f_host); // default port, default timeout
    $ret = is_resource($this->f_conn);
    return $ret;
  }

  /**
   * implementation of close
   * @return boolean
   */
  protected function f_close() {
    // echo func_name() . "\n";
    $ret = ftp_close($this->f_conn);
    return $ret;
  }

  /**
   * implementation of login
   * @return boolean
   */
  protected function f_login() {
    // echo func_name() . "\n";
    $ret = ftp_login($this->f_conn, $this->f_user, $this->f_pass);
    return $ret;
  }

  /**
   * implementation of get
   *
   * @return int FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
   */
  protected function f_get() {
    // echo func_name() . "\n";
    $this->f_fp = fopen($this->f_localfile, "wb");
    if (!is_resource($this->f_fp)) {
      $ret = FTP_FAILED;
      throw new Exception(func_name() . ": could not create local file $this->f_file");
    }

    $this->f_goal = ftp_size($this->f_conn, $this->f_file);

    $ret = ftp_nb_fget($this->f_conn, $this->f_fp, $this->f_file, FTP_BINARY);
    if ($ret == FTP_FINISHED)
      fclose($this->f_fp);
    call_user_func($this->f_callback, $this, 'post');
    return $ret;
  }

  /**
   * implementation of continue
   * @return int FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
   */
  protected function f_continue() {
    if ($this->f_callback)
      call_user_func($this->f_callback, $this, 'pre');
    $ret = ftp_nb_continue($this->f_conn);
    if ($ret == FTP_FINISHED)
      fclose($this->f_fp);
    if ($this->f_callback)
      call_user_func($this->f_callback, $this, 'post');
    return $ret;
  }

  /**
   * interface to connect
   * @return void
   */
  public function connect() {
    // echo func_name() . "\n";
    return $this->applyEvent('connect');
  }

  /**
   * interface to login
   *
   * @return boolean
   */
  public function login() {
    // echo func_name() . "\n";
    return $this->applyEvent('login');
  }

  /**
   * interface to close
   *
   * @return boolean
   */
  public function close() {
    // echo func_name() . "\n";
    return $this->applyEvent('close');
  }

  /**
   * get a file using previously defined parameters
   * @return int FTP_FAILED | FTP_MOREDATA | FTP_FINISHED
   */
  public function get() {
    // echo func_name() . "\n";
    return $this->applyEvent('get');
  }

  /**
   * continue a current transfer
   *
   * @param string $callback name of function to be called before and after
   * @return int FTP_FINISHED | FTP_MOREDATA | FTP_FAILED
   */
  // continue is a php reserved word
  public function cont() {
    // echo func_name() . "\n";
    $ret = $this->applyEvent('continue');
    $ret = $ret->fsmState;
    return $ret;
  }

  public function get_progress() {
    if ((!$this->fState == 'active') || (!is_resource($this->f_fp)))
      $ret = 0;
    else {
      $pos = ftell($this->f_fp);
      $ret = $pos / $this->f_goal;
    }
    return $ret;
  }

  /* missing: put*/
}