[horde] help with VFS?

Cliff Green green at umdnj.edu
Fri Feb 3 10:34:30 PST 2006


Quoting Michael M Slusarz <slusarz at bigworm.curecanti.org>:
[munch]

> Without knowing what your SSH2-based VFS driver looks like (hint
> hint), we have no way of knowing what is wrong.
Yah.  As soon as I hit "Send Message" I realized that...  Please see 
the attached.

Also, below is a patch for horde/test.php to test for the ssh2 module...

> Additionally, several VFS bugs have been fixed in Horde 3.1 so that
> may also be the issue.
I was concerned about being able to use it with the Release version of 
Horde, wanting to avoid any unknown dependencies.  If there aren't any, 
I'll get to work on it.  FWIW, this is based on the ftp driver.  (or 
did I say that already?)


<--- test.php patch begins --->
--- test.php.dist       2006-02-02 10:43:17.000000000 -0500
+++ test.php    2006-02-02 10:44:38.000000000 -0500
@@ -48,6 +48,7 @@
     'ctype' => 'Ctype Support',
     (version_compare(phpversion(), '5') < 0 ? 'domxml' : 'dom') => 
'DOM XML Support',
     'ftp' => 'FTP Support',
+    'ssh2' => 'SSH2 Support',
     'gd' => 'GD Support',
     'gettext' => array(
         'descrip' => 'Gettext Support',
<----- test.php patch ends ----->

c
-- 
Cliff Green
BS&T
UMDNJ
-------------- next part --------------
<?php
/**
 * VFS implementation for an SSH server.
 *
 * Required values for $params:<pre>
 *      'username'       The username with which to connect to the ssh2 server.
 *      'password'       The password with which to connect to the ssh2 server.
 *      'hostspec'       The ssh2 server to connect to.</pre>
 *
 * Optional values for $params:<pre>
 *      'port'           The port used to connect to the ssh2 server if other
 *                       than 22.
 *
 * $Horde: framework/VFS/VFS/ssh2.php,v 1.75.4.9 2005/08/24 14:04:39 selsky Exp $
 *
 * Copyright 2002-2005 Chuck Hagenbuch <chuck at horde.org>
 * Copyright 2002-2005 Michael Varghese <mike.varghese at 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 at horde.org>
 * @author  Michael Varghese <mike.varghese at ascellatech.com>
 * @butcher  Cliff Green <green at umdnj.edu>
 * @since   Horde 2.2
 * @package VFS
 */
class VFS_ssh2 extends VFS {

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

    /**
     * List of permissions and if they can be changed in this VFS backend.
     *
     * @var 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 ssh2 server.
     *
     * @var resource
     */
    var $_stream = false;

    /**
     * Retrieves 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 = @ssh2_scp_recv($this->_stream, $this->_getPath($path, $name), $tmpFile);
        if ($fetch === false) {
            return PEAR::raiseError(sprintf(_("Unable to open VFS file \"%s\"."), $this->_getPath($path, $name)));
        }

        $size = filesize($tmpFile);
        if ($size === 0) {
            return '';
        }

        if (OS_WINDOWS) {
            $mode = 'rb';
        } else {
            $mode = 'r';
        }
        $fp = fopen($tmpFile, $mode);
        $data = fread($fp, $size);
        fclose($fp);
        unlink($tmpFile);

        return $data;
    }

    /**
     * Stores 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  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 (!@ssh2_scp_send($this->_stream, $tmpFile, $this->_getPath($path, $name))) {
            if ($autocreate) {
                $result = $this->autocreatePath($path);
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }
                if (!@ssh2_scp_send($this->_stream, $tmpFile, $this->_getPath($path, $name))) {
                    return PEAR::raiseError(sprintf(_("Unable to write VFS file \"%s\"."), $this->_getPath($path, $name)));
                }
            } else {
                return PEAR::raiseError(sprintf(_("Unable to write VFS file \"%s\"."), $this->_getPath($path, $name)));
            }
        }

        return true;
    }

    /**
     * Stores 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  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;
    }

    /**
     * Deletes a file from the VFS.
     *
     * @param string $path  The path to delete the file from.
     * @param string $name  The filename to delete.
     *
     * @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;
        }
        /*  create sftp  resource */
        $sftp = @ssh2_sftp( $conn );

        if (!@ssh2_unlink($sftp, $this->_getPath($path, $name))) {
            return PEAR::raiseError(sprintf(_("Unable to delete VFS file \"%s\"."), $this->_getPath($path, $name)));
        }

        return true;
    }

    /**
     * Checks if a given item is a folder.
     *
     * @param string $path  The parent folder.
     * @param string $name  The item name.
     *
     * @return boolean  True if it is a folder, false otherwise.
     */
    function isFolder($path, $name)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $result = false;
        $olddir = $this->getCurrentDirectory();

        /* See if we can stat the remote filename */
        /*  create sftp  resource */
        $sftp = @ssh2_sftp( $conn );
        $statinfo = @ssh2_stat($sftp, $this->_getPath($path, $name));
        if ($statinfo['mode'] & 040000) {
            $result = true;
        }

        return $result;
    }

    /**
     * Deletes a folder from the VFS.
     *
     * @param string $path        The parent folder.
     * @param string $name        The name of the folder to delete.
     * @param boolean $recursive  Force a recursive delete?
     *
     * @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;
        }

        /*  create sftp  resource */
        $sftp = @ssh2_sftp( $conn );

        @ssh2_sftp_rmdir($sftp, $this->_getPath($path, $name));

        return true;
    }

    /**
     * Renames 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;
        }

        /*  create sftp  resource */
        $sftp = @ssh2_sftp( $conn );

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

        return true;
    }

    /**
     * Creates a folder on the VFS.
     *
     * @param string $path  The parent folder.
     * @param string $name  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;
        }

        /*  create sftp  resource */
        $sftp = @ssh2_sftp( $conn );

        if (!@ssh2_sftp_mkdir($sftp, $this->_getPath($path, $name))) {
            return PEAR::raiseError(sprintf(_("Unable to create VFS directory \"%s\"."), $this->_getPath($path, $name)));
        }

        return true;
    }

    /**
     * Changes permissions for an item on the VFS.
     *
     * @param string $path        The parent folder of the item.
     * @param string $name        The name of the item.
     * @param string $permission  The permission to set.
     *
     * @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 (!@ssh2_exec($conn, 'chmod ' . $permission . ' ' . $this->_getPath($path, $name))) {
            return PEAR::raiseError(sprintf(_("Unable to change permission for VFS file \"%s\"."), $this->_getPath($path, $name)));
        }

        return true;
    }

    /**
     * Returns an an unsorted file list of the specified directory.
     *
     * @param string $path       The path of the directory.
     * @param mixed $filter      String/hash to filter file/dirname on.
     * @param boolean $dotfiles  Show dotfiles?
     * @param boolean $dironly   Show only directories?
     *
     * @return array  File list on success or PEAR_Error on failure.
     */
    function _listFolder($path = '', $filter = null, $dotfiles = true,
                         $dironly = false)
    {
        $conn = $this->_connect();
        if (is_a($conn, 'PEAR_Error')) {
            return $conn;
        }

        $files = array();
        // Go with unix-style listings by default.
        $type = 'unix';
        $olddir = $this->getCurrentDirectory();
        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 = ssh2_exec($conn,  'ls -al');
                $dotfiles = true;
            } else {
                $list = ssh2_exec($conn, 'ls -l');
            }
        } else {
           $list = ssh2_exec($conn, '');
        }

        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' || ($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 (strpos($item[7], ':') !== false) {
                    $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 SSH 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 the specified directory.
     *
     * @param string $path         The path of the directory to get the
     *                             directory list for.
     * @param mixed $filter        Hash of items to filter based on folderlist.
     * @param boolean $dotfolders  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 of the original file.
     * @param string $name  The name of the original file.
     * @param string $dest  The name of the destination directory.
     *
     * @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);
        if (is_a($fileCheck, 'PEAR_Error')) {
            return $fileCheck;
        }
        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);
        if (is_a($dirCheck, 'PEAR_Error')) {
            return $dirCheck;
        }
        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 = @ssh2_scp_recv($conn, $this->_getPath($path, $name), $tmpFile);
            if (!$fetch) {
                unlink($tmpFile);
                return PEAR::raiseError(sprintf(_("Failed to copy from \"%s\"."), $this->_getPath($path, $name)));
            }

            if (!@ssh2_scp_send($conn, $tmpFile, $this->_getPath($dest, $name))) {
                unlink($tmpFile);
                return PEAR::raiseError(sprintf(_("Failed to copy to \"%s\"."), $this->_getPath($dest, $name)));
            }

            unlink($tmpFile);
        }

        return true;
    }

    /**
     * Moves a file through the backend.
     *
     * @param string $path  The path of the original file.
     * @param string $name  The name of the original file.
     * @param string $dest  The destination file name.
     *
     * @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;
        }
        /*  create sftp  resource */
        $sftp = @ssh2_sftp( $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 (!@ssh2_sftp_rename($sftp, $this->_getPath($path, $name), $this->_getPath($dest, $name))) {
            return PEAR::raiseError(sprintf(_("Failed to move to \"%s\"."), $this->_getPath($dest, $name)));
        }

        return true;
    }

    /**
     * Returns the current working directory on the SSH server.
     *
     * @return string  The current working directory.
     */
    function getCurrentDirectory()
    {
        if (is_a($conn = $this->_connect(), 'PEAR_Error')) {
            return $conn;
        }
        return ssh2_exec($conn, 'pwd');
    }

    /**
     * Changes the current directory on the server.
     *
     * @access private
     *
     * @param string $path  The path to change to.
     *
     * @return mixed  True on success, or a PEAR_Error on failure.
     */
    function _setPath($path)
    {
        if (!@ssh2_exec($conn, 'cd $path')) {
            return PEAR::raiseError(sprintf(_("Unable to change to %s."), $path));
        }
        return true;
    }

    /**
     * Returns the full path of an item.
     *
     * @access private
     *
     * @param string $path  The directory of the item.
     * @param string $name  The name of the item.
     *
     * @return mixed  Full path to the file when $path is not empty and just
     *                $name when not set.
     */
    function _getPath($path, $name)
    {
        if ($path !== '') {
             return ($path . '/' . $name);
        }
        return ($name);
    }

    /**
     * Returns the parent directory of the specified path.
     *
     * @access private
     *
     * @param string $path  The path to get the parent of.
     *
     * @return string  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 = $this->getCurrentDirectory();
        @ssh2_exec($conn, 'cd ..');

        $parent = $this->getCurrentDirectory();
        $this->_setPath($olddir);

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

        return $parent;
    }

    /**
     * Attempts to open a connection to the SSH server.
     *
     * @access private
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function _connect()
    {
        if ($this->_stream === false) {
            if (!extension_loaded('ssh2')) {
                return PEAR::raiseError(_("The SSH extension is not available."));
            }

            if (!is_array($this->_params)) {
                return PEAR::raiseError(_("No configuration information specified for SSH VFS."));
            }

            $required = array('hostspec', 'username', 'password');
            foreach ($required as $val) {
                if (!isset($this->_params[$val])) {
                    return PEAR::raiseError(sprintf(_("Required \"%s\" not specified in VFS configuration."), $val));
                }
            }

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

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

        }

        return true;
    }

    /**
     * Disconnects from the SSH server and cleans up the connection.
     *
     * @access private
     */
    function _disconnect()
    {
        @ssh2_exec($this->_stream, 'exit');
        $this->_stream = false;
    }

    /**
     * Determines the location of the system temporary directory.  If a
     * specific setting cannot be found, it defaults to /tmp
     *
     * @access private
     *
     * @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;
    }

    /**
     * Creates a temporary file.
     *
     * @access private
     *
     * @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 deleton and return */
        if (empty($tmp_file)) {
            return false;
        } else {
            return $tmp_file;
        }
    }
}


More information about the horde mailing list