first commit

This commit is contained in:
2026-02-08 21:16:11 +01:00
commit e17b7026fd
8881 changed files with 1160453 additions and 0 deletions

View File

@@ -0,0 +1,740 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Util\Transfer;
defined('AKEEBAENGINE') || die();
use Exception;
use RuntimeException;
/**
* 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 void
*
* @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();
}
/**
* 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 = [])
{
try
{
$connector = new static([
'host' => 'test.rebex.net',
'port' => 21,
'username' => 'demo',
'password' => 'password',
'directory' => '',
'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;
}
/**
* Save all parameters on serialization except the connection resource
*
* @return array
*/
public function __sleep()
{
return ['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
$defaultDir = @ftp_pwd($this->connection) ?: '/';
$directories = [
$this->directory,
rtrim($this->directory, '/'),
trim($this->directory, '/'),
$defaultDir,
];
foreach ($directories as $dir)
{
$changedDir = @ftp_chdir($this->connection, $dir);
if ($changedDir)
{
$this->directory = $dir;
break;
}
}
if (!$changedDir)
{
@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. Pro tip: the default directory of your FTP connection is reported to be %s', $this->directory, $defaultDir), 500);
}
// Apply the passive mode preference
@ftp_pasv($this->connection, $this->passive);
}
/**
* 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);
$cwd = $this->cwd();
$remoteFilename = '/' . ltrim($fileName, '/');
$remotePath = dirname($remoteFilename);
$remoteName = basename($remoteFilename);
if (!$this->isDir($remotePath))
{
$this->mkdir($remotePath);
}
$changedDir = @ftp_chdir($this->connection, $remotePath);
$ret = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);
if ($changedDir)
{
@ftp_chdir($this->connection, $cwd);
}
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
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function upload($localFilename, $remoteFilename, $useExceptions = true)
{
$handle = @fopen($localFilename, 'r');
if ($handle === false)
{
if ($useExceptions)
{
throw new RuntimeException("Unreadable local file $localFilename");
}
return false;
}
$cwd = $this->cwd();
$remoteFilename = '/' . ltrim($remoteFilename, '/');
$remotePath = dirname($remoteFilename);
$remoteName = basename($remoteFilename);
if (!$this->isDir($remotePath))
{
$this->mkdir($remotePath);
}
$changedDir = @ftp_chdir($this->connection, $remotePath);
$ret = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);
if ($changedDir)
{
@ftp_chdir($this->connection, $cwd);
}
@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+');
$cwd = $this->cwd();
$remoteFilename = '/' . ltrim($fileName, '/');
$remotePath = dirname($remoteFilename);
$remoteName = basename($remoteFilename);
$changedDir = @ftp_chdir($this->connection, $remotePath);
$result = $changedDir && @ftp_fget($this->connection, $handle, $remoteName, FTP_BINARY);
if ($changedDir)
{
@ftp_chdir($this->connection, $cwd);
}
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 The remote file path to download from
* @param string $localFilename The local file path to download to
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function download($remoteFilename, $localFilename, $useExceptions = true)
{
$cwd = $this->cwd();
$remoteFilename = '/' . ltrim($remoteFilename, '/');
$remotePath = dirname($remoteFilename);
$remoteName = basename($remoteFilename);
$changedDir = @ftp_chdir($this->connection, $remotePath);
$ret = $changedDir && @ftp_get($this->connection, $localFilename, $remoteName, FTP_BINARY);
if ($changedDir)
{
@ftp_chdir($this->connection, $cwd);
}
if (!$ret && $useExceptions)
{
throw new RuntimeException("Cannot download remote file $remoteFilename through FTP.");
}
return $ret;
}
/**
* 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)
{
$cwd = $this->cwd();
$remoteFilename = '/' . ltrim($fileName, '/');
$remotePath = dirname($remoteFilename);
$remoteName = basename($remoteFilename);
if (!$this->isDir($remotePath))
{
$this->mkdir($remotePath);
}
$changedDir = @ftp_chdir($this->connection, $remotePath);
$ret = $changedDir && @ftp_delete($this->connection, $remoteName);;
if ($changedDir)
{
ftp_chdir($this->connection, $cwd);
}
return $ret;
}
/**
* 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+');
$cwd = $this->cwd();
$remoteFilename = '/' . ltrim($from, '/');
$remotePath = dirname($remoteFilename);
$remoteName = basename($remoteFilename);
$changedDir = @ftp_chdir($this->connection, $remotePath);
$ret = $changedDir && @ftp_fget($this->connection, $handle, $remoteName, FTP_BINARY);
if ($ret !== false)
{
rewind($handle);
$remoteFilename = '/' . ltrim($to, '/');
$remotePath = dirname($remoteFilename);
$remoteName = basename($remoteFilename);
if (!$this->isDir($remotePath))
{
$this->mkdir($remotePath);
}
$changedDir = @ftp_chdir($this->connection, $remotePath);
$ret = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);
}
if ($changedDir)
{
@ftp_chdir($this->connection, $cwd);
}
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)
{
if (@ftp_chmod($this->connection, $permissions, $fileName) !== false)
{
return true;
}
$permissionsOctal = decoct((int) $permissions);
if (@ftp_site($this->connection, "CHMOD $permissionsOctal $fileName") !== false)
{
return true;
}
return 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 = [];
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)
{
$usernameEncoded = urlencode($this->username);
$passwordEncoded = urlencode($this->password);
$hostname = $this->host . ($this->port ? ":{$this->port}" : '');
$protocol = $this->ssl ? "ftps" : "ftp";
return "{$protocol}://{$usernameEncoded}:{$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);
}
}

View File

@@ -0,0 +1,831 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Util\Transfer;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Postproc\ProxyAware;
use RuntimeException;
/**
* FTP transfer object, using cURL as the transport backend
*/
class FtpCurl extends Ftp implements TransferInterface
{
use ProxyAware;
/**
* Timeout for transferring data to the FTP server, default: 10 minutes
*
* @var integer
*/
protected $timeout = 600;
/**
* Should I ignore the IP returned by the server during Passive mode transfers?
*
* @var bool
*
* @see http://www.elitehosts.com/blog/php-ftp-passive-ftp-server-behind-nat-nightmare/
*/
private $skipPassiveIP = true;
/**
* Should we enable verbose output to STDOUT? Useful for debugging.
*
* @var bool
*/
private $verbose = false;
/**
* Public constructor
*
* @param array $options Configuration options
*
* @throws RuntimeException
*/
public function __construct(array $options)
{
parent::__construct($options);
if (isset($options['passive_fix']))
{
$this->skipPassiveIP = $options['passive_fix'] ? true : false;
}
if (isset($options['verbose']))
{
$this->verbose = $options['verbose'] ? true : false;
}
}
/**
* Save all parameters on serialization except the connection resource
*
* @return array
*/
public function __sleep()
{
return [
'host',
'port',
'username',
'password',
'directory',
'ssl',
'passive',
'timeout',
'skipPassiveIP',
'verbose',
];
}
/**
* Test the connection to the FTP server and whether the initial directory is correct. This is done by attempting to
* list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check
* if we can upload files to that remote folder.
*
* @throws RuntimeException
*/
public function connect()
{
$ch = $this->getCurlHandle($this->directory . '/');
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_NOBODY, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_exec($ch);
$errNo = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errNo)
{
throw new RuntimeException("cURL Error $errNo connecting to remote FTP server: $error", 500);
}
}
/**
* 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_curl', 'r+');
fwrite($handle, $contents);
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
try
{
$this->uploadFromHandle($fileName, $handle);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function upload($localFilename, $remoteFilename, $useExceptions = true)
{
$fp = @fopen($localFilename, 'r');
if ($fp === false)
{
throw new RuntimeException("Unreadable local file $localFilename");
}
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
try
{
$this->uploadFromHandle($remoteFilename, $fp);
}
catch (RuntimeException $e)
{
if ($useExceptions)
{
throw $e;
}
return false;
}
return true;
}
/**
* 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)
{
try
{
return $this->downloadToString($fileName);
}
catch (RuntimeException $e)
{
throw new RuntimeException("Can not download remote file $fileName", 500, $e);
}
}
/**
* Download a remote file into a local file
*
* @param string $remoteFilename The remote file path to download from
* @param string $localFilename The local file path to download to
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function download($remoteFilename, $localFilename, $useExceptions = true)
{
$fp = @fopen($localFilename, 'w');
if ($fp === false)
{
if ($useExceptions)
{
throw new RuntimeException(sprintf('Download from FTP failed. Can not open local file %s for writing.', $localFilename));
}
return false;
}
// Note: don't manually close the file pointer, it's closed automatically by downloadToHandle
try
{
$this->downloadToHandle($remoteFilename, $fp);
}
catch (RuntimeException $e)
{
if ($useExceptions)
{
throw $e;
}
return false;
}
return true;
}
/**
* 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)
{
$commands = [
'DELE ' . $this->getPath($fileName),
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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+');
try
{
$this->downloadToHandle($from, $handle, false);
$this->uploadFromHandle($to, $handle);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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)
{
$from = $this->getPath($from);
$to = $this->getPath($to);
$commands = [
'RNFR /' . $from,
'RNTO /' . $to,
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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)
{
// Make sure permissions are in an octal string representation
if (!is_string($permissions))
{
$permissions = decoct($permissions);
}
$commands = [
'SITE CHMOD ' . $permissions . ' /' . $this->getPath($fileName),
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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;
}
$commands = [
'MKD ' . $remoteDir,
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
}
$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)
{
$ch = $this->getCurlHandle($path . '/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_exec($ch);
$errNo = curl_errno($ch);
curl_close($ch);
if ($errNo)
{
return false;
}
return true;
}
/**
* Get the current working directory. NOT IMPLEMENTED.
*
* @return string
*/
public function cwd()
{
$commands = [
'PWD',
];
try
{
$result = $this->executeServerCommands($commands, '');
}
catch (RuntimeException $e)
{
return '/';
}
return $result;
}
/**
* 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 (empty($dir))
{
$dir = $this->directory;
}
$dir = rtrim($dir, '/');
$ch = $this->getCurlHandle($dir . '/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$list = curl_exec($ch);
$errNo = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errNo)
{
throw new RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" make sure the folder exists and that you have adequate permissions to it", $dir), 500);
}
if (empty($list))
{
throw new RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser.");
}
$folders = [];
// Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters
$list = explode("\n", $list);
$list = array_map('rtrim', $list);
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;
}
/**
* Is the verbose debug option set?
*
* @return boolean
*/
public function isVerbose()
{
return $this->verbose;
}
/**
* Set the verbose debug option
*
* @param boolean $verbose
*
* @return void
*/
public function setVerbose($verbose)
{
$this->verbose = $verbose;
}
/**
* 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 = ltrim(str_replace('\\', '/', $fileName), '/');
$isInitialDirectory = $fileName === $this->directory;
$startsWithInitialDirectory = (strpos($fileName, rtrim($this->directory, '/') . '/') === 0)
|| (strpos($fileName, trim($this->directory, '/') . '/') === 0);
// Relative file? Add the initial directory
if (!$isInitialDirectory && !$startsWithInitialDirectory)
{
$fileName = '/' . trim($this->directory, '/') .
(empty($fileName) ? '' : '/') . $fileName;
}
return '/' . ltrim($fileName, '/');
}
/**
* Returns a cURL resource handler for the remote FTP server
*
* @param string $remoteFile Optional. The remote file / folder on the FTP server you'll be manipulating with cURL.
*
* @return resource
*/
protected function getCurlHandle($remoteFile = '')
{
/**
* Get the FTP URI
*
* VERY IMPORTANT! WE NEED THE DOUBLE SLASH AFTER THE HOST NAME since we are giving an absolute path.
* @see https://technicalsanctuary.wordpress.com/2012/11/01/curl-curl-9-server-denied-you-to-change-to-the-given-directory/
*/
$ftpUri = 'ftp://' . $this->host . '//';
$isInitialDirectory = $remoteFile === $this->directory;
$startsWithInitialDirectory = (strpos($remoteFile, rtrim($this->directory, '/') . '/') === 0)
|| (strpos($remoteFile, trim($this->directory, '/') . '/') === 0);
// Relative file? Add the initial directory
if (!$isInitialDirectory && !$startsWithInitialDirectory)
{
$ftpUri .= '/' . trim($this->directory, '/');
}
if (!empty($remoteFile) && substr($ftpUri, -2) !== '//')
{
$ftpUri .= '/';
}
// Add a remote file if necessary. The filename must be URL encoded since we're creating a URI.
if (!empty($remoteFile))
{
$suffix = '';
if (substr($remoteFile, -7, 6) == ';type=')
{
$suffix = substr($remoteFile, -7);
$remoteFile = substr($remoteFile, 0, -7);
}
$dirname = dirname($remoteFile);
// Windows messing up dirname('/'). KILL ME.
if ($dirname == '\\')
{
$dirname = '';
}
$dirname = trim($dirname, '/');
$basename = basename($remoteFile);
if ((substr($remoteFile, -1) == '/') && !empty($basename))
{
$suffix = '/' . $suffix;
}
$ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix;
}
// Colons in usernames must be URL escaped
$username = str_replace(':', '%3A', $this->username);
$ch = curl_init();
$this->applyProxySettingsToCurl($ch);
curl_setopt($ch, CURLOPT_URL, $ftpUri);
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $this->password);
curl_setopt($ch, CURLOPT_PORT, $this->port);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
// Should I enable Implict SSL?
if ($this->ssl)
{
curl_setopt($ch, CURLOPT_FTP_SSL, CURLFTPSSL_ALL);
curl_setopt($ch, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_DEFAULT);
// Most FTPS servers use self-signed certificates. That's the only way to connect to them :(
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
}
// Should I ignore the server-supplied passive mode IP address?
if ($this->passive && $this->skipPassiveIP)
{
curl_setopt($ch, CURLOPT_FTP_SKIP_PASV_IP, 1);
}
// Should I enable active mode?
if (!$this->passive)
{
/**
* cURL always uses passive mode for FTP transfers. Setting the CURLOPT_FTPPORT flag enables the FTP PORT
* command which makes the connection active. Setting it to '-' lets the library use your system's default
* IP address.
*
* @see https://curl.haxx.se/libcurl/c/CURLOPT_FTPPORT.html
*/
curl_setopt($ch, CURLOPT_FTPPORT, '-');
}
// Should I enable verbose output? Useful for debugging.
if ($this->verbose)
{
curl_setopt($ch, CURLOPT_VERBOSE, 1);
}
// Automatically create missing directories
curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);
return $ch;
}
/**
* Uploads a file using file contents provided through a file handle
*
* @param string $remoteFilename Remote file to write contents to
* @param resource $fp File or stream handler of the source data to upload
*
* @return void
*
* @throws RuntimeException
*/
protected function uploadFromHandle($remoteFilename, $fp)
{
// We need the file size. We can do that by getting the file position at EOF
fseek($fp, 0, SEEK_END);
$filesize = ftell($fp);
rewind($fp);
/**
* The ;type=i suffix forces Binary file transfer mode
*
* @see https://curl.haxx.se/mail/archive-2008-05/0089.html
*/
$ch = $this->getCurlHandle($remoteFilename . ';type=i');
curl_setopt($ch, CURLOPT_UPLOAD, 1);
curl_setopt($ch, CURLOPT_INFILE, $fp);
curl_setopt($ch, CURLOPT_INFILESIZE, $filesize);
curl_exec($ch);
$error_no = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
fclose($fp);
if ($error_no)
{
throw new RuntimeException($error, $error_no);
}
}
/**
* Downloads a remote file to the provided file handle
*
* @param string $remoteFilename Filename on the remote server
* @param resource $fp File handle where the downloaded content will be written to
* @param bool $close Optional. Should I close the file handle when I'm done? (Default: true)
*
* @return void
*
* @throws RuntimeException
*/
protected function downloadToHandle($remoteFilename, $fp, $close = true)
{
/**
* The ;type=i suffix forces Binary file transfer mode
*
* @see https://curl.haxx.se/mail/archive-2008-05/0089.html
*/
$ch = $this->getCurlHandle($remoteFilename . ';type=i');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_exec($ch);
$error_no = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($close)
{
fclose($fp);
}
if ($error_no)
{
throw new RuntimeException($error, $error_no);
}
}
/**
* Downloads a remote file and returns it as a string
*
* @param string $remoteFilename Filename on the remote server
*
* @return string
*
* @throws RuntimeException
*/
protected function downloadToString($remoteFilename)
{
/**
* The ;type=i suffix forces Binary file transfer mode
*
* @see https://curl.haxx.se/mail/archive-2008-05/0089.html
*/
$ch = $this->getCurlHandle($remoteFilename . ';type=i');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
$ret = curl_exec($ch);
$error_no = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error_no)
{
throw new RuntimeException($error, $error_no);
}
return $ret;
}
/**
* Executes arbitrary FTP commands
*
* @param string[] $commands An array with the FTP commands to be executed
* @param string|null $remoteFile The remote file / folder to use in the cURL URI when executing the commands
*
* @return string The output of the executed commands
*
*/
protected function executeServerCommands(array $commands, ?string $remoteFile = null): string
{
$remoteFile = $remoteFile ?? ($this->directory . '/');
$ch = $this->getCurlHandle($remoteFile);
curl_setopt($ch, CURLOPT_QUOTE, $commands);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_NOBODY, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$listing = curl_exec($ch);
$errNo = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errNo)
{
throw new RuntimeException($error, $errNo);
}
return $listing;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Util\Transfer;
defined('AKEEBAENGINE') || die();
/**
* An interface for Transfer adapters which support remote resources, allowing us to efficient read from / write to
* remote locations as if they were local files.
*/
interface RemoteResourceInterface
{
/**
* 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);
/**
* Return the raw server listing for the requested folder.
*
* @param string $folder The path name to list
*
* @return string
*/
public function getRawList($folder);
}

