* Simple elFinder driver for FTP
* @author Dmitry (dio) Levashov
* @author Cem (discofever)
class elFinderVolumeFTP extends elFinderVolumeDriver
* Must be started from letter and contains [a-z0-9]
* Used as part of volume id
protected $driverId = 'f';
* FTP Connection Instance
* @var resource a FTP stream
protected $connect = null;
* Directory for tmp files
* If not set driver will try to use tmbDir as tmpDir
protected $ftpError = '';
* FTP server output list as ftp on linux
* FTP LIST command option
protected $ftpListOption = '-al';
* Is connected server Pure FTPd?
protected $isPureFtpd = false;
* Is connected server with FTPS?
protected $isFTPS = false;
* FTP command `MLST` support
private $MLSTsupprt = false;
* Calling cacheDir() target path with non-MLST
private $cacheDirTarget = '';
* Extend options with required fields
* @author Dmitry (dio) Levashov
* @author Cem (DiscoFever)
public function __construct()
'rootCssClass' => 'elfinder-navbar-root-ftp',
'ftpListOption' => '-al',
$this->options = array_merge($this->options, $opts);
$this->options['mimeDetect'] = 'internal';
* Call from elFinder::netmout() before volume->mount()
* @return array volume root options
public function netmountPrepare($options)
if (!empty($_REQUEST['encoding']) && iconv('UTF-8', $_REQUEST['encoding'], '') !== false) {
$options['encoding'] = $_REQUEST['encoding'];
if (!empty($_REQUEST['locale']) && setlocale(LC_ALL, $_REQUEST['locale'])) {
setlocale(LC_ALL, elFinder::$locale);
$options['locale'] = $_REQUEST['locale'];
if (!empty($_REQUEST['FTPS'])) {
$options['statOwner'] = true;
$options['allowChmodReadOnly'] = true;
$options['acceptedName'] = '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#';
/*********************************************************************/
/*********************************************************************/
* Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn
* @author Dmitry (dio) Levashov
* @author Cem (DiscoFever)
protected function init()
if (!$this->options['host']
|| !$this->options['port']) {
return $this->setError('Required options undefined.');
if (!$this->options['user']) {
$this->options['user'] = 'anonymous';
$this->options['pass'] = '';
if (!$this->options['path']) {
$this->options['path'] = '/';
$this->netMountKey = md5(join('-', array('ftp', $this->options['host'], $this->options['port'], $this->options['path'], $this->options['user'])));
if (!function_exists('ftp_connect')) {
return $this->setError('FTP extension not loaded.');
// remove protocol from host
$scheme = parse_url($this->options['host'], PHP_URL_SCHEME);
$this->options['host'] = substr($this->options['host'], strlen($scheme) + 3);
$this->root = $this->options['path'] = $this->_normpath($this->options['path']);
if (empty($this->options['alias'])) {
$this->options['alias'] = $this->options['user'] . '@' . $this->options['host'];
if (!empty($this->options['netkey'])) {
elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']);
$this->rootName = $this->options['alias'];
$this->options['separator'] = '/';
if (is_null($this->options['syncChkAsTs'])) {
$this->options['syncChkAsTs'] = true;
if (isset($this->options['ftpListOption'])) {
$this->ftpListOption = $this->options['ftpListOption'];
return $this->needOnline? $this->connect() : true;
* Configure after successfull mount.
* @throws elFinderAbortException
* @author Dmitry (dio) Levashov
protected function configure()
if (!empty($this->options['tmpPath'])) {
if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'], 0755, true)) && is_writable($this->options['tmpPath'])) {
$this->tmp = $this->options['tmpPath'];
if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) {
// fallback of $this->tmp
if (!$this->tmp && $this->tmbPathWritable) {
$this->tmp = $this->tmbPath;
$this->disabled[] = 'mkfile';
$this->disabled[] = 'paste';
$this->disabled[] = 'duplicate';
$this->disabled[] = 'upload';
$this->disabled[] = 'edit';
$this->disabled[] = 'archive';
$this->disabled[] = 'extract';
* @author Dmitry (dio) Levashov
protected function connect()
$withSSL = empty($this->options['ssl']) ? '' : ' with SSL';
if (!function_exists('ftp_ssl_connect') || !($this->connect = ftp_ssl_connect($this->options['host'], $this->options['port'], $this->options['timeout']))) {
return $this->setError('Unable to connect to FTP server ' . $this->options['host'] . $withSSL);
if (!($this->connect = ftp_connect($this->options['host'], $this->options['port'], $this->options['timeout']))) {
return $this->setError('Unable to connect to FTP server ' . $this->options['host']);
if (!ftp_login($this->connect, $this->options['user'], $this->options['pass'])) {
return $this->setError('Unable to login into ' . $this->options['host'] . $withSSL);
ftp_raw($this->connect, 'OPTS UTF8 OFF');
ftp_raw($this->connect, 'OPTS UTF8 ON');
$help = ftp_raw($this->connect, 'HELP');
$this->isPureFtpd = stripos(implode(' ', $help), 'Pure-FTPd') !== false;
if (!$this->isPureFtpd) {
// switch off extended passive mode - may be usefull for some servers
// this command, for pure-ftpd, doesn't work and takes a timeout in some pure-ftpd versions
ftp_raw($this->connect, 'epsv4 off');
// enter passive mode if required
$pasv = ($this->options['mode'] == 'passive');
if (!ftp_pasv($this->connect, $pasv)) {
$this->options['mode'] = 'active';
if (!ftp_chdir($this->connect, $this->root)
|| $this->root != ftp_pwd($this->connect)) {
return $this->setError('Unable to open root folder.');
// check for MLST support
$features = ftp_raw($this->connect, 'FEAT');
if (!is_array($features)) {
return $this->setError('Server does not support command FEAT.');
foreach ($features as $feat) {
if (strpos(trim($feat), 'MLST') === 0) {
$this->MLSTsupprt = true;
* Call ftp_rawlist with option prefix
protected function ftpRawList($path)
$path = str_replace(' ', '\ ', $path);
if ($this->ftpListOption) {
$path = $this->ftpListOption . ' ' . $path;
$res = ftp_rawlist($this->connect, $path);
/*********************************************************************/
/*********************************************************************/
* Close opened connection
* @author Dmitry (dio) Levashov
$this->connect && ftp_close($this->connect);
* Parse line from ftp_rawlist() output and return file stat (array)
* @param string $raw line from ftp_rawlist() output
* @author Dmitry Levashov
protected function parseRaw($raw, $base, $nameOnly = false)
$lastyear = date('Y') - 1;
$info = preg_split("/\s+/", $raw, 8);
list($info[7], $info[8]) = explode(' ', $info[7], 2);
if (!isset($this->ftpOsUnix)) {
$this->ftpOsUnix = !preg_match('/\d/', substr($info[0], 0, 1));
$info = $this->normalizeRawWindows($raw);
if (count($info) < 9 || $info[8] == '.' || $info[8] == '..') {
if (preg_match('|(.+)\-\>(.+)|', $name, $m)) {
// check recursive processing
if ($this->cacheDirTarget && $this->_joinPath($base, $name) !== $this->cacheDirTarget) {
if (substr($target, 0, 1) !== $this->separator) {
$target = $this->getFullPath($target, $base);
$target = $this->_normpath($target);
$stat['target'] = $target;
return array('name' => $name);
if (is_numeric($info[5]) && !$info[6] && !$info[7]) {
// by normalizeRawWindows()
$stat['ts'] = strtotime($info[5] . ' ' . $info[6] . ' ' . $info[7]);
if ($stat['ts'] && $stat['ts'] > $now && strpos($info[7], ':') !== false) {
$stat['ts'] = strtotime($info[5] . ' ' . $info[6] . ' ' . $lastyear . ' ' . $info[7]);
if (empty($stat['ts'])) {
$stat['ts'] = strtotime($info[6] . ' ' . $info[5] . ' ' . $info[7]);
if ($stat['ts'] && $stat['ts'] > $now && strpos($info[7], ':') !== false) {
$stat['ts'] = strtotime($info[6] . ' ' . $info[5] . ' ' . $lastyear . ' ' . $info[7]);
if ($this->options['statOwner']) {
$stat['owner'] = $info[2];
$stat['group'] = $info[3];
$stat['perm'] = substr($info[0], 1);
// if not exists owner in LS ftp ==> isowner = true
// if is defined as option : 'owner' => true isowner = true
// if exist owner in LS ftp and 'owner' => False isowner = result of owner(file) == user(logged with ftp)
$stat['isowner'] = isset($stat['owner']) ? ($this->options['owner'] ? true : ($stat['owner'] == $this->options['user'])) : true;
$owner_computed = isset($stat['isowner']) ? $stat['isowner'] : $this->options['owner'];
$perm = $this->parsePermissions($info[0], $owner_computed);
$stat['mime'] = substr(strtolower($info[0]), 0, 1) == 'd' ? 'directory' : $this->mimetype($stat['name'], true);
$stat['size'] = $stat['mime'] == 'directory' ? 0 : $info[4];
$stat['read'] = $perm['read'];
$stat['write'] = $perm['write'];
* Normalize MS-DOS style FTP LIST Raw line
* @param string $raw line from FTP LIST (MS-DOS style)
protected function normalizeRawWindows($raw)
$info = array_pad(array(), 9, '');
$item = preg_replace('#\s+#', ' ', trim($raw), 3);
list($date, $time, $size, $name) = explode(' ', $item, 4);
$format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
$dateObj = DateTime::createFromFormat($format, $date . $time);
$info[5] = strtotime($dateObj->format('Y-m-d H:i'));
* Parse permissions string. Return array(read => true/false, write => true/false)
* @param string $perm permissions string 'rwx' + 'rwx' + 'rwx'