<?php
/**
 * VFS implementation for an FTP server.
 *
 * Required values for $params:
 *      'username'       The username with which to connect to the ftp server.
 *      'password'       The password with which to connect to the ftp server.
 *      'hostspec'       The ftp server to connect to.
 * Optional values for $params:
 *      'port'           The port used to connect to the ftp server if other 
 *                       than 21.
 *
 * $Horde: horde/lib/VFS/ftp.php,v 1.51 2003/08/22 00:05:19 chuck Exp $
 *
 * Copyright 2002-2003 Chuck Hagenbuch <chuck@horde.org>
 * Copyright 2002-2003 Michael Varghese <mike.varghese@ascellatech.com>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @author  Michael Varghese <mike.varghese@ascellatech.com>
 * @version $Revision: 1.51 $
 * @since   Horde 2.2
 * @package vfs
 */
class VFS_ftp extends VFS {

    /**
     * Boolean indicating whether or not we're connected to the ftp
     * server.
     *
     * @var $_connected boolean
     */
    var $_connected = false;

    /**
     * List of additional credentials required for this VFS backend.
     *
     * @var $_credentials array
     */
    var $_credentials = array('username', 'password');

    /**
     * List of permissions and if they can be changed in this VFS
     * backend.
     *
     * @var $_permissions array
     */
    var $_permissions = array(
        'owner' => array('read' => true, 'write' => true, 'execute' => true),
        'group' => array('read' => true, 'write' => true, 'execute' => true),
        'all'   => array('read' => true, 'write' => true, 'execute' => true));

    /**
     * Variable holding the connection to the ftp server.
     *
     * @var $_stream resource
     */
    var $_stream = '';

