<?php

goweb::loadClass('Goweb_Ftp_Abstract');

/**
 * Goweb Sitemanager 5 Framework
 *
 * @version    5.0.25
 * @category   Goweb
 * @package    Goweb
 * @author     I&D <dev@goweb.pt>, Pedro Pinto <ppinto@goweb.pt>
 * @copyright  2010 (c) Goweb (http://www.goweb.pt)
 */

/**
 *
 * @version    5.0.25
 * @category   Goweb
 * @package    Goweb
 * @author     I&D <dev@goweb.pt>, Pedro Pinto <ppinto@goweb.pt>
 * @copyright  2010 (c) Goweb (http://www.goweb.pt)
 */

class Goweb_Ftp_Sync {
    
    /**
     * Default transfer mode used when uploading or downloading files
     * @var integer
     */
    protected $transfermode = FTP_BINARY;
    
    /**
     * Default timeout in seconds
     * @var integer
     */
    protected $timeout = 30;
    
    /**
     * Default port which is used while connecting
     * @var integer
     */
    protected $port = 21;
    
    /**
     * Username 
     * @var string
     */
    protected $user;
    
    /**
     * Password
     * @var string
     */
    protected $pass;
    
    /**
     * Host, IP or hostname
     * @var string
     */
    protected $host;
    
    /**
     * Stores the current directory
     * @var string
     */
    public $curDir;
    
    /**
     * Stores the number of bytes uploaded (files), reset on reconnect
     * @var integer
     */
    public $bytesUploaded = 0;
    
    /**
     * Stores number of bytes downloaded (files), reset on reconnect
     * @var integer
     */
    public $bytesDownloaded = 0;
    
    /**
     * Stores the stream ressource when connected
     * @var ressource
     */
    protected $stream;

    /**
     * Directories to process
     *
     * @var array
     */
    protected $_directories = array();

    /**
     * Files to process
     *
     * @var array
     */
    protected $_files = array();

    /**
     * Goweb_Config
     *
     * @var Goweb_Config
     */
    protected $_config = null;
    
    /**
     * Creates a new FTP Instance
     * 
     * @param string $host Hostname or ip to connect to
     * @param string $user  Username
     * @param string $pass Password
     * @param integer $port optional alternate post (default: 21)
     * @param integer $timeout optional timout in seconds
     */
    public function __construct($host, $user = null, $pass = null, $port = null, $timeout = null) {
        foreach(array('host', 'user', 'pass', 'port', 'timeout') as $key) {
            if ($$key !== null) $this->{$key} = $$key;
        }
        $this->_config = goweb::registry('config');
        return $this;
    }
    
    /**
     * Checks if a connection is established and returns the result true/false
     * @return boolean
     */
    public function connected() {
        return !empty($this->stream);
    }
    
    /**
     * Checks for a connection, if no connection established throws a Exception
     * @return boolean
     */
    public function checkConnection() {
        if (!$this->connected()) throw new Goweb_Exception('No FTP connection established.');
        return true;
    }
    
    /**
     * Tries to establish a ftp connection to the host.
     * Host is validated so only found ips for hostnames and ips with hostnames
     * are valid. {@link afterConnect} is called after a connection was
     * successfully set up.
     */
    public function connect() {
        $this->disconnect();
        // check ip / or host
        if (empty($this->host)) {
            throw new Goweb_Exception('Invalid Host (empty)');
        } elseif (preg_match('/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b/', $this->host)) {
            if (!@gethostbyaddr($this->host)) {
                throw new Goweb_Exception('Invalid host: \'' . $this->host . '\'');
            }
        } elseif (in_array(gethostbyname($this->host), array('', $this->host))) {
            throw new Goweb_Exception('Invalid host: \'' . $this->host . '\'');
        }
        // establish connection
        $started = time();
        if (!($this->stream = ftp_connect($this->host, $this->port, $this->timeout))) {
            if (time() - $started >= $this->timeout) {
                throw new Goweb_Exception('Timeout hit while connecting.');
            } else {
                throw new Goweb_Exception('unknown reason');
            }
        }
        $this->afterConnect();
        return $this;
    }
    
