* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. * * @copyright Nicolas Tallefourtane http://nicolab.net */ namespace FtpClient; use \Countable; /** * The FTP and SSL-FTP client for PHP. * * @method bool alloc() alloc(int $filesize, string &$result = null) Allocates space for a file to be uploaded * @method bool cdup() cdup() Changes to the parent directory * @method bool chdir() chdir(string $directory) Changes the current directory on a FTP server * @method int chmod() chmod(int $mode, string $filename) Set permissions on a file via FTP * @method bool delete() delete(string $path) Deletes a file on the FTP server * @method bool exec() exec(string $command) Requests execution of a command on the FTP server * @method bool fget() fget(resource $handle, string $remote_file, int $mode, int $resumepos = 0) Downloads a file from the FTP server and saves to an open file * @method bool fput() fput(string $remote_file, resource $handle, int $mode, int $startpos = 0) Uploads from an open file to the FTP server * @method mixed get_option() get_option(int $option) Retrieves various runtime behaviours of the current FTP stream * @method bool get() get(string $local_file, string $remote_file, int $mode, int $resumepos = 0) Downloads a file from the FTP server * @method int mdtm() mdtm(string $remote_file) Returns the last modified time of the given file * @method int nb_continue() nb_continue() Continues retrieving/sending a file (non-blocking) * @method int nb_fget() nb_fget(resource $handle, string $remote_file, int $mode, int $resumepos = 0) Retrieves a file from the FTP server and writes it to an open file (non-blocking) * @method int nb_fput() nb_fput(string $remote_file, resource $handle, int $mode, int $startpos = 0) Stores a file from an open file to the FTP server (non-blocking) * @method int nb_get() nb_get(string $local_file, string $remote_file, int $mode, int $resumepos = 0) Retrieves a file from the FTP server and writes it to a local file (non-blocking) * @method int nb_put() nb_put(string $remote_file, string $local_file, int $mode, int $startpos = 0) Stores a file on the FTP server (non-blocking) * @method bool pasv() pasv(bool $pasv) Turns passive mode on or off * @method bool put() put(string $remote_file, string $local_file, int $mode, int $startpos = 0) Uploads a file to the FTP server * @method string pwd() pwd() Returns the current directory name * @method bool quit() quit() Closes an FTP connection * @method array raw() raw(string $command) Sends an arbitrary command to an FTP server * @method bool rename() rename(string $oldname, string $newname) Renames a file or a directory on the FTP server * @method bool set_option() set_option(int $option, mixed $value) Set miscellaneous runtime FTP options * @method bool site() site(string $command) Sends a SITE command to the server * @method int size() size(string $remote_file) Returns the size of the given file * @method string systype() systype() Returns the system type identifier of the remote FTP server * * @author Nicolas Tallefourtane */ class FtpClient implements Countable { /** * The connection with the server. * * @var resource */ protected $conn; /** * PHP FTP functions wrapper. * * @var FtpWrapper */ private $ftp; /** * Constructor. * * @param resource|null $connection * @throws FtpException If FTP extension is not loaded. */ public function __construct($connection = null) { if (!extension_loaded('ftp')) { throw new FtpException('FTP extension is not loaded!'); } if ($connection) { $this->conn = $connection; } $this->setWrapper(new FtpWrapper($this->conn)); } /** * Close the connection when the object is destroyed. */ public function __destruct() { if ($this->conn) { $this->ftp->close(); } } /** * Call an internal method or a FTP method handled by the wrapper. * * Wrap the FTP PHP functions to call as method of FtpClient object. * The connection is automaticaly passed to the FTP PHP functions. * * @param string $method * @param array $arguments * @return mixed * @throws FtpException When the function is not valid */ public function __call($method, array $arguments) { return $this->ftp->__call($method, $arguments); } /** * Overwrites the PHP limit * * @param string|null $memory The memory limit, if null is not modified * @param int $time_limit The max execution time, unlimited by default * @param bool $ignore_user_abort Ignore user abort, true by default * @return FtpClient */ public function setPhpLimit($memory = null, $time_limit = 0, $ignore_user_abort = true) { if (null !== $memory) { ini_set('memory_limit', $memory); } ignore_user_abort(true); set_time_limit($time_limit); return $this; } /** * Get the help information of the remote FTP server. * * @return array */ public function help() { return $this->ftp->raw('help'); } /** * Open a FTP connection. * * @param string $host * @param bool $ssl * @param int $port * @param int $timeout * * @return FTPClient * @throws FtpException If unable to connect */ public function connect($host, $ssl = false, $port = 21, $timeout = 90) { if ($ssl) { $this->conn = @$this->ftp->ssl_connect($host, $port, $timeout); } else { $this->conn = @$this->ftp->connect($host, $port, $timeout); } if (!$this->conn) { throw new FtpException('Unable to connect'); } return $this; } /** * Closes the current FTP connection. * * @return bool */ public function close() { if ($this->conn) { $this->ftp->close(); $this->conn = null; } } /** * Get the connection with the server. * * @return resource */ public function getConnection() { return $this->conn; } /** * Get the wrapper. * * @return FtpWrapper */ public function getWrapper() { return $this->ftp; } /** * Logs in to an FTP connection. * * @param string $username * @param string $password * * @return FtpClient * @throws FtpException If the login is incorrect */ public function login($username = 'anonymous', $password = '') { $result = $this->ftp->login($username, $password); if ($result === false) { throw new FtpException('Login incorrect'); } return $this; } /** * Returns the last modified time of the given file. * Return -1 on error * * @param string $remoteFile * @param string|null $format * * @return int */ public function modifiedTime($remoteFile, $format = null) { $time = $this->ftp->mdtm($remoteFile); if ($time !== -1 && $format !== null) { return date($format, $time); } return $time; } /** * Changes to the parent directory. * * @throws FtpException * @return FtpClient */ public function up() { $result = @$this->ftp->cdup(); if ($result === false) { throw new FtpException('Unable to get parent folder'); } return $this; } /** * Returns a list of files in the given directory. * * @param string $directory The directory, by default is "." the current directory * @param bool $recursive * @param callable $filter A callable to filter the result, by default is asort() PHP function. * The result is passed in array argument, * must take the argument by reference ! * The callable should proceed with the reference array * because is the behavior of several PHP sorting * functions (by reference ensure directly the compatibility * with all PHP sorting functions). * * @return array * @throws FtpException If unable to list the directory */ public function nlist($directory = '.', $recursive = false, $filter = 'sort') { if (!$this->isDir($directory)) { throw new FtpException('"'.$directory.'" is not a directory'); } $files = $this->ftp->nlist($directory); if ($files === false) { throw new FtpException('Unable to list directory'); } $result = array(); $dir_len = strlen($directory); // if it's the current if (false !== ($kdot = array_search('.', $files))) { unset($files[$kdot]); } // if it's the parent if(false !== ($kdot = array_search('..', $files))) { unset($files[$kdot]); } if (!$recursive) { foreach ($files as $file) { $result[] = $directory.'/'.$file; } // working with the reference (behavior of several PHP sorting functions) $filter($result); return $result; } // utils for recursion $flatten = function (array $arr) use (&$flatten) { $flat = []; foreach ($arr as $k => $v) { if (is_array($v)) { $flat = array_merge($flat, $flatten($v)); } else { $flat[] = $v; } } return $flat; }; foreach ($files as $file) { $file = $directory.'/'.$file; // if contains the root path (behavior of the recursivity) if (0 === strpos($file, $directory, $dir_len)) { $file = substr($file, $dir_len); } if ($this->isDir($file)) { $result[] = $file; $items = $flatten($this->nlist($file, true, $filter)); foreach ($items as $item) { $result[] = $item; } } else { $result[] = $file; } } $result = array_unique($result); $filter($result); return $result; } /** * Creates a directory. * * @see FtpClient::rmdir() * @see FtpClient::remove() * @see FtpClient::put() * @see FtpClient::putAll() * * @param string $directory The directory * @param bool $recursive * @return array */ public function mkdir($directory, $recursive = false) { if (!$recursive or $this->isDir($directory)) { return $this->ftp->mkdir($directory); } $result = false; $pwd = $this->ftp->pwd(); $parts = explode('/', $directory); foreach ($parts as $part) { if (!@$this->ftp->chdir($part)) { $result = $this->ftp->mkdir($part); $this->ftp->chdir($part); } } $this->ftp->chdir($pwd); return $result; } /** * Remove a directory. * * @see FtpClient::mkdir() * @see FtpClient::cleanDir() * @see FtpClient::remove() * @see FtpClient::delete() * @param string $directory * @param bool $recursive Forces deletion if the directory is not empty * @return bool * @throws FtpException If unable to list the directory to remove */ public function rmdir($directory, $recursive = true) { if ($recursive) { $files = $this->nlist($directory, false, 'rsort'); // remove children foreach ($files as $file) { $this->remove($file, true); } } // remove the directory return $this->ftp->rmdir($directory); } /** * Empty directory. * * @see FtpClient::remove() * @see FtpClient::delete() * @see FtpClient::rmdir() * * @param string $directory * @return bool */ public function cleanDir($directory) { if(!$files = $this->nlist($directory)) { return $this->isEmpty($directory); } // remove children foreach ($files as $file) { $this->remove($file, true); } return $this->isEmpty($directory); } /** * Remove a file or a directory. * * @see FtpClient::rmdir() * @see FtpClient::cleanDir() * @see FtpClient::delete() * @param string $path The path of the file or directory to remove * @param bool $recursive Is effective only if $path is a directory, {@see FtpClient::rmdir()} * @return bool */ public function remove($path, $recursive = false) { try { if (@$this->ftp->delete($path) or ($this->isDir($path) and @$this->rmdir($path, $recursive))) { return true; } return false; } catch (\Exception $e) { return false; } } /** * Check if a directory exist. * * @param string $directory * @return bool * @throws FtpException */ public function isDir($directory) { $pwd = $this->ftp->pwd(); if ($pwd === false) { throw new FtpException('Unable to resolve the current directory'); } if (@$this->ftp->chdir($directory)) { $this->ftp->chdir($pwd); return true; } $this->ftp->chdir($pwd); return false; } /** * Check if a directory is empty. * * @param string $directory * @return bool */ public function isEmpty($directory) { return $this->count($directory, null, false) === 0 ? true : false; } /** * Scan a directory and returns the details of each item. * * @see FtpClient::nlist() * @see FtpClient::rawlist() * @see FtpClient::parseRawList() * @see FtpClient::dirSize() * @param string $directory * @param bool $recursive * @return array */ public function scanDir($directory = '.', $recursive = false) { return $this->parseRawList($this->rawlist($directory, $recursive)); } /** * Returns the total size of the given directory in bytes. * * @param string $directory The directory, by default is the current directory. * @param bool $recursive true by default * @return int The size in bytes. */ public function dirSize($directory = '.', $recursive = true) { $items = $this->scanDir($directory, $recursive); $size = 0; foreach ($items as $item) { $size += (int) $item['size']; } return $size; } /** * Count the items (file, directory, link, unknown). * * @param string $directory The directory, by default is the current directory. * @param string|null $type The type of item to count (file, directory, link, unknown) * @param bool $recursive true by default * @return int */ public function count($directory = '.', $type = null, $recursive = true) { $items = (null === $type ? $this->nlist($directory, $recursive) : $this->scanDir($directory, $recursive)); $count = 0; foreach ($items as $item) { if (null === $type or $item['type'] == $type) { $count++; } } return $count; } /** * Uploads a file to the server from a string. * * @param string $remote_file * @param string $content * @return FtpClient * @throws FtpException When the transfer fails */ public function putFromString($remote_file, $content) { $handle = fopen('php://temp', 'w'); fwrite($handle, $content); rewind($handle); if ($this->ftp->fput($remote_file, $handle, FTP_BINARY)) { return $this; } throw new FtpException('Unable to put the file "'.$remote_file.'"'); } /** * Uploads a file to the server. * * @param string $local_file * @return FtpClient * @throws FtpException When the transfer fails */ public function putFromPath($local_file) { $remote_file = basename($local_file); $handle = fopen($local_file, 'r'); if ($this->ftp->fput($remote_file, $handle, FTP_BINARY)) { rewind($handle); return $this; } throw new FtpException( 'Unable to put the remote file from the local file "'.$local_file.'"' ); } /** * Upload files. * * @param string $source_directory * @param string $target_directory * @param int $mode * @return FtpClient */ public function putAll($source_directory, $target_directory, $mode = FTP_BINARY) { $d = dir($source_directory); // do this for each file in the directory while ($file = $d->read()) { // to prevent an infinite loop if ($file != "." && $file != "..") { // do the following if it is a directory if (is_dir($source_directory.'/'.$file)) { if (!$this->isDir($target_directory.'/'.$file)) { // create directories that do not yet exist $this->ftp->mkdir($target_directory.'/'.$file); } // recursive part $this->putAll( $source_directory.'/'.$file, $target_directory.'/'.$file, $mode ); } else { // put the files $this->ftp->put( $target_directory.'/'.$file, $source_directory.'/'.$file, $mode ); } } } return $this; } /** * Returns a detailed list of files in the given directory. * * @see FtpClient::nlist() * @see FtpClient::scanDir() * @see FtpClient::dirSize() * @param string $directory The directory, by default is the current directory * @param bool $recursive * @return array * @throws FtpException */ public function rawlist($directory = '.', $recursive = false) { if (!$this->isDir($directory)) { throw new FtpException('"'.$directory.'" is not a directory.'); } $list = $this->ftp->rawlist($directory); $items = array(); if (!$list) { return $items; } if (false == $recursive) { foreach ($list as $path => $item) { $chunks = preg_split("/\s+/", $item); // if not "name" if (empty($chunks[8]) || $chunks[8] == '.' || $chunks[8] == '..') { continue; } $path = $directory.'/'.$chunks[8]; if (isset($chunks[9])) { $nbChunks = count($chunks); for ($i = 9; $i < $nbChunks; $i++) { $path .= ' '.$chunks[$i]; } } if (substr($path, 0, 2) == './') { $path = substr($path, 2); } $items[ $this->rawToType($item).'#'.$path ] = $item; } return $items; } $path = ''; foreach ($list as $item) { $len = strlen($item); if (!$len // "." || ($item[$len-1] == '.' && $item[$len-2] == ' ' // ".." or $item[$len-1] == '.' && $item[$len-2] == '.' && $item[$len-3] == ' ') ){ continue; } $chunks = preg_split("/\s+/", $item); // if not "name" if (empty($chunks[8]) || $chunks[8] == '.' || $chunks[8] == '..') { continue; } $path = $directory.'/'.$chunks[8]; if (isset($chunks[9])) { $nbChunks = count($chunks); for ($i = 9; $i < $nbChunks; $i++) { $path .= ' '.$chunks[$i]; } } if (substr($path, 0, 2) == './') { $path = substr($path, 2); } $items[$this->rawToType($item).'#'.$path] = $item; if ($item[0] == 'd') { $sublist = $this->rawlist($path, true); foreach ($sublist as $subpath => $subitem) { $items[$subpath] = $subitem; } } } return $items; } /** * Parse raw list. * * @see FtpClient::rawlist() * @see FtpClient::scanDir() * @see FtpClient::dirSize() * @param array $rawlist * @return array */ public function parseRawList(array $rawlist) { $items = array(); $path = ''; foreach ($rawlist as $key => $child) { $chunks = preg_split("/\s+/", $child); if (isset($chunks[8]) && ($chunks[8] == '.' or $chunks[8] == '..')) { continue; } if (count($chunks) === 1) { $len = strlen($chunks[0]); if ($len && $chunks[0][$len-1] == ':') { $path = substr($chunks[0], 0, -1); } continue; } $item = [ 'permissions' => $chunks[0], 'number' => $chunks[1], 'owner' => $chunks[2], 'group' => $chunks[3], 'size' => $chunks[4], 'month' => $chunks[5], 'day' => $chunks[6], 'time' => $chunks[7], 'name' => $chunks[8], 'type' => $this->rawToType($chunks[0]), ]; unset($chunks[0]); unset($chunks[1]); unset($chunks[2]); unset($chunks[3]); unset($chunks[4]); unset($chunks[5]); unset($chunks[6]); unset($chunks[7]); $item['name'] = implode(' ', $chunks); if ($item['type'] == 'link') { $item['target'] = $chunks[10]; // 9 is "->" } // if the key is not the path, behavior of ftp_rawlist() PHP function if (is_int($key) || false === strpos($key, $item['name'])) { array_splice($chunks, 0, 8); $key = $item['type'].'#' .($path ? $path.'/' : '') .implode(" ", $chunks); if ($item['type'] == 'link') { // get the first part of 'link#the-link.ext -> /path/of/the/source.ext' $exp = explode(' ->', $key); $key = rtrim($exp[0]); } $items[$key] = $item; } else { // the key is the path, behavior of FtpClient::rawlist() method() $items[$key] = $item; } } return $items; } /** * Convert raw info (drwx---r-x ...) to type (file, directory, link, unknown). * Only the first char is used for resolving. * * @param string $permission Example : drwx---r-x * * @return string The file type (file, directory, link, unknown) * @throws FtpException */ public function rawToType($permission) { if (!is_string($permission)) { throw new FtpException('The "$permission" argument must be a string, "' .gettype($permission).'" given.'); } if (empty($permission[0])) { return 'unknown'; } switch ($permission[0]) { case '-': return 'file'; case 'd': return 'directory'; case 'l': return 'link'; default: return 'unknown'; } } /** * Set the wrapper which forward the PHP FTP functions to use in FtpClient instance. * * @param FtpWrapper $wrapper * @return FtpClient */ protected function setWrapper(FtpWrapper $wrapper) { $this->ftp = $wrapper; return $this; } }