    /**
     * Retrieve a file from the VFS.
     *
     * @param string $path  The pathname to the file.
     * @param string $name  The filename to retrieve.
     *
     * @return string The file data.
     */
    function read($path, $name)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $tmpFile = $this->_getTempFile();
        $fetch = @ftp_get($this->_stream, $tmpFile,
                          $this->_getPath($path, $name), FTP_BINARY);
        if ($fetch !== false) {
            if (OS_WINDOWS) {
                $mode = 'rb';
            } else {
                $mode = 'r';
            }
            $fp = fopen($tmpFile, $mode);
            $data = fread($fp, filesize($tmpFile));
            fclose($fp);
            unlink($tmpFile);
            return $data;
        } else {
            unlink($tmpFile);
            return PEAR::raiseError(_('Unable to open VFS file.'));
        }
    }

    /**
     * Store a file in the VFS.
     *
     * @param string  $path        The path to store the file in.
     * @param string  $name        The filename to use.
     * @param string  $tmpFile     The temporary file containing the data to be stored.
     * @param boolean $autocreate  (optional) Automatically create directories?
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function write($path, $name, $tmpFile, $autocreate = false)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        if (!@ftp_put($this->_stream, $this->_getPath($path, $name), $tmpFile, FTP_BINARY)) {
            return PEAR::raiseError(_('Unable to write VFS file.'));
        }

        return true;
    }

    /**
     * Store a file in the VFS from raw data.
     *
     * @param string  $path        The path to store the file in.
     * @param string  $name        The filename to use.
     * @param string  $data        The file data.
     * @param boolean $autocreate  (optional) Automatically create directories?
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function writeData($path, $name, $data, $autocreate = false)
    {
        $tmpFile = $this->_getTempFile();
        $fp = fopen($tmpFile, 'wb');
        fwrite($fp, $data);
        fclose($fp);

        $result = $this->write($path, $name, $tmpFile, $autocreate);
        unlink($tmpFile);
        return $result;
    }

    /**
     * Delete a file from the VFS.
     *
     * @param string $path  The path to delete the file from.
     * @param string $name  The filename to use.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function deleteFile($path, $name)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        if (!@ftp_delete($this->_stream, $this->_getPath($path, $name))) {
            return PEAR::raiseError(_('Unable to delete VFS file.'));
        }

        return true;
    }

    /**
     * Check if a given pathname is a folder.
     *
     * @param string $path  The path to the folder.
     * @param string $name  The file/folder name.
     *
     * @return boolean  True if it is a folder, false otherwise.
     */
    function isFolder($path, $name)
    {
        $result = false;
        $olddir = ftp_pwd($this->_stream);

        /* See if we can change to the given path. */
        if (@ftp_chdir($this->_stream, $this->_getPath($path, $name))) {
            $result = true;
        }

        $this->_setPath($olddir);

        return $result;
    }

    /**
     * Delete a folder from the VFS.
     *
     * @param string  $path       The path to delete the folder from.
     * @param string  $name       The name of the folder to delete.
     * @param boolean $recursive  (optional) Force a recursive delete. Defaults to no.
     *
     * @return mixed True on success or a PEAR_Error object on failure.
     */
    function deleteFolder($path, $name, $recursive = false)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $isDir = false;
        $dirCheck = $this->listFolder($path);
        foreach ($dirCheck as $file) {
            if ($file['name'] == $name && $file['type'] == '**dir') {
                $isDir = true;
                break;
            }
        }

        if ($isDir) {
            $file_list = $this->listFolder($this->_getPath($path, $name));
            if (is_a($file_list, 'PEAR_Error')) {
                return $file_list;
            }

            if (count($file_list) && !$recursive) {
                return PEAR::raiseError(sprintf(_("Unable to delete %s, the directory is not empty"),
                                                $path . '/' . $name));
            }

            foreach ($file_list as $file) {
                if ($file['type'] == '**dir') {
                    $result = $this->deleteFolder($this->_getPath($path, $name), $file['name'], $recursive);
                } else {
                    $result = $this->deleteFile($this->_getPath($path, $name), $file['name']);
                }
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }
            }

            if (!@ftp_rmdir($this->_stream, $this->_getPath($path, $name))) {
                return PEAR::raiseError(_('Cannot remove directory: ') . $this->_getPath($path, $name));
            }
        } else {
            if (!@ftp_delete($this->_stream, $this->_getPath($path, $name))) {
                return PEAR::raiseError(_('Cannot delete file: ') . $this->_getPath($path, $name));
            }
        }

        return true;
    }

    /**
     * Rename a file in the VFS.
     *
     * @param string $oldpath  The old path to the file.
     * @param string $oldname  The old filename.
     * @param string $newpath  The new path of the file.
     * @param string $newname  The new filename.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function rename($oldpath, $oldname, $newpath, $newname)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        if (!@ftp_rename($this->_stream, $this->_getPath($oldpath, $oldname), $this->_getPath($newpath, $newname))) {
            return PEAR::raiseError(sprintf(_('Unable to rename VFS file %s/%s.'), $oldpath, $oldname));
        }

        return true;
    }

    /**
     * Creates a folder on the VFS.
     *
     * @param string $path Holds the path of directory to create folder.
     * @param string $name Holds the name of the new folder.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function createFolder($path, $name)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        if (!@ftp_mkdir($this->_stream, $this->_getPath($path, $name))) {
            return PEAR::raiseError(_('Unable to create VFS folder.'));
        }

        return true;
    }

    /**
     * Changes permissions for an Item on the VFS.
     *
     * @param string $path Holds the path of directory of the Item.
     * @param string $name Holds the name of the Item.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function changePermissions($path, $name, $permission)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        if (!@ftp_site($this->_stream, 'CHMOD ' . $permission . ' ' . $this->_getPath($path, $name))) {
            return PEAR::raiseError(sprintf(_('Unable to change permission for VFS file %s/%s.'), $path, $name));
        }

        return true;
    }

    /**
     * Returns an unsorted file list.
     *
     * @param string  $path     The path of the directory to get the file list for.
     * @param mixed   $filter   (optional) hash of items to filter based on filename.
     * @param boolean $dotfiles (optional) Show dotfiles?
     * @param boolean $dironly  (optional) Show directories only?
     *
     * @return mixed  File list on success or a PEAR_Error object on failure.
     */
    function listFolder($path = '', $filter = null, $dotfiles = true, $dironly = false)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $files = array();
        $type = @ftp_systype($this->_stream);
        if ($type == 'UNKNOWN') {
            // Go with unix-style listings by default.
            $type = 'UNIX';
        }

        $olddir = ftp_pwd($this->_stream);
        if (!empty($path)) {
            $res = $this->_setPath($path);
            if (is_a($res, 'PEAR_Error')) {
                return $res;
            }
        }

        if ($type == 'UNIX') {
            // If we don't want dotfiles, We can save work here by not
            // doing an ls -a and then not doing the check later (by
            // setting $dotfiles to true, the if is short-circuited).
            if ($dotfiles) {
                $list = ftp_rawlist($this->_stream, '-al');
                $dotfiles = true;
            } else {
                $list = ftp_rawlist($this->_stream, '-l');
            }
        } else {
           $list = ftp_rawlist($this->_stream, '');
        }

        if (!is_array($list)) {
            if (isset($olddir)) {
                $res = $this->_setPath($olddir);
                if (is_a($res, 'PEAR_Error')) {
                    return $res;
                }
            }
            return array();
        }

        foreach ($list as $line) {
            $file = array();
            $item = preg_split('/\s+/', $line);
            if ($type == 'UNIX' || (stristr($type, 'win') && !preg_match('|\d\d-\d\d-\d\d|', $item[0]))) {
                if (count($item) < 8 || substr($line, 0, 5) == 'total') {
                    continue;
                }
                $file['perms'] = $item[0];
                $file['owner'] = $item[2];
                $file['group'] = $item[3];
                $file['name'] = substr($line, strpos($line, sprintf("%s %2s %5s", $item[5], $item[6], $item[7])) + 13);

                // Filter out '.' and '..' entries.
                if (preg_match('/^\.\.?\/?$/', $file['name'])) {
                    continue;
                }

                // Filter out dotfiles if they aren't wanted.
                if (!$dotfiles && substr($file['name'], 0, 1) == '.') {
                    continue;
                }

                $p1 = substr($file['perms'], 0, 1);
                if ($p1 === 'l') {
                    $file['link'] = substr($file['name'], strpos($file['name'], '->') + 3);
                    $file['name'] = substr($file['name'], 0, strpos($file['name'], '->') - 1);
                    $file['type'] = '**sym';

                   if ($this->isFolder('', $file['link'])) {
                              $file['linktype'] = '**dir';
                                                    } else {
                                                    $parts = explode('/', $file['link']);
                                                    $name = explode('.', array_pop($parts));
                                                    if (count($name) == 1 || ($name[0] === '' && count($name) == 2)) {
                                                        $file['linktype'] = '**none';
                                                        } else {
                                                            $file['linktype'] = VFS::strtolower(array_pop($name));
                                                            }
                                                                   }
                } elseif ($p1 === 'd') {
                    $file['type'] = '**dir';
                } else {
                    $name = explode('.', $file['name']);
                    if (count($name) == 1 || (substr($file['name'], 0, 1) === '.' && count($name) == 2)) {
                        $file['type'] = '**none';
                    } else {
                        $file['type'] = VFS::strtolower($name[count($name) - 1]);
                    }
                }
                if ($file['type'] == '**dir') {
                    $file['size'] = -1;
                } else {
                    $file['size'] = $item[4];
                }
                if (strstr($item[7], ':')) {
                    $file['date'] = strtotime($item[7] . ':00' . $item[5] . ' ' . $item[6] . ' ' . date('Y', time()));
                    if ($file['date'] > time()) {
                        $file['date'] = strtotime($item[7] . ':00' . $item[5] . ' ' . $item[6] . ' ' . (date('Y', time()) - 1));
                    }
                } else {
                    $file['date'] = strtotime('00:00:00' . $item[5] . ' ' . $item[6] . ' ' . $item[7]);
                }
            } else {
                /* Handle Windows FTP servers returning DOS-style file
                 * listings. */
                $file['perms'] = '';
                $file['owner'] = '';
                $file['group'] = '';
                $file['name'] = $item[3];
                $index = 4;
                while ($index < count($item)) {
                    $file['name'] .= ' ' . $item[$index];
                    $index++;
                }
                $file['date'] = strtotime($item[0] . ' ' . $item[1]);
                if ($item[2] == '<DIR>') {
                    $file['type'] = '**dir';
                    $file['size'] = -1;
                } else {
                    $file['size'] = $item[2];
                    $name = explode('.', $file['name']);
                    if (count($name) == 1 || (substr($file['name'], 0, 1) === '.' && count($name) == 2)) {
                        $file['type'] = '**none';
                    } else {
                        $file['type'] = VFS::strtolower($name[count($name) - 1]);
                    }
                }
            }

            // Filtering.
            if ($this->_filterMatch($filter, $file['name'])) {
                unset($file);
                continue;
            }
            if ($dironly && $file['type'] !== '**dir') {
                unset($file);
                continue;
            }

            $files[$file['name']] = $file;
            unset($file);
        }

        if (isset($olddir)) {
            $res = $this->_setPath($olddir);
            if (is_a($res, 'PEAR_Error')) {
                return $res;
            }
        }
        return $files;
    }

    /**
     * Returns a sorted list of folders in specified directory.
     *
     * @param string  $path       The path of the directory to get the directory list for.
     * @param mixed   $filter     (optional) hash of items to filter based on folderlist.
     * @param boolean $dotfolders (optional) Include dotfolders?
     *
     * @return mixed  Folder list on success or a PEAR_Error object on failure.
     */
    function listFolders($path = '', $filter = null, $dotfolders = true)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $folders = array();
        $folder = array();

        $folderList = $this->listFolder($path, null, $dotfolders, true);
        if (is_a($folderList, 'PEAR_Error')) {
            return $folderList;
        }

        $folder['val'] = $this->_parentDir($path);
        $folder['abbrev'] = '..';
        $folder['label'] = '..';

        $folders[$folder['val']] = $folder;

        foreach ($folderList as $files) {
            $folder['val'] = $this->_getPath($path, $files['name']);
            $folder['abbrev'] = $files['name'];
            $folder['label'] = $folder['val'];

            $folders[$folder['val']] = $folder;
        }

        ksort($folders);
        return $folders;
    }

    /**
     * Copies a file through the backend.
     *
     * @param string $path  The path to store the file in.
     * @param string $name  The filename to use.
     * @param string $dest  The destination of the file.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function copy($path, $name, $dest)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $fileCheck = $this->listFolder($dest, null, true);
        foreach ($fileCheck as $file) {
            if ($file['name'] == $name) {
                return PEAR::raiseError(sprintf(_("%s already exists."), $this->_getPath($dest, $name)));
            }
        }

        $isDir = false;
        $dirCheck = $this->listFolder($path, null, false);
        foreach ($dirCheck as $file) {
            if ($file['name'] == $name && $file['type'] == '**dir') {
                $isDir = true;
                break;
            }
        }

        if ($isDir) {
            $result = $this->createFolder($dest, $name);

            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }

            $file_list = $this->listFolder($this->_getPath($path, $name));
            foreach ($file_list as $file) {
                $result = $this->copy($this->_getPath($path, $name), $file['name'], $this->_getPath($dest, $name));
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }
            }
        } else {
            $tmpFile = $this->_getTempFile();
            $fetch = @ftp_get($this->_stream, $tmpFile, $this->_getPath($path, $name), FTP_BINARY);
            if (!$fetch) {
                unlink($tmpFile);
                return PEAR::raiseError(sprintf(_("Failed to retrieve: %s"), $this->_getPath($path, $name)));
            }

            if (!@ftp_put($this->_stream, $this->_getPath($dest, $name), $tmpFile, FTP_BINARY)) {
                unlink($tmpFile);
                return PEAR::raiseError(sprintf(_("Copy failed: %s"), $this->_getPath($dest, $name)));
            }

            unlink($tmpFile);
        }

        return true;
    }

    /**
     * Moves a file through the backend.
     *
     * @param string $path  The path to store the file in.
     * @param string $name  The filename to use.
     * @param string $dest  The destination of the file.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function move($path, $name, $dest)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $fileCheck = $this->listFolder($dest, null, true);
        foreach ($fileCheck as $file) {
            if ($file['name'] == $name) {
                return PEAR::raiseError(sprintf(_("%s already exists."), $this->_getPath($dest, $name)));
            }
        }

        if (!@ftp_rename($this->_stream, $this->_getPath($path, $name), $this->_getPath($dest, $name))) {
            return PEAR::raiseError(sprintf(_("Move failed: %s"), $this->_getPath($dest, $name)));
        }

        return true;
    }

    /**
     * Change directories on the server.
     *
     * @param string $path  The path to change to.
     *
     * @return mixed  True on success, or a PEAR_Error on failure.
     */
    function _setPath($path)
    {
        if (!@ftp_chdir($this->_stream, $path)) {
            return PEAR::raiseError(sprintf(_("Unable to change to %s."), $path));
        }
        return true;
    }

    /**
     * Returns the full path of an item.
     *
     * @param string $path Holds the path of directory of the Item.
     * @param string $name Holds the name of the Item.
     *
     * @return mixed  Full path when $path isset and just $name when not set.
     */
    function _getPath($path, $name)
    {
        if ($path !== '') {
             return ($path . '/' . $name);
        }
        return ($name);
    }

    /**
     * Returns the parent directory of specified path.
     *
     * @param string $path  The path to get the parent of.
     *
     * @return mixed  The parent directory (string) on success
     *                or a PEAR_Error object on failure.
     */
    function _parentDir($path)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $olddir = ftp_pwd($this->_stream);
        @ftp_cdup($this->_stream);

        $parent = @ftp_pwd($this->_stream);
        $this->_setPath($olddir);

        if (!$parent) {
            return PEAR::raiseError(_("Unable to determine current directory."));
        }

        return $parent;
    }

    /**
     * Attempts to open a connection to the FTP server.
     *
     * @return mixed       True on success or a PEAR_Error object on failure.
     */
    function _connect()
    {
        if (!$this->_connected) {
            if (!is_array($this->_params)) {
                return PEAR::raiseError(_("No configuration information specified for FTP VFS."));
            }
            if (!isset($this->_params['username'])) {
                return PEAR::raiseError(sprintf(_("Required '%s' not specified in VFS configuration."), 'username'));
            }
            if (!isset($this->_params['password'])) {
                return PEAR::raiseError(sprintf(_("Required '%s' not specified in VFS configuration."), 'password'));
            }
            if (!isset($this->_params['hostspec'])) {
                return PEAR::raiseError(sprintf(_("Required '%s' not specified in VFS configuration."), 'hostspec'));
            }
            if (!isset($this->_params['port'])) {
                $this->_params['port'] = 21;
            }

            /* Connect to the ftp server using the supplied parameters. */
            $this->_stream = @ftp_connect($this->_params['hostspec'], $this->_params['port']);
            if (!$this->_stream) {
                return PEAR::raiseError(_("Connection to FTP server failed."));
            }

            $this->_connected = @ftp_login($this->_stream, $this->_params['username'], $this->_params['password']);
            if (!$this->_connected) {
                return PEAR::raiseError(_("Authentication to FTP server failed."));
            }


            @ftp_pasv($this->_stream, true);
        }
        return true;
    }

    /**
     * Disconnect from the FTP server and clean up the connection.
     *
     * @return boolean     True on success, false on failure.
     */
    function _disconnect()
    {
        @ftp_quit($this->_stream);
        $this->_connected = false;
    }

    /**
     * Determine the location of the system temporary directory.  If a
     * specific setting cannot be found, it defaults to /tmp
     *
     * @access public
     *
     * @return string  A directory name which can be used for temp files.
     *                 Returns false if one could not be found.
     */
    function _getTempDir()
    {
        $tmp_locations = array('/tmp', '/var/tmp', 'c:\WUTemp', 'c:\temp', 'c:\windows\temp', 'c:\winnt\temp');

        /* Try PHP's upload_tmp_dir directive. */
        $tmp = ini_get('upload_tmp_dir');

        /* Otherwise, try to determine the TMPDIR environment
           variable. */
        if (empty($tmp)) {
            $tmp = getenv('TMPDIR');
        }

        /* If we still cannot determine a value, then cycle through a
         * list of preset possibilities. */
        while (empty($tmp) && sizeof($tmp_locations)) {
            $tmp_check = array_shift($tmp_locations);
            if (@is_dir($tmp_check)) {
                $tmp = $tmp_check;
            }
        }

        /* If it is still empty, we have failed, so return false;
         * otherwise return the directory determined. */
        return empty($tmp) ? false : $tmp;
    }

    /**
     * Create a temporary file.
     *
     * @access public
     *
     * @return string   Returns the full path-name to the temporary file.
     *                  Returns false if a temp file could not be created.
     */
    function _getTempFile()
    {
        $tmp_dir = $this->_getTempDir();
        if (empty($tmp_dir)) {
            return false;
        }

        $tmp_file = tempnam($tmp_dir, 'vfs');

        /* If the file was created, then register it for deletion and return */
        if (empty($tmp_file)) {
            return false;
        } else {
            return $tmp_file;
        }
    }

}
