<?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->f_transitions = 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->apply_event('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->apply_event('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->apply_event('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 apply_event
     */

    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->apply_event('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->apply_event('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->apply_event('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->apply_event('connect');
    }

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

  /**
   * interface to close
   *
   * @return boolean
   */
  public function close()
    {
    // echo func_name() . "\n";
    return $this->apply_event('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->apply_event('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
   */
  public function cont() // continue is a php reserved word
    {
    // echo func_name() . "\n";
    $ret = $this->apply_event('continue');
    $ret = $ret->fsm_state;
    return $ret;
    }

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

  /* missing: put*/
  }