Files
server/usr/share/psa-pear/pear/php/Horde/Vfs/Ftp.php
2026-01-07 20:52:11 +01:00

831 lines
28 KiB
PHP

<?php
/**
* VFS implementation for an FTP server.
*
* Required values for $params:
* - username: (string) The username with which to connect to the FTP server.
* - password: (string) The password with which to connect to the FTP server.
* - hostspec: (string) The FTP server to connect to.
*
* Optional values for $params:
* - lsformat: (string) The return formatting from the 'ls' command.
* Possible values: 'aix', 'standard' (default).
* - maplocalids: (boolean) If true and the POSIX extension is available, the
* driver will map the user and group IDs returned from the FTP
* server with the local IDs from the local password file. This
* is useful only if the FTP server is running on localhost or
* if the local user/group IDs are identical to the remote FTP
* server.
* - pasv: (boolean) If true, connection will be set to passive mode.
* - port: (integer) The port used to connect to the ftp server if other than
* 21 (FTP default).
* - ssl: (boolean) If true, and PHP had been compiled with OpenSSL support,
* TLS transport-level encryption will be negotiated with the server.
* - timeout: (integer) The timeout for the server.
* - type: (string) The type of the remote FTP server. Possible values: 'unix',
* 'win', 'netware' By default, we attempt to auto-detect type.
*
* Copyright 2002-2017 Horde LLC (http://www.horde.org/)
* Copyright 2002-2007 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.horde.org/licenses/lgpl21.
*
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Michael Varghese <mike.varghese@ascellatech.com>
* @package Vfs
*/
class Horde_Vfs_Ftp extends Horde_Vfs_Base
{
/**
* Hash containing connection parameters.
*
* @var array
*/
protected $_params = array('port' => 21);
/**
* List of additional credentials required for this VFS backend.
*
* @var array
*/
protected $_credentials = array('username', 'password');
/**
* List of permissions and if they can be changed in this VFS backend.
*
* @var array
*/
protected $_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 resource
*/
protected $_stream = false;
/**
* Local cache array for user IDs.
*
* @var array
*/
protected $_uids = array();
/**
* Local cache array for group IDs.
*
* @var array
*/
protected $_gids = array();
/**
* The FTP server type.
*
* @var string
*/
protected $_type;
/**
* Returns the size of a file.
*
* @param string $path The path of the file.
* @param string $name The filename.
*
* @return integer The size of the file in bytes.
* @throws Horde_Vfs_Exception
*/
public function size($path, $name)
{
$this->_connect();
if (($size = @ftp_size($this->_stream, $this->_getPath($path, $name))) === -1) {
throw new Horde_Vfs_Exception(sprintf('Unable to check file size of "%s".', $this->_getPath($path, $name)));
}
return $size;
}
/**
* 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.
*/
public function read($path, $name)
{
$file = $this->readFile($path, $name);
clearstatcache();
$size = filesize($file);
return ($size === 0)
? ''
: file_get_contents($file);
}
/**
* Retrieves a file from the VFS as an on-disk local file.
*
* This function provides a file on local disk with the data of a VFS file
* in it. This file <em>cannot</em> be modified! The behavior if you do
* modify it is undefined. It will be removed at the end of the request.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return string A local filename.
* @throws Horde_Vfs_Exception
*/
public function readFile($path, $name)
{
$this->_connect();
// Create a temporary file and register it for deletion at the
// end of this request.
if (!($localFile = Horde_Util::getTempFile('vfs'))) {
throw new Horde_Vfs_Exception('Unable to create temporary file.');
}
$result = @ftp_get(
$this->_stream,
$localFile,
$this->_getPath($path, $name),
FTP_BINARY);
if ($result === false) {
throw new Horde_Vfs_Exception(sprintf('Unable to open VFS file "%s".', $this->_getPath($path, $name)));
}
clearstatcache();
return $localFile;
}
/**
* Open a stream to a file in the VFS.
*
* @param string $path The pathname to the file.
* @param string $name The filename to retrieve.
*
* @return resource The stream.
* @throws Horde_Vfs_Exception
*/
public function readStream($path, $name)
{
if (!empty($this->_params['ssl'])) {
if (function_exists('ftp_ssl_connect')) {
$url = 'ftps://';
} else {
throw new Horde_Vfs_Exception('Unable to connect with SSL.');
}
} else {
$url = 'ftp://';
}
$url .= $this->_params['username'] . ':' . $this->_params['password']
. '@' . $this->_params['hostspec'] . ':' . $this->_params['port']
. '/' . $this->_getPath($path, $name);
$stream = @fopen($url, 'r');
if (!is_resource($stream)) {
throw new Horde_Vfs_Exception('Unable to open VFS file.');
}
return $stream;
}
/**
* 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?
*
* @throws Horde_Vfs_Exception
*/
public function write($path, $name, $tmpFile, $autocreate = false)
{
$this->_connect();
$this->_checkQuotaWrite('file', $tmpFile, $path, $name);
if (!@ftp_put($this->_stream, $this->_getPath($path, $name), $tmpFile, FTP_BINARY)) {
if ($autocreate) {
$this->autocreatePath($path);
if (@ftp_put($this->_stream, $this->_getPath($path, $name), $tmpFile, FTP_BINARY)) {
return;
}
}
throw new Horde_Vfs_Exception(sprintf('Unable to write VFS file "%s".', $this->_getPath($path, $name)));
}
}
/**
* 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|resource $data The data as a string or stream resource.
* Resources allowed @since 2.4.0
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function writeData($path, $name, $data, $autocreate = false)
{
$tmpFile = Horde_Util::getTempFile('vfs');
$data = $this->_ensureSeekable($data);
file_put_contents($tmpFile, $data);
try {
$this->write($path, $name, $tmpFile, $autocreate);
unlink($tmpFile);
} catch (Horde_Vfs_Exception $e) {
unlink($tmpFile);
throw $e;
}
}
/**
* Deletes a file from the VFS.
*
* @param string $path The path to delete the file from.
* @param string $name The filename to delete.
*
* @throws Horde_Vfs_Exception
*/
public function deleteFile($path, $name)
{
$this->_connect();
$this->_checkQuotaDelete($path, $name);
if (!@ftp_delete($this->_stream, $this->_getPath($path, $name))) {
throw new Horde_Vfs_Exception(sprintf('Unable to delete VFS file "%s".', $this->_getPath($path, $name)));
}
}
/**
* 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.
*/
public function isFolder($path, $name)
{
$result = false;
try {
$this->_connect();
$olddir = $this->getCurrentDirectory();
/* See if we can change to the given path. */
$result = @ftp_chdir($this->_stream, $this->_getPath($path, $name));
$this->_setPath($olddir);
} catch (Horde_Vfs_Exception $e) {
}
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?
*
* @throws Horde_Vfs_Exception
*/
public function deleteFolder($path, $name, $recursive = false)
{
$this->_connect();
$isDir = false;
foreach ($this->listFolder($path) as $file) {
if ($file['name'] == $name && $file['type'] == '**dir') {
$isDir = true;
break;
}
}
if ($isDir) {
$dir = $path . '/' . $name;
$file_list = $this->listFolder($dir);
if (count($file_list) && !$recursive) {
throw new Horde_Vfs_Exception(sprintf('Unable to delete "%s", as the directory is not empty.', $this->_getPath($path, $name)));
}
foreach ($file_list as $file) {
if ($file['type'] == '**dir') {
$this->deleteFolder($dir, $file['name'], $recursive);
} else {
$this->deleteFile($dir, $file['name']);
}
}
if (!@ftp_rmdir($this->_stream, $this->_getPath($path, $name))) {
throw new Horde_Vfs_Exception(sprintf('Cannot remove directory "%s".', $this->_getPath($path, $name)));
}
} elseif (!@ftp_delete($this->_stream, $this->_getPath($path, $name))) {
throw new Horde_Vfs_Exception(sprintf('Cannot delete file "%s".', $this->_getPath($path, $name)));
}
}
/**
* 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.
*
* @throws Horde_Vfs_Exception
*/
public function rename($oldpath, $oldname, $newpath, $newname)
{
$this->_connect();
$this->autocreatePath($newpath);
if (!@ftp_rename($this->_stream, $this->_getPath($oldpath, $oldname), $this->_getPath($newpath, $newname))) {
throw new Horde_Vfs_Exception(sprintf('Unable to rename VFS file "%s".', $this->_getPath($oldpath, $oldname)));
}
}
/**
* Creates a folder on the VFS.
*
* @param string $path The parent folder.
* @param string $name The name of the new folder.
*
* @throws Horde_Vfs_Exception
*/
public function createFolder($path, $name)
{
$this->_connect();
if (!@ftp_mkdir($this->_stream, $this->_getPath($path, $name))) {
throw new Horde_Vfs_Exception(sprintf('Unable to create VFS directory "%s".', $this->_getPath($path, $name)));
}
}
/**
* 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 in octal notation.
*
* @throws Horde_Vfs_Exception
*/
public function changePermissions($path, $name, $permission)
{
$this->_connect();
if (!@ftp_site($this->_stream, 'CHMOD ' . $permission . ' ' . $this->_getPath($path, $name))) {
throw new Horde_Vfs_Exception(sprintf('Unable to change permission for VFS file "%s".', $this->_getPath($path, $name)));
}
}
/**
* Returns an unsorted file list of the specified directory.
*
* @param string $path The path of the directory.
* @param string|array $filter Regular expression(s) to filter
* file/directory name on.
* @param boolean $dotfiles Show dotfiles?
* @param boolean $dironly Show only directories?
*
* @return array File list.
* @throws Horde_Vfs_Exception
*/
protected function _listFolder($path = '', $filter = null,
$dotfiles = true, $dironly = false)
{
$this->_connect();
if (empty($this->_type)) {
if (!empty($this->_params['type'])) {
$this->_type = $this->_params['type'];
} else {
$type = Horde_String::lower(@ftp_systype($this->_stream));
if ($type == 'unknown') {
// Go with unix-style listings by default.
$type = 'unix';
} elseif (strpos($type, 'win') !== false) {
$type = 'win';
} elseif (strpos($type, 'netware') !== false) {
$type = 'netware';
}
$this->_type = $type;
}
}
$olddir = $this->getCurrentDirectory();
$path = $this->_getPath('', $path);
if (strlen($path)) {
$this->_setPath($path);
}
if ($this->_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)) {
$this->_setPath($olddir);
}
return array();
}
/* If 'maplocalids' is set, check for the POSIX extension. */
$mapids = (!empty($this->_params['maplocalids']) && extension_loaded('posix'));
$currtime = time();
$files = array();
foreach ($list as $line) {
$file = array();
$item = preg_split('/\s+/', $line);
if (($this->_type == 'unix') ||
(($this->_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];
if ($mapids) {
if (!isset($this->_uids[$item[2]])) {
$entry = posix_getpwuid($item[2]);
$this->_uids[$item[2]] = (empty($entry)) ? $item[2] : $entry['name'];
}
$file['owner'] = $this->_uids[$item[2]];
if (!isset($this->_uids[$item[3]])) {
$entry = posix_getgrgid($item[3]);
$this->_uids[$item[3]] = (empty($entry)) ? $item[3] : $entry['name'];
}
$file['group'] = $this->_uids[$item[3]];
} else {
$file['owner'] = $item[2];
$file['group'] = $item[3];
}
if (!empty($this->_params['lsformat']) &&
($this->_params['lsformat'] == 'aix')) {
$file['name'] = substr($line, strpos($line, sprintf("%s %2s %-5s", $item[5], $item[6], $item[7])) + 13);
} else {
$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'] = Horde_String::lower(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'] = Horde_String::lower($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', $currtime));
// If the ftp server reports a file modification date more
// less than one day in the future, don't try to subtract
// a year from the date. There is no way to know, for
// example, if the VFS server and the ftp server reside
// in different timezones. We should simply report to the
// user what the FTP server is returning.
if ($file['date'] > ($currtime + 86400)) {
$file['date'] = strtotime($item[7] . ':00' . $item[5] . ' ' . $item[6] . ' ' . (date('Y', $currtime) - 1));
}
} else {
$file['date'] = strtotime('00:00:00' . $item[5] . ' ' . $item[6] . ' ' . $item[7]);
}
} elseif ($this->_type == 'netware') {
if (count($item) < 8 || substr($line, 0, 5) == 'total') {
continue;
}
$file = array();
$file['perms'] = $item[1];
$file['owner'] = $item[2];
if ($item[0] == 'd') {
$file['type'] = '**dir';
} else {
$file['type'] = '**none';
}
$file['size'] = $item[3];
// We don't know the timezone here. Just report what the FTP server says.
if (strpos($item[6], ':') !== false) {
$file['date'] = strtotime($item[6] . ':00 ' . $item[5] . ' ' . $item[4] . ' ' . date('Y'));
} else {
$file['date'] = strtotime('00:00:00 ' . $item[5] . ' ' . $item[4] . ' ' . $item[6]);
}
$file['name'] = $item[7];
for ($index = 8, $c = count($item); $index < $c; $index++) {
$file['name'] .= ' ' . $item[$index];
}
} else {
/* Handle Windows FTP servers returning DOS-style file
* listings. */
$file['perms'] = '';
$file['owner'] = '';
$file['group'] = '';
$file['name'] = $item[3];
for ($index = 4, $c = count($item); $index < $c; $index++) {
$file['name'] .= ' ' . $item[$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'] = Horde_String::lower($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)) {
$this->_setPath($olddir);
}
return $files;
}
/**
* 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.
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function copy($path, $name, $dest, $autocreate = false)
{
$this->_checkDestination($path, $dest);
$this->_connect();
if ($autocreate) {
$this->autocreatePath($dest);
}
foreach ($this->listFolder($dest, null, true) as $file) {
if ($file['name'] == $name) {
throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
}
}
if ($this->isFolder($path, $name)) {
$this->_copyRecursive($path, $name, $dest);
} else {
$tmpFile = Horde_Util::getTempFile('vfs');
$orig = $this->_getPath($path, $name);
$fetch = @ftp_get($this->_stream, $tmpFile, $orig, FTP_BINARY);
if (!$fetch) {
unlink($tmpFile);
throw new Horde_Vfs_Exception(sprintf('Failed to copy from "%s".', $orig));
}
clearstatcache();
$this->_checkQuotaWrite('file', $tmpFile, $dest, $name);
if (!@ftp_put($this->_stream, $this->_getPath($dest, $name), $tmpFile, FTP_BINARY)) {
unlink($tmpFile);
throw new Horde_Vfs_Exception(sprintf('Failed to copy to "%s".', $this->_getPath($dest, $name)));
}
unlink($tmpFile);
}
}
/**
* 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.
* @param boolean $autocreate Automatically create directories?
*
* @throws Horde_Vfs_Exception
*/
public function move($path, $name, $dest, $autocreate = false)
{
$orig = $this->_getPath($path, $name);
if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
throw new Horde_Vfs_Exception('Cannot move file(s) - destination is within source.');
}
$this->_connect();
if ($autocreate) {
$this->autocreatePath($dest);
}
foreach ($this->listFolder($dest, null, true) as $file) {
if ($file['name'] == $name) {
throw new Horde_Vfs_Exception(sprintf('%s already exists.', $this->_getPath($dest, $name)));
}
}
if (!@ftp_rename($this->_stream, $orig, $this->_getPath($dest, $name))) {
throw new Horde_Vfs_Exception(sprintf('Failed to move to "%s".', $this->_getPath($dest, $name)));
}
}
/**
* Returns the current working directory on the FTP server.
*
* @return string The current working directory.
* @throws Horde_Vfs_Exception
*/
public function getCurrentDirectory()
{
$this->_connect();
return @ftp_pwd($this->_stream);
}
/**
* Returns the full path of an item.
*
* @param string $path The path of directory of the item.
* @param string $name The name of the item.
*
* @return mixed Full path when $path isset and just $name when not set.
*/
protected function _getPath($path, $name)
{
if (isset($this->_params['vfsroot']) &&
strlen($this->_params['vfsroot'])) {
if (strlen($path)) {
$path = $this->_params['vfsroot'] . '/' . $path;
} else {
$path = $this->_params['vfsroot'];
}
}
return parent::_getPath($path, $name);
}
/**
* Changes the current directory on the server.
*
* @param string $path The path to change to.
*
* @throws Horde_Vfs_Exception
*/
protected function _setPath($path)
{
if (!@ftp_chdir($this->_stream, $path)) {
throw new Horde_Vfs_Exception(sprintf('Unable to change to %s.', $path));
}
}
/**
* Attempts to open a connection to the FTP server.
*
* @throws Horde_Vfs_Exception
*/
protected function _connect()
{
if ($this->_stream !== false) {
return;
}
if (!extension_loaded('ftp')) {
throw new Horde_Vfs_Exception('The FTP extension is not available.');
}
if (!is_array($this->_params)) {
throw new Horde_Vfs_Exception('No configuration information specified for FTP VFS.');
}
$required = array('hostspec', 'username', 'password');
foreach ($required as $val) {
if (!isset($this->_params[$val])) {
throw new Horde_Vfs_Exception(sprintf('Required "%s" not specified in VFS configuration.', $val));
}
}
/* Connect to the ftp server using the supplied parameters. */
if (!empty($this->_params['ssl'])) {
if (function_exists('ftp_ssl_connect')) {
$this->_stream = @ftp_ssl_connect($this->_params['hostspec'], $this->_params['port']);
} else {
throw new Horde_Vfs_Exception('Unable to connect with SSL.');
}
} else {
$this->_stream = @ftp_connect($this->_params['hostspec'], $this->_params['port']);
}
if (!$this->_stream) {
throw new Horde_Vfs_Exception('Connection to FTP server failed.');
}
if (!@ftp_login($this->_stream, $this->_params['username'], $this->_params['password'])) {
@ftp_quit($this->_stream);
$this->_stream = false;
throw new Horde_Vfs_Exception('Authentication to FTP server failed.');
}
if (!empty($this->_params['pasv'])) {
@ftp_pasv($this->_stream, true);
}
if (!empty($this->_params['timeout'])) {
ftp_set_option($this->_stream, FTP_TIMEOUT_SEC, $this->_params['timeout']);
}
if (!empty($this->_params['vfsroot']) &&
!@ftp_chdir($this->_stream, $this->_params['vfsroot']) &&
!@ftp_mkdir($this->_stream, $this->_params['vfsroot'])) {
throw new Horde_Vfs_Exception(sprintf('Unable to create VFS root directory "%s".', $this->_params['vfsroot']));
}
}
}