View File

@@ -0,0 +1,667 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Util\Transfer;
defined('AKEEBAENGINE') || die();
use DirectoryIterator;
use Exception;
use RuntimeException;
/**
* SFTP transfer object
*/
class Sftp implements TransferInterface, RemoteResourceInterface
{
/**
* SFTP server's hostname or IP address
*
* @var string
*/
private $host = 'localhost';
/**
* SFTP server's port, default: 21
*
* @var integer
*/
private $port = 22;
/**
* Username used to authenticate to the SFTP server
*
* @var string
*/
private $username = '';
/**
* Password used to authenticate to the SFTP server
*
* @var string
*/
private $password = '';
/**
* SFTP initial directory
*
* @var string
*/
private $directory = '/';
/**
* The absolute filesystem path to a private key file used for authentication instead of a password.
*
* @var string
*/
private $privateKey = '';
/**
* The absolute filesystem path to a public key file used for authentication instead of a password.
*
* @var string
*/
private $publicKey = '';
/**
* The SSH2 connection handle
*
* @var resource|null
*/
private $connection = null;
/**
* The SFTP connection handle
*
* @var resource|null
*/
private $sftpHandle = null;
/**
* Public constructor
*
* @param array $options Configuration options for the filesystem abstraction object
*
* @return Sftp
*
* @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['privateKey']))
{
$this->privateKey = $options['privateKey'];
}
if (isset($options['publicKey']))
{
$this->publicKey = $options['publicKey'];
}
$this->connect();
}
/**
* 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 = [])
{
try
{
$connector = new static([
'host' => 'test.rebex.net',
'port' => 22,
'username' => 'demo',
'password' => 'password',
'directory' => '',
]);
$data = $connector->read('readme.txt');
if (empty($data))
{
return true;
}
}
catch (Exception $e)
{
return true;
}
return false;
}
/**
* Save all parameters on serialization except the connection resource
*
* @return array
*/
public function __sleep()
{
return ['host', 'port', 'username', 'password', 'directory', 'privateKey', 'publicKey'];
}
/**
* Reconnect to the server on unserialize
*
* @return void
*/
public function __wakeup()
{
$this->connect();
}
public function __destruct()
{
if (is_resource($this->connection))
{
@ssh2_exec($this->connection, 'exit;');
$this->connection = null;
$this->sftpHandle = null;
}
}
/**
* Connect to the FTP server
*
* @throws RuntimeException
*/
public function connect()
{
// Try to connect to the SSH server
if (!function_exists('ssh2_connect'))
{
throw new RuntimeException('Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', 500);
}
$this->connection = ssh2_connect($this->host, $this->port);
if ($this->connection === false)
{
$this->connection = null;
throw new RuntimeException(sprintf('Cannot connect to SFTP server [host:port] = %s:%s', $this->host, $this->port), 500);
}
// Attempt to authenticate
if (!empty($this->publicKey) && !empty($this->privateKey))
{
if (!@ssh2_auth_pubkey_file($this->connection, $this->username, $this->publicKey, $this->privateKey, $this->password))
{
$this->connection = null;
throw new RuntimeException(sprintf('Cannot log in to SFTP server using key files [username:private_key_file:public_key_file:password] = %s:%s:%s:%s', $this->username, $this->privateKey, $this->publicKey, $this->password), 500);
}
}
else
{
if (!@ssh2_auth_password($this->connection, $this->username, $this->password))
{
$this->connection = null;
throw new RuntimeException(sprintf('Cannot log in to SFTP server [username:password] = %s:%s', $this->username, $this->password), 500);
}
}
// Get an SFTP handle
$this->sftpHandle = ssh2_sftp($this->connection);
if ($this->sftpHandle === false)
{
throw new RuntimeException('Cannot start an SFTP session with the server', 500);
}
}
/**
* 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)
{
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'w');
if ($fp === false)
{
return false;
}
$ret = @fwrite($fp, $contents);
@fclose($fp);
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
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function upload($localFilename, $remoteFilename, $useExceptions = true)
{
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'w');
if ($fp === false)
{
if ($useExceptions)
{
throw new RuntimeException("Could not open remote SFTP file $remoteFilename for writing");
}
return false;
}
$localFp = @fopen($localFilename, 'r');
if ($localFp === false)
{
fclose($fp);
if ($useExceptions)
{
throw new RuntimeException("Could not open local file $localFilename for reading");
}
return false;
}
while (!feof($localFp))
{
$data = fread($localFp, 131072);
$ret = @fwrite($fp, $data);
if ($ret < strlen($data))
{
fclose($fp);
fclose($localFp);
if ($useExceptions)
{
throw new RuntimeException("An error occurred while copying file $localFilename to $remoteFilename");
}
return false;
}
}
@fclose($fp);
@fclose($localFp);
return true;
}
/**
* 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)
{
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'r');
if ($fp === false)
{
throw new RuntimeException("Can not download remote file $fileName");
}
$ret = '';
while (!feof($fp))
{
$ret .= fread($fp, 131072);
}
@fclose($fp);
return $ret;
}
/**
* Download a remote file into a local file
*
* @param string $remoteFilename The remote file path to download from
* @param string $localFilename The local file path to download to
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function download($remoteFilename, $localFilename, $useExceptions = true)
{
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'r');
if ($fp === false)
{
if ($useExceptions)
{
throw new RuntimeException("Could not open remote SFTP file $remoteFilename for reading");
}
return false;
}
$localFp = @fopen($localFilename, 'w');
if ($localFp === false)
{
fclose($fp);
if ($useExceptions)
{
throw new RuntimeException("Could not open local file $localFilename for writing");
}
return false;
}
while (!feof($fp))
{
$chunk = fread($fp, 131072);
if ($chunk === false)
{
fclose($fp);
fclose($localFp);
if ($useExceptions)
{
throw new RuntimeException("An error occurred while copying file $remoteFilename to $localFilename");
}
return false;
}
fwrite($localFp, $chunk);
}
@fclose($fp);
@fclose($localFp);
return true;
}
/**
* 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)
{
try
{
$ret = @ssh2_sftp_unlink($this->sftpHandle, $fileName);
}
catch (Exception $e)
{
$ret = false;
}
return $ret;
}
/**
* 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)
{
$contents = @file_get_contents($from);
return $this->write($to, $contents);
}
/**
* Move or rename a file. Actually, we have to read it, upload it again and then delete the original.
*
* @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)
{
$ret = $this->copy($from, $to);
if ($ret)
{
$ret = $this->delete($from);
}
return $ret;
}
/**
* 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)
{
// Prefer the SFTP way, if available
if (function_exists('ssh2_sftp_chmod'))
{
return @ssh2_sftp_chmod($this->sftpHandle, $fileName, $permissions);
}
// Otherwise fall back to the (likely to fail) raw command mode
else
{
$cmd = 'chmod ' . decoct($permissions) . ' ' . escapeshellarg($fileName);
return @ssh2_exec($this->connection, $cmd);
}
}
/**
* 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, '/');
$ret = @ssh2_sftp_mkdir($this->sftpHandle, $targetDir, $permissions, true);
return $ret;
}
/**
* 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)
{
return @ssh2_sftp_stat($this->sftpHandle, $path);
}
/**
* Get the current working directory
*
* @return string
*/
public function cwd()
{
return ssh2_sftp_realpath($this->sftpHandle, ".");
}
/**
* 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);
$fileName = rtrim($this->directory, '/') . '/' . $fileName;
return $fileName;
}
/**
* Lists the subdirectories inside an SFTP 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 SFTP folder scanner
*/
public function listFolders($dir = null)
{
if (empty($dir))
{
$dir = $this->directory;
}
// Get a raw directory listing (hoping it's a UNIX server!)
$list = [];
$dir = ltrim($dir, '/');
try
{
$di = new DirectoryIterator("ssh2.sftp://" . $this->sftpHandle . "/$dir");
}
catch (Exception $e)
{
throw new RuntimeException(sprintf('Cannot change to SFTP directory "%s" make sure the folder exists and that you have adequate permissions to it', $dir), 500);
}
if (!$di->valid())
{
throw new RuntimeException(sprintf('Cannot change to SFTP directory "%s" make sure the folder exists and that you have adequate permissions to it', $dir), 500);
}
/** @var DirectoryIterator $entry */
foreach ($di as $entry)
{
if ($entry->isDot())
{
continue;
}
if (!$entry->isDir())
{
continue;
}
$list[] = $entry->getFilename();
}
unset($di);
if (!empty($list))
{
asort($list);
}
return $list;
}
/**
* 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)
{
return "ssh2.sftp://{$this->sftpHandle}{$path}";
}
/**
* Return the raw server listing for the requested folder.
*
* @param string $folder The path name to list
*
* @return string
*/
public function getRawList($folder)
{
// First try the command for Linxu servers
$res = $this->ssh2cmd('ls -l ' . escapeshellarg($folder));
// If an error occurred let's try the command for Windows servers
if (empty($res))
{
$res = $this->ssh2cmd('CMD /C ' . escapeshellarg($folder));
}
return $res;
}
private function ssh2cmd($command)
{
$stream = ssh2_exec($this->connection, $command);
stream_set_blocking($stream, true);
$res = @stream_get_contents($stream);
@fclose($stream);
return $res;
}
}

