<?php /** * Akeeba Engine * The modular PHP5 site backup engine * * @copyright Copyright (c)2006-2017 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or, at your option, any later version * @package akeebaengine * */ namespace Akeeba\Engine\Util\Transfer; // Protection against direct access defined('AKEEBAENGINE') or die(); /** * FTP transfer object, using PHP as the transport backend */ class Ftp implements TransferInterface, RemoteResourceInterface { /** * FTP server's hostname or IP address * * @var string */ protected $host = 'localhost'; /** * FTP server's port, default: 21 * * @var integer */ protected $port = 21; /** * Username used to authenticate to the FTP server * * @var string */ protected $username = ''; /** * Password used to authenticate to the FTP server * * @var string */ protected $password = ''; /** * FTP initial directory * * @var string */ protected $directory = '/'; /** * Should I use SSL to connect to the server (FTP over explicit SSL, a.k.a. FTPS)? * * @var boolean */ protected $ssl = false; /** * Should I use FTP passive mode? * * @var bool */ protected $passive = true; /** * Timeout for connecting to the FTP server, default: 10 * * @var integer */ protected $timeout = 10; /** * The FTP connection handle * * @var resource|null */ private $connection = null; /** * Public constructor * * @param array $options Configuration options * * @return self * * @throws \RuntimeException */ public function __construct(array $options) { if (isset($options['host'])) { $this->host = $options['host']; } if (isset($options['port'])) { $this->port = (int)$options['port']; } if (isset($options['username'])) { $this->username = $options['username']; } if (isset($options['password'])) { $this->password = $options['password']; } if (isset($options['directory'])) { $this->directory = '/' . ltrim(trim($options['directory']), '/'); } if (isset($options['ssl'])) { $this->ssl = $options['ssl']; } if (isset($options['passive'])) { $this->passive = $options['passive']; } if (isset($options['timeout'])) { $this->timeout = max(1, (int) $options['timeout']); } $this->connect(); } /** * Save all parameters on serialization except the connection resource * * @return array */ public function __sleep() { return array('host', 'port', 'username', 'password', 'directory', 'ssl', 'passive', 'timeout'); } /** * Reconnect to the server on unserialize * * @return void */ public function __wakeup() { $this->connect(); } /** * Connect to the FTP server * * @throws \RuntimeException */ public function connect() { // Try to connect to the server if ($this->ssl) { if (function_exists('ftp_ssl_connect')) { $this->connection = @ftp_ssl_connect($this->host, $this->port); } else { $this->connection = false; throw new \RuntimeException('ftp_ssl_connect not available on this server', 500); } } else { $this->connection = @ftp_connect($this->host, $this->port, $this->timeout); } if ($this->connection === false) { throw new \RuntimeException(sprintf('Cannot connect to FTP server [host:port] = %s:%s', $this->host, $this->port), 500); } // Attempt to authenticate if (!@ftp_login($this->connection, $this->username, $this->password)) { @ftp_close($this->connection); $this->connection = null; throw new \RuntimeException(sprintf('Cannot log in to FTP server [username:password] = %s:%s', $this->username, $this->password), 500); } // Attempt to change to the initial directory if (!@ftp_chdir($this->connection, $this->directory)) { @ftp_close($this->connection); $this->connection = null; throw new \RuntimeException(sprintf('Cannot change to initial FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $this->directory), 500); } // Apply the passive mode preference @ftp_pasv($this->connection, $this->passive); } /** * Is this transfer method blocked by a server firewall? * * @param array $params Any additional parameters you might need to pass * * @return boolean True if the firewall blocks connections to a known host */ public static function isFirewalled(array $params = array()) { try { $connector = new static(array( 'host' => 'test.rebex.net', 'port' => 21, 'username' => 'demo', 'password' => 'password', 'directory' => '', 'ssl' => isset($params['ssl']) ? $params['ssl'] : false, 'passive' => true, 'timeout' => 5, )); $data = $connector->read('readme.txt'); if (empty($data)) { return true; } } catch (\Exception $e) { return true; } return false; } /** * Public destructor, closes any open FTP connections */ public function __destruct() { if (!is_null($this->connection)) { @ftp_close($this->connection); } } /** * Write the contents into the file * * @param string $fileName The full path to the file * @param string $contents The contents to write to the file * * @return boolean True on success */ public function write($fileName, $contents) { // Make sure the buffer:// wrapper is loaded class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); fwrite($handle, $contents); rewind($handle); $ret = @ftp_fput($this->connection, $fileName, $handle, FTP_BINARY); fclose($handle); return $ret; } /** * Uploads a local file to the remote storage * * @param string $localFilename The full path to the local file * @param string $remoteFilename The full path to the remote file * * @return boolean True on success */ public function upload($localFilename, $remoteFilename) { $handle = @fopen($localFilename, 'rb'); if ($handle === false) { throw new \RuntimeException("Unreadable local file $localFilename"); } $ret = @ftp_fput($this->connection, $remoteFilename, $handle, FTP_BINARY); @fclose($handle); return $ret; } /** * Read the contents of a remote file into a string * * @param string $fileName The full path to the remote file * * @return string The contents of the remote file */ public function read($fileName) { // Make sure the buffer:// wrapper is loaded class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); $result = @ftp_fget($this->connection, $handle, $fileName, FTP_BINARY); if ($result === false) { fclose($handle); throw new \RuntimeException("Can not download remote file $fileName"); } rewind($handle); $ret = ''; while (!feof($handle)) { $ret .= fread($handle, 131072); } fclose($handle); return $ret; } /** * Download a remote file into a local file * * @param string $remoteFilename * @param string $localFilename * * @return boolean True on success */ public function download($remoteFilename, $localFilename) { return @ftp_get($this->connection, $localFilename, $remoteFilename, FTP_BINARY); } /** * Delete a file (remove it from the disk) * * @param string $fileName The full path to the file * * @return boolean True on success */ public function delete($fileName) { return @ftp_delete($this->connection, $fileName); } /** * Create a copy of the file. Actually, we have to read it in memory and upload it again. * * @param string $from The full path of the file to copy from * @param string $to The full path of the file that will hold the copy * * @return boolean True on success */ public function copy($from, $to) { // Make sure the buffer:// wrapper is loaded class_exists('\\Akeeba\\Engine\\Util\\Buffer', true); $handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+'); $ret = @ftp_fget($this->connection, $handle, $from, FTP_BINARY); if ($ret !== false) { rewind($handle); $ret = @ftp_fput($this->connection, $to, $handle, FTP_BINARY); } fclose($handle); return $ret; } /** * Move or rename a file * * @param string $from The full path of the file to move * @param string $to The full path of the target file * * @return boolean True on success */ public function move($from, $to) { return @ftp_rename($this->connection, $from, $to); } /** * Change the permissions of a file * * @param string $fileName The full path of the file whose permissions will change * @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!) * * @return boolean True on success */ public function chmod($fileName, $permissions) { return (@ftp_chmod($this->connection, $permissions, $fileName) !== false); } /** * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all * intermediate directories if they do not already exist. * * @param string $dirName The full path of the directory to create * @param integer $permissions The permissions of the created directory * * @return boolean True on success */ public function mkdir($dirName, $permissions = 0755) { $targetDir = rtrim($dirName, '/'); $directories = explode('/', $targetDir); $remoteDir = ''; foreach ($directories as $dir) { if (!$dir) { continue; } $remoteDir .= '/' . $dir; // Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine if ($this->isDir($remoteDir)) { continue; } $ret = @ftp_mkdir($this->connection, $remoteDir); if ($ret === false) { return $ret; } } $this->chmod($dirName, $permissions); return true; } /** * Checks if the given directory exists * * @param string $path The full path of the remote directory to check * * @return boolean True if the directory exists */ public function isDir($path) { $cur_dir = ftp_pwd($this->connection); if (@ftp_chdir($this->connection, $path ) ) { // If it is a directory, then change the directory back to the original directory ftp_chdir($this->connection, $cur_dir); return true; } else { return false; } } /** * Get the current working directory * * @return string */ public function cwd() { return ftp_pwd($this->connection); } /** * Returns the absolute remote path from a path relative to the initial directory configured when creating the * transfer object. * * @param string $fileName The relative path of a file or directory * * @return string The absolute path for use by the transfer object */ public function getPath($fileName) { $fileName = str_replace('\\', '/', $fileName); if (strpos($fileName, $this->directory) === 0) { return $fileName; } $fileName = trim($fileName, '/'); $fileName = rtrim($this->directory, '/') . '/' . $fileName; return $fileName; } /** * Lists the subdirectories inside an FTP directory * * @param null|string $dir The directory to scan. Skip to use the current directory. * * @return array|bool A list of folders, or false if we could not get a listing * * @throws \RuntimeException When the server is incompatible with our FTP folder scanner */ public function listFolders($dir = null) { if (!@ftp_chdir($this->connection, $dir)) { throw new \RuntimeException(sprintf('Cannot change to FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500); } $list = @ftp_rawlist($this->connection, '.'); if ($list === false) { throw new \RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser."); } $folders = array(); foreach ($list as $v) { $vInfo = preg_split("/[\s]+/", $v, 9); if ($vInfo[0] !== "total") { $perms = $vInfo[0]; if (substr($perms,0,1) == 'd') { $folders[] = $vInfo[8]; } } } asort($folders); return $folders; } /** * Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP * functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents, * fopen etc. * * @param string $path * * @return string */ public function getWrapperStringFor($path) { $passwordEncoded = urlencode($this->password); $hostname = $this->host . ($this->port ? ":{$this->port}" : ''); $protocol = $this->ssl ? "ftps" : "ftp"; return "{$protocol}://{$this->username}:{$passwordEncoded}@{$hostname}{$path}"; } /** * Return the raw server listing for the requested folder. * * @param string $folder The path name to list * * @return string */ public function getRawList($folder) { return ftp_rawlist($this->connection, $folder); } }