first commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user