View File

@@ -0,0 +1,876 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Util\Transfer;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Postproc\ProxyAware;
use RuntimeException;
/**
* SFTP transfer object, using cURL as the transport backend
*/
class SftpCurl extends Sftp implements TransferInterface
{
use ProxyAware;
/**
* SFTP server's hostname or IP address
*
* @var string
*/
private $host = 'localhost';
/**
* SFTP server's port, default: 21
*
* @var integer
*/
private $port = 22;
/**
* Username used to authenticate to the SFTP server
*
* @var string
*/
private $username = '';
/**
* Password used to authenticate to the SFTP server
*
* @var string
*/
private $password = '';
/**
* SFTP initial directory
*
* @var string
*/
private $directory = '/';
/**
* The absolute filesystem path to a private key file used for authentication instead of a password.
*
* @var string
*/
private $privateKey = '';
/**
* The absolute filesystem path to a public key file used for authentication instead of a password.
*
* @var string
*/
private $publicKey = '';
/**
* Timeout for connecting to the SFTP server, default: 10 minutes
*
* @var integer
*/
private $timeout = 600;
/**
* Should we enable verbose output to STDOUT? Useful for debugging.
*
* @var bool
*/
private $verbose = false;
/**
* Should I enabled the passive IP workaround for cURL?
*
* @var bool
*/
private $skipPassiveIP = false;
/**
* 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['privateKey']))
{
$this->privateKey = $options['privateKey'];
}
if (isset($options['publicKey']))
{
$this->publicKey = $options['publicKey'];
}
if (isset($options['timeout']))
{
$this->timeout = max(1, (int) $options['timeout']);
}
if (isset($options['passive_fix']))
{
$this->skipPassiveIP = $options['passive_fix'] ? true : false;
}
if (isset($options['verbose']))
{
$this->verbose = $options['verbose'] ? true : false;
}
}
/**
* Save all parameters on serialization except the connection resource
*
* @return array
*/
public function __sleep()
{
return [
'host',
'port',
'username',
'password',
'directory',
'privateKey',
'publicKey',
'timeout',
'skipPassiveIP',
'verbose',
];
}
/**
* Test the connection to the SFTP server and whether the initial directory is correct. This is done by attempting to
* list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check
* if we can upload files to that remote folder.
*
* @throws RuntimeException
*/
public function connect()
{
$ch = $this->getCurlHandle($this->directory . '/');
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_NOBODY, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$listing = curl_exec($ch);
$errNo = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errNo)
{
throw new RuntimeException("cURL Error $errNo connecting to remote SFTP server: $error", 500);
}
}
/**
* 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_curl', 'r+');
fwrite($handle, $contents);
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
try
{
$this->uploadFromHandle($fileName, $handle);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function upload($localFilename, $remoteFilename, $useExceptions = true)
{
$fp = @fopen($localFilename, 'r');
if ($fp === false)
{
throw new RuntimeException("Unreadable local file $localFilename");
}
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
try
{
$this->uploadFromHandle($remoteFilename, $fp);
}
catch (RuntimeException $e)
{
if ($useExceptions)
{
throw $e;
}
return false;
}
return true;
}
/**
* 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)
{
try
{
return $this->downloadToString($fileName);
}
catch (RuntimeException $e)
{
throw new RuntimeException("Can not download remote file $fileName", 500, $e);
}
}
/**
* Download a remote file into a local file
*
* @param string $remoteFilename The remote file path to download from
* @param string $localFilename The local file path to download to
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function download($remoteFilename, $localFilename, $useExceptions = true)
{
$fp = @fopen($localFilename, 'w');
if ($fp === false)
{
if ($useExceptions)
{
throw new RuntimeException(sprintf('Download from FTP failed. Can not open local file %s for writing.', $localFilename));
}
return false;
}
// Note: don't manually close the file pointer, it's closed automatically by downloadToHandle
try
{
$this->downloadToHandle($remoteFilename, $fp);
}
catch (RuntimeException $e)
{
if ($useExceptions)
{
throw $e;
}
return false;
}
return true;
}
/**
* 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)
{
$commands = [
'rm ' . $this->getPath($fileName),
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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+');
try
{
$this->downloadToHandle($from, $handle, false);
$this->uploadFromHandle($to, $handle);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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)
{
$from = $this->getPath($from);
$to = $this->getPath($to);
$commands = [
'rename ' . $from . ' ' . $to,
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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)
{
// Make sure permissions are in an octal string representation
if (!is_string($permissions))
{
$permissions = decoct($permissions);
}
$commands = [
'chmod ' . $permissions . ' ' . $this->getPath($fileName),
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
return true;
}
/**
* 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;
}
$commands = [
'mkdir ' . $remoteDir,
];
try
{
$this->executeServerCommands($commands);
}
catch (RuntimeException $e)
{
return false;
}
}
$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)
{
$ch = $this->getCurlHandle($path . '/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$list = curl_exec($ch);
$errNo = curl_errno($ch);
curl_close($ch);
if ($errNo)
{
return false;
}
return true;
}
/**
* Get the current working directory. NOT IMPLEMENTED.
*
* @return string
*/
public function cwd()
{
return '';
}
/**
* 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 SFTP 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 SFTP folder scanner
*/
public function listFolders($dir = null)
{
if (empty($dir))
{
$dir = $this->directory;
}
$dir = rtrim($dir, '/');
$ch = $this->getCurlHandle($dir . '/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$list = curl_exec($ch);
$errNo = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errNo)
{
throw new RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" make sure the folder exists and that you have adequate permissions to it", $dir), 500);
}
if (empty($list))
{
throw new RuntimeException("Sorry, your SFTP server doesn't support our SFTP directory browser.");
}
$folders = [];
// Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters
$list = explode("\n", $list);
$list = array_map('rtrim', $list);
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;
}
/**
* Is the verbose debug option set?
*
* @return boolean
*/
public function isVerbose()
{
return $this->verbose;
}
/**
* Set the verbose debug option
*
* @param boolean $verbose
*
* @return void
*/
public function setVerbose($verbose)
{
$this->verbose = $verbose;
}
/**
* Returns a cURL resource handler for the remote SFTP server
*
* @param string $remoteFile Optional. The remote file / folder on the SFTP server you'll be manipulating with cURL.
*
* @return resource
*/
protected function getCurlHandle($remoteFile = '')
{
// Remember, the username has to be URL encoded as it's part of a URI!
$authentication = urlencode($this->username);
// We will only use username and password authentication if there are no certificates configured.
if (empty($this->publicKey))
{
// Remember, both the username and password have to be URL encoded as they're part of a URI!
$password = urlencode($this->password);
$authentication .= ':' . $password;
}
$ftpUri = 'sftp://' . $authentication . '@' . $this->host;
if (!empty($this->port))
{
$ftpUri .= ':' . (int) $this->port;
}
// Relative path? Append the initial directory.
if (substr($remoteFile, 0, 1) != '/')
{
$ftpUri .= $this->directory;
}
// Add a remote file if necessary. The filename must be URL encoded since we're creating a URI.
if (!empty($remoteFile))
{
$suffix = '';
$dirname = dirname($remoteFile);
// Windows messing up dirname('/'). KILL ME.
if ($dirname == '\\')
{
$dirname = '';
}
$dirname = trim($dirname, '/');
$basename = basename($remoteFile);
if ((substr($remoteFile, -1) == '/') && !empty($basename))
{
$suffix = '/' . $suffix;
}
$ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix;
}
$ch = curl_init();
$this->applyProxySettingsToCurl($ch);
curl_setopt($ch, CURLOPT_URL, $ftpUri);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
// Do I have to use certificate authentication?
if (!empty($this->publicKey))
{
// We always need to provide a public key file
curl_setopt($ch, CURLOPT_SSH_PUBLIC_KEYFILE, $this->publicKey);
// Since SSH certificates are self-signed we cannot have cURL verify their signatures against a CA.
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYSTATUS, 0);
/**
* This is optional because newer versions of cURL can extract the private key file from a combined
* certificate file.
*/
if (!empty($this->privateKey))
{
curl_setopt($ch, CURLOPT_SSH_PRIVATE_KEYFILE, $this->privateKey);
}
/**
* In case of encrypted (a.k.a. password protected) private key files you need to also specify the
* certificate decryption key in the password field. However, if libcurl is compiled against the GnuTLS
* library (instead of OpenSSL) this will NOT work because of bugs / missing features in GnuTLS. It's the
* same problem you get when libssh is compiled against GnuTLS. The solution to that is having an
* unencrypted private key file.
*/
if (!empty($this->password))
{
curl_setopt($ch, CURLOPT_KEYPASSWD, $this->password);
}
}
// Should I enable verbose output? Useful for debugging.
if ($this->verbose)
{
curl_setopt($ch, CURLOPT_VERBOSE, 1);
}
// Automatically create missing directories
curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);
return $ch;
}
/**
* Uploads a file using file contents provided through a file handle
*
* @param string $remoteFilename
* @param resource $fp
*
* @return void
*
* @throws RuntimeException
*/
protected function uploadFromHandle($remoteFilename, $fp)
{
// We need the file size. We can do that by getting the file position at EOF
fseek($fp, 0, SEEK_END);
$filesize = ftell($fp);
rewind($fp);
$ch = $this->getCurlHandle($remoteFilename);
curl_setopt($ch, CURLOPT_UPLOAD, 1);
curl_setopt($ch, CURLOPT_INFILE, $fp);
curl_setopt($ch, CURLOPT_INFILESIZE, $filesize);
curl_exec($ch);
$error_no = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
fclose($fp);
if ($error_no)
{
throw new RuntimeException($error, $error_no);
}
}
/**
* Downloads a remote file to the provided file handle
*
* @param string $remoteFilename Filename on the remote server
* @param resource $fp File handle where the downloaded content will be written to
* @param bool $close Optional. Should I close the file handle when I'm done? (Default: true)
*
* @return void
*
* @throws RuntimeException
*/
protected function downloadToHandle($remoteFilename, $fp, $close = true)
{
$ch = $this->getCurlHandle($remoteFilename);
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_exec($ch);
$error_no = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($close)
{
fclose($fp);
}
if ($error_no)
{
throw new RuntimeException($error, $error_no);
}
}
/**
* Downloads a remote file and returns it as a string
*
* @param string $remoteFilename Filename on the remote server
*
* @return string
*
* @throws RuntimeException
*/
protected function downloadToString($remoteFilename)
{
$ch = $this->getCurlHandle($remoteFilename);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
$ret = curl_exec($ch);
$error_no = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error_no)
{
throw new RuntimeException($error, $error_no);
}
return $ret;
}
/**
* Executes arbitrary SFTP commands
*
* @param array $commands An array with the SFTP commands to be executed
*
* @return string The output of the executed commands
*
* @throws RuntimeException
*/
protected function executeServerCommands($commands)
{
$ch = $this->getCurlHandle($this->directory . '/');
curl_setopt($ch, CURLOPT_QUOTE, $commands);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_NOBODY, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$listing = curl_exec($ch);
$errNo = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errNo)
{
throw new RuntimeException($error, $errNo);
}
return $listing;
}
}