    /**
     * Callback after successfull {@link connect}
     * @return FTP
     */
    public function afterConnect() {
        ftp_set_option($this->stream, FTP_TIMEOUT_SEC, $this->timeout);
        $this->login();
        $this->curDir = $this->pwd();
        return $this;
    }
    
    /**
     * Close the current ftp connection
     * @return FTP
     */
    public function disconnect() {
        $this->bytesDownloaded = $this->bytesUploaded = 0;
        if ($this->connected()) {
            ftp_close($this->stream);
            unset($this->stream);
        }
        return $this;
    }
    
    /**
     * Logs into the ftp server
     * @throws Goweb_Exception if the login failed
     * @return boolean
     */
    
    public function login() {
        $this->checkConnection();
        if (!@ftp_login($this->stream, $this->user, $this->pass)) {
            $lastError = error_get_last();
            // add information about user and password used (but don’t show password)
            $message = 'Failed login on as user \''.$this->user.'\' using password: ';
            if ($this->pass) {
                $message .= 'yes';
            } else {
                $message .= 'no';
            }
            // add error message from last login if found
            if (isset($lastError['message'])) {
                $message .= ', \''.$lastError['message'].'\'';
            }
            throw new Goweb_Exception($message);
        }
        return true;
    }
    
    /**
     * Try to determine the time difference between the local and remote system
     * by uploading a file and compare the modified time values. The file that
     * is uploaded is _this_ file, after time difference the file is deleted.
     * @return integer
     */
    public function detectTimeDifference() {
        $remoteName = $this->curDir.'.ftpTimdifferenceTestFile';
        $this->upload(__FILE__, $remoteName);
        $modified = $this->lastModified($remoteName);
        $result = time() - $modified;
        if ($this->exists($remoteName)) {
            $this->delete($remoteName);
        }
        if ($result > 5) {
            return $result;
        } else {
            return 0;
        }
    }
    
    /**
     * Returns a simple list of files and directories in the current directory.
     * If you want a more precise list use {@link rawList}
     * 
     * Note: This won't show you hidden files on -nix systems (the files with
     * the dot at the beginning!)
     * 
     * @param string $filename name of directory to list files from
     * @return array(string)
     */
    public function ls($filename = null) {
        $this->checkConnection();
        if ($filename === null) {
            $filename = $this->curDir;
        }
        // check if directory exits
        $dirname = dirname($filename.'/s');
        if (!$this->exists($dirname.'/')) {
            throw new Goweb_Exception('Directory \''.$dirname.'\' not found.');
        }
        return ftp_nlist($this->stream, $filename);
    }
    
    /**
     * Set the passive mode to true or false
     * @param boolean $value
     * @return boolean
     */
    public function pasv($value) {
        $this->checkConnection();
        ftp_pasv($this->stream, (bool) $value);
        return true;
    }
    