View File

@@ -0,0 +1,167 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Util\Transfer;
defined('AKEEBAENGINE') || die();
use RuntimeException;
/**
* An interface for Transfer adapters, used to transfer files to remote servers over FTP, FTPS, SFTP and possibly other
* file transfer methods we might implement.
*
* @package Akeeba\Engine\Util\Transfer
*/
interface TransferInterface
{
/**
* Creates the uploader
*
* @param array $config
*/
public function __construct(array $config);
/**
* 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 = []);
/**
* Write the contents into the file
*
* @param string $fileName The full path to the remote file
* @param string $contents The contents to write to the file
*
* @return boolean True on success
*/
public function write($fileName, $contents);
/**
* 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
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function upload($localFilename, $remoteFilename, $useExceptions = true);
/**
* 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);
/**
* Download a remote file into a local file
*
* @param string $remoteFilename The remote file path to download from
* @param string $localFilename The local file path to download to
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
*
* @return boolean True on success
*/
public function download($remoteFilename, $localFilename, $useExceptions = true);
/**
* Delete a remote file
*
* @param string $fileName The full path to the remote file
*
* @return boolean True on success
*/
public function delete($fileName);
/**
* Create a copy of the remote file
*
* @param string $from The full path of the remote file to copy from
* @param string $to The full path of the remote file that will hold the copy
*
* @return boolean True on success
*/
public function copy($from, $to);
/**
* Move or rename a file
*
* @param string $from The full remote path of the file to move
* @param string $to The full remote path of the target file
*
* @return boolean True on success
*/
public function move($from, $to);
/**
* Change the permissions of a file
*
* @param string $fileName The full path of the remote 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);
/**
* 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 remote directory to create
* @param integer $permissions The permissions of the created directory
*
* @return boolean True on success
*/
public function mkdir($dirName, $permissions = 0755);
/**
* 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);
/**
* Get the current working directory
*
* @return string
*/
public function cwd();
/**
* 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);
/**
* Lists the subdirectories inside a 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 folder scanner
*/
public function listFolders($dir = null);
}