    /**
     * Returns a more precise list of files and directories than {@link ls}.
     * 
     * <code>
     * $exampleReturn = array(
     *  'file1.txt' => array('isDirectory' => false, 'flags' => '-rw-rw-rw', 'filename' => 'file1.txt'),
     *  'directory' => array('isDirectory' => true, 'flags' => 'drw-rw-rw', 'filename' => 'directory/')
     * );
     * </code>
     * 
     * Note that this method also returns hidden files.
     * 
     * @param string $filename
     * @param boolean $hiddenFiles  List hidden files
     * @return array(string)
     */
    public function rawList($filename = null, $hiddenFiles = true) {
        $this->checkConnection();
        if ($filename === null) {
            $filename = $this->curDir;
        }
        // extract dirname, cause we need to change this directory
        $dirname = dirname($filename.'/a');
        $lastDir = $this->curDir;
        $this->cd($dirname);
        $result = ftp_rawlist($this->stream, '-al');
        if ($this->curDir !== $lastDir) {
            $this->cd($lastDir);
        }
        // parse result
        $files = array();
        foreach ($result as $line) {
            $found = preg_split('[ ]', $line, 9, PREG_SPLIT_NO_EMPTY);
            if (in_array($found[8], array('.', '..', ''))) {
                continue;
            }
            $firstFlag = substr($found[0], 0, 1);
            // ignore symbolic links
            if ($firstFlag == 'l'){
            } else if ($firstFlag == 'd') {
                $files = array_merge($files, $this->rawList($filename . '/' . $found[8]));
            } else if ($firstFlag == '-') {
                if ($found[0] != 'total') {
                    $fileInfo['size']     = $found[4];
                    $fileInfo['filename'] = str_replace($this->_config->ftp->directory, '',  $dirname) . '/' . $found[8];
                    if ($this->_config->ftp->public_html != 'public_html'){
                        $fileInfo['filename'] = str_replace($this->_config->ftp->public_html . '/', 'public_html/', $fileInfo['filename']);
                    }
                }

                if ( strrpos($found[7], ':') != 2 ) {
                    $fileInfo['last_modified'] = strtotime($found[5] ." " . $found[6] . " " . $found[7] . " 00:00");
                } else {
                    $fileInfo['last_modified'] = strtotime($found[5] ." " . $found[6] . " " . date("Y") . ' ' . $found[7]);
                }
                $files[] = $fileInfo;
            }
            
        }
        return $files;
    }

    /**
     * Enter description here...
     *
     * @param string $path the path to scan
     * @param int $pattern the pattern passed to glob()
     * @param int $flags he flags passed to glob()
     * @return mixed an array of files in the given path matching the pattern.
     */
    protected function _recursiveGlob($path = '', $pattern = '*', $flags = 0) {
        $paths = glob($path . '*', GLOB_MARK|GLOB_ONLYDIR|GLOB_NOSORT);
        foreach ($paths as $path) { 
            $paths = array_merge($paths, $this->_recursiveGlob($path, $pattern, $flags));
        }
        return $paths;
    }
    
    public function setAllDirectories($path = null){
        if (is_string($path)){
            $this->_directories = $this->_recursiveGlob($path);
        } else {
            throw new Goweb_Exception('Invalid parameter');
        }
        $this->_files = array();
        foreach ($this->_directories as $directory){
            $files = glob($directory . '*');
            foreach ($files as $file){
                $this->_files[] = array(
                                'last_modified' => filemtime ($file),
                                'size'          => filesize ($file),
                                'filename'      => str_replace($path, '', $file)
                );
                
            }
        }
        return $this;
    }

    public function getAllDirectories($path = null){
        return $this->_directories;
    }
    
    public function getAllFiles(){
        return $this->_files;
    }
    
    /**
     * Uploads a local file to the remote server and returns the transferrate
     * in bytes per second
     * @param string $filename
     * @param string $remoteFilename
     * @param boolean $overwrite
     * @param integer $mode
     * @return integer  Number of bytes per second transferred
     */
    public function upload($filename, $remoteFilename = null, $overwrite = false, $mode = null) {
        $this->checkConnection();
        if ($mode == null) {
            $mode = $this->transfermode;
        }
        if (!file_exists($filename)) {
            throw new Goweb_Exception('File not found \'' . $filename . '\'.');
        } elseif (is_dir($filename)) {
            // todo enable recursive directory upload
        }
        if ($remoteFilename === null) {
            $remoteFilename = $filename;
        }
        // change to directory the file should go to or create the directory
        if (strpos($remoteFilename, '/') !== false) {
            $lastDir = $this->curDir;
            try {
                $this->cd(dirname($remoteFilename));
            } catch (Goweb_Exception $e) {
                $this->mkdir(dirname($remoteFilename));
            }
        }
        // perform the upload and get the time needed
        $fileSize = filesize($filename);
        $transferStart = microtime(true);
        $result = ftp_put($this->stream, $remoteFilename, $filename, $this->transfermode);
        $lastError = error_get_last();
        if (isset($lastError['message']) && preg_match('@PORT command successful@i', $lastError['message'])) {
            $result = true;
        }
        $transferEnd = microtime(true);
        if (!empty($lastDir) && $lastDir !== $this->curDir) {
            $this->cd($lastDir);
        }
        if ($result) {
            $this->bytesUploaded = $fileSize;
            return $fileSize / ($transferEnd - $transferStart);
        } else {
            false;
        }
    }
    
    /**
     * Downloads a remote file to local directory
     * @param string $remoteFilename
     * @param string $localFilename
     * @param boolean $overwrite
     * @param integer $mode
     * @return FTP
     */
    public function download($remoteFilename, $localFilename, $overwrite = false, $mode = null) {
        $this->checkConnection();
        if (file_exists($localFilename) && $overwrite !== true) {
            throw new Goweb_Exception('File allready exists on the local machine.');
        }
        if (!@ftp_get($this->stream, $localFilename, $remoteFilename, $mode)) {
            return false;
        }
        return $this;
    }
    
    /**
     * Returns the current directory
     * @return string
     */
    public function pwd() {
        $this->checkConnection();
        return ftp_pwd($this->stream);
    }
    
    /**
     * Changes directory to $path
     * @param string $path
     * @return boolean
     */
    public function cd($path) {
        $this->checkConnection();
        if (substr($path, -1, 1) !== '/' && strlen($path) > 1) {
            $path .= '/';
        }
        if (!@ftp_chdir($this->stream, $path)) {
            throw new Goweb_Exception('Directory \'' . $path . '\' not found.');
        }
        $this->curDir = $path;
        return true;
    }
    
    /**
     * Create a directory $path
     * <code>
     * if(!$ftp->mkdir('test')) {
     *  die('failed to create directory');
     * }
     * </code>
     * @return boolean
     */
    public function mkdir($path) {
        $this->checkConnection();
        $result = @ftp_mkdir($this->stream, $path);
        return $result;
    }
    
    /**
     * Delete a file or directory $path
     * @return boolean
     */
    public function delete($path) {
        $this->checkConnection();
        if (!@ftp_delete($this->stream, $path)) {
            return false;
        }
        return $this;
    }
    
    /**
     * Tests if a file or directory $filename exists on the remote system
     * @param string $filename
     * @return boolean
     */
    public function exists($filename) {
        if (empty($filename)) {
            return false;
        }
        $this->checkConnection();
        // file sizes are always true and good
        if ($size = $this->filesize($filename)) {
            return $size;
        }
        // check if directory exists by trying to change to it
        $curDir = $this->curDir;
        if (!@ftp_chdir($this->stream, $filename)) {
            $this->cd($curDir);
            return false;
        }
        $this->cd($curDir);
        return true;
    }
    
    /**
     * Returns the size of a file in bytes, if file was not found it returns
     * false, so check it with ===:
     * <code>
     * if ($ftp->size('notexistentfile') === false) {
     *  die('file not found');
     * }
     * </code>
     * @param string $filename
     * @return integer|boolean
     */
    public function size($filename) {
        if (empty($filename)) {
            return false;
        }
        $this->checkConnection();
        $size = ftp_size($this->stream, $filename);
        if ($size == -1) {
            return false;
        }
        return $size;
    }
    
    /**
     * Alias for {@link size}
     * @param string $filename
     * @return integer|boolean
     */
    public function filesize($filename) {
        return $this->size($filename);
    }
    
    /**
     * Returns timestamp of last modification of a file.
     * Note that the timestamp is the time on the server, not your local time.
     * @param string $filename
     * @return integer|boolean
     */
    public function lastModified($filename) {
        if (empty($filename)) {
            return false;
        }
        // use raw command to retreive modified timestamp, ftp_mdtm does not work :(
        return ftp_mdtm($this->stream, $filename);
    }
    
    /**
     * Sends a raw command to the server and returns a response
     * @param string $command
     * @return array(string)
     */
    public function raw($command) {
        $this->checkConnection();
        if (empty($command)) return '';
        return ftp_raw($this->stream, $command);
    }
    
    /**
     * Returns a string that contains the answer to the SYS command
     * @return string
     */
    public function systemType() {
        $this->checkConnection();
        return ftp_systype($this->stream);
    }
    
    public function __destroy() {
        $this->disconnect();
    }
}

