1<?php
2
3/**
4 * Simple elFinder driver for FTP
5 *
6 * @author Dmitry (dio) Levashov
7 * @author Cem (discofever)
8 **/
9class elFinderVolumeFTP extends elFinderVolumeDriver
10{
11
12    /**
13     * Driver id
14     * Must be started from letter and contains [a-z0-9]
15     * Used as part of volume id
16     *
17     * @var string
18     **/
19    protected $driverId = 'f';
20
21    /**
22     * FTP Connection Instance
23     *
24     * @var resource a FTP stream
25     **/
26    protected $connect = null;
27
28    /**
29     * Directory for tmp files
30     * If not set driver will try to use tmbDir as tmpDir
31     *
32     * @var string
33     **/
34    protected $tmpPath = '';
35
36    /**
37     * Last FTP error message
38     *
39     * @var string
40     **/
41    protected $ftpError = '';
42
43    /**
44     * FTP server output list as ftp on linux
45     *
46     * @var bool
47     **/
48    protected $ftpOsUnix;
49
50    /**
51     * FTP LIST command option
52     *
53     * @var string
54     */
55    protected $ftpListOption = '-al';
56
57
58    /**
59     * Is connected server Pure FTPd?
60     *
61     * @var bool
62     */
63    protected $isPureFtpd = false;
64
65    /**
66     * Is connected server with FTPS?
67     *
68     * @var bool
69     */
70    protected $isFTPS = false;
71
72    /**
73     * Tmp folder path
74     *
75     * @var string
76     **/
77    protected $tmp = '';
78
79    /**
80     * FTP command `MLST` support
81     *
82     * @var bool
83     */
84    private $MLSTsupprt = false;
85
86    /**
87     * Calling cacheDir() target path with non-MLST
88     *
89     * @var string
90     */
91    private $cacheDirTarget = '';
92
93    /**
94     * Constructor
95     * Extend options with required fields
96     *
97     * @author Dmitry (dio) Levashov
98     * @author Cem (DiscoFever)
99     */
100    public function __construct()
101    {
102        $opts = array(
103            'host' => 'localhost',
104            'user' => '',
105            'pass' => '',
106            'port' => 21,
107            'mode' => 'passive',
108            'ssl' => false,
109            'path' => '/',
110            'timeout' => 20,
111            'owner' => true,
112            'tmbPath' => '',
113            'tmpPath' => '',
114            'separator' => '/',
115            'checkSubfolders' => -1,
116            'dirMode' => 0755,
117            'fileMode' => 0644,
118            'rootCssClass' => 'elfinder-navbar-root-ftp',
119            'ftpListOption' => '-al',
120        );
121        $this->options = array_merge($this->options, $opts);
122        $this->options['mimeDetect'] = 'internal';
123    }
124
125    /**
126     * Prepare
127     * Call from elFinder::netmout() before volume->mount()
128     *
129     * @param $options
130     *
131     * @return array volume root options
132     * @author Naoki Sawada
133     */
134    public function netmountPrepare($options)
135    {
136        if (!empty($_REQUEST['encoding']) && iconv('UTF-8', $_REQUEST['encoding'], '') !== false) {
137            $options['encoding'] = $_REQUEST['encoding'];
138            if (!empty($_REQUEST['locale']) && setlocale(LC_ALL, $_REQUEST['locale'])) {
139                setlocale(LC_ALL, elFinder::$locale);
140                $options['locale'] = $_REQUEST['locale'];
141            }
142        }
143        if (!empty($_REQUEST['FTPS'])) {
144            $options['ssl'] = true;
145        }
146        $options['statOwner'] = true;
147        $options['allowChmodReadOnly'] = true;
148        $options['acceptedName'] = '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#';
149        return $options;
150    }
151
152    /*********************************************************************/
153    /*                        INIT AND CONFIGURE                         */
154    /*********************************************************************/
155
156    /**
157     * Prepare FTP connection
158     * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn
159     *
160     * @return bool
161     * @author Dmitry (dio) Levashov
162     * @author Cem (DiscoFever)
163     **/
164    protected function init()
165    {
166        if (!$this->options['host']
167            || !$this->options['port']) {
168            return $this->setError('Required options undefined.');
169        }
170
171        if (!$this->options['user']) {
172            $this->options['user'] = 'anonymous';
173            $this->options['pass'] = '';
174        }
175        if (!$this->options['path']) {
176            $this->options['path'] = '/';
177        }
178
179        // make ney mount key
180        $this->netMountKey = md5(join('-', array('ftp', $this->options['host'], $this->options['port'], $this->options['path'], $this->options['user'])));
181
182        if (!function_exists('ftp_connect')) {
183            return $this->setError('FTP extension not loaded.');
184        }
185
186        // remove protocol from host
187        $scheme = parse_url($this->options['host'], PHP_URL_SCHEME);
188
189        if ($scheme) {
190            $this->options['host'] = substr($this->options['host'], strlen($scheme) + 3);
191        }
192
193        // normalize root path
194        $this->root = $this->options['path'] = $this->_normpath($this->options['path']);
195
196        if (empty($this->options['alias'])) {
197            $this->options['alias'] = $this->options['user'] . '@' . $this->options['host'];
198            if (!empty($this->options['netkey'])) {
199                elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']);
200            }
201        }
202
203        $this->rootName = $this->options['alias'];
204        $this->options['separator'] = '/';
205
206        if (is_null($this->options['syncChkAsTs'])) {
207            $this->options['syncChkAsTs'] = true;
208        }
209
210        if (isset($this->options['ftpListOption'])) {
211            $this->ftpListOption = $this->options['ftpListOption'];
212        }
213
214        return $this->needOnline? $this->connect() : true;
215
216    }
217
218
219    /**
220     * Configure after successfull mount.
221     *
222     * @return void
223     * @throws elFinderAbortException
224     * @author Dmitry (dio) Levashov
225     */
226    protected function configure()
227    {
228        parent::configure();
229
230        if (!empty($this->options['tmpPath'])) {
231            if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'], 0755, true)) && is_writable($this->options['tmpPath'])) {
232                $this->tmp = $this->options['tmpPath'];
233            }
234        }
235        if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) {
236            $this->tmp = $tmp;
237        }
238
239        // fallback of $this->tmp
240        if (!$this->tmp && $this->tmbPathWritable) {
241            $this->tmp = $this->tmbPath;
242        }
243
244        if (!$this->tmp) {
245            $this->disabled[] = 'mkfile';
246            $this->disabled[] = 'paste';
247            $this->disabled[] = 'duplicate';
248            $this->disabled[] = 'upload';
249            $this->disabled[] = 'edit';
250            $this->disabled[] = 'archive';
251            $this->disabled[] = 'extract';
252        }
253
254        // echo $this->tmp;
255
256    }
257
258    /**
259     * Connect to ftp server
260     *
261     * @return bool
262     * @author Dmitry (dio) Levashov
263     **/
264    protected function connect()
265    {
266        $withSSL = empty($this->options['ssl']) ? '' : ' with SSL';
267        if ($withSSL) {
268            if (!function_exists('ftp_ssl_connect') || !($this->connect = ftp_ssl_connect($this->options['host'], $this->options['port'], $this->options['timeout']))) {
269                return $this->setError('Unable to connect to FTP server ' . $this->options['host'] . $withSSL);
270            }
271            $this->isFTPS = true;
272        } else {
273            if (!($this->connect = ftp_connect($this->options['host'], $this->options['port'], $this->options['timeout']))) {
274                return $this->setError('Unable to connect to FTP server ' . $this->options['host']);
275            }
276        }
277        if (!ftp_login($this->connect, $this->options['user'], $this->options['pass'])) {
278            $this->umount();
279            return $this->setError('Unable to login into ' . $this->options['host'] . $withSSL);
280        }
281
282        // try switch utf8 mode
283        if ($this->encoding) {
284            ftp_raw($this->connect, 'OPTS UTF8 OFF');
285        } else {
286            ftp_raw($this->connect, 'OPTS UTF8 ON');
287        }
288
289        $help = ftp_raw($this->connect, 'HELP');
290        $this->isPureFtpd = stripos(implode(' ', $help), 'Pure-FTPd') !== false;
291
292        if (!$this->isPureFtpd) {
293            // switch off extended passive mode - may be usefull for some servers
294            // this command, for pure-ftpd, doesn't work and takes a timeout in some pure-ftpd versions
295            ftp_raw($this->connect, 'epsv4 off');
296        }
297        // enter passive mode if required
298        $pasv = ($this->options['mode'] == 'passive');
299        if (!ftp_pasv($this->connect, $pasv)) {
300            if ($pasv) {
301                $this->options['mode'] = 'active';
302            }
303        }
304
305        // enter root folder
306        if (!ftp_chdir($this->connect, $this->root)
307            || $this->root != ftp_pwd($this->connect)) {
308            $this->umount();
309            return $this->setError('Unable to open root folder.');
310        }
311
312        // check for MLST support
313        $features = ftp_raw($this->connect, 'FEAT');
314        if (!is_array($features)) {
315            $this->umount();
316            return $this->setError('Server does not support command FEAT.');
317        }
318
319        foreach ($features as $feat) {
320            if (strpos(trim($feat), 'MLST') === 0) {
321                $this->MLSTsupprt = true;
322                break;
323            }
324        }
325
326        return true;
327    }
328
329    /**
330     * Call ftp_rawlist with option prefix
331     *
332     * @param string $path
333     *
334     * @return array
335     */
336    protected function ftpRawList($path)
337    {
338        if ($this->isPureFtpd) {
339            $path = str_replace(' ', '\ ', $path);
340        }
341        if ($this->ftpListOption) {
342            $path = $this->ftpListOption . ' ' . $path;
343        }
344        $res = ftp_rawlist($this->connect, $path);
345        if ($res === false) {
346            $res = array();
347        }
348        return $res;
349    }
350
351    /*********************************************************************/
352    /*                               FS API                              */
353    /*********************************************************************/
354
355    /**
356     * Close opened connection
357     *
358     * @return void
359     * @author Dmitry (dio) Levashov
360     **/
361    public function umount()
362    {
363        $this->connect && ftp_close($this->connect);
364    }
365
366
367    /**
368     * Parse line from ftp_rawlist() output and return file stat (array)
369     *
370     * @param  string $raw line from ftp_rawlist() output
371     * @param         $base
372     * @param bool    $nameOnly
373     *
374     * @return array
375     * @author Dmitry Levashov
376     */
377    protected function parseRaw($raw, $base, $nameOnly = false)
378    {
379        static $now;
380        static $lastyear;
381
382        if (!$now) {
383            $now = time();
384            $lastyear = date('Y') - 1;
385        }
386
387        $info = preg_split("/\s+/", $raw, 8);
388        if (isset($info[7])) {
389        	list($info[7], $info[8]) = explode(' ', $info[7], 2);
390        }
391        $stat = array();
392
393        if (!isset($this->ftpOsUnix)) {
394            $this->ftpOsUnix = !preg_match('/\d/', substr($info[0], 0, 1));
395        }
396        if (!$this->ftpOsUnix) {
397            $info = $this->normalizeRawWindows($raw);
398        }
399
400        if (count($info) < 9 || $info[8] == '.' || $info[8] == '..') {
401            return false;
402        }
403
404        $name = $info[8];
405
406        if (preg_match('|(.+)\-\>(.+)|', $name, $m)) {
407            $name = trim($m[1]);
408            // check recursive processing
409            if ($this->cacheDirTarget && $this->_joinPath($base, $name) !== $this->cacheDirTarget) {
410                return array();
411            }
412            if (!$nameOnly) {
413                $target = trim($m[2]);
414                if (substr($target, 0, 1) !== $this->separator) {
415                    $target = $this->getFullPath($target, $base);
416                }
417                $target = $this->_normpath($target);
418                $stat['name'] = $name;
419                $stat['target'] = $target;
420                return $stat;
421            }
422        }
423
424        if ($nameOnly) {
425            return array('name' => $name);
426        }
427
428        if (is_numeric($info[5]) && !$info[6] && !$info[7]) {
429            // by normalizeRawWindows()
430            $stat['ts'] = $info[5];
431        } else {
432            $stat['ts'] = strtotime($info[5] . ' ' . $info[6] . ' ' . $info[7]);
433            if ($stat['ts'] && $stat['ts'] > $now && strpos($info[7], ':') !== false) {
434                $stat['ts'] = strtotime($info[5] . ' ' . $info[6] . ' ' . $lastyear . ' ' . $info[7]);
435            }
436            if (empty($stat['ts'])) {
437                $stat['ts'] = strtotime($info[6] . ' ' . $info[5] . ' ' . $info[7]);
438                if ($stat['ts'] && $stat['ts'] > $now && strpos($info[7], ':') !== false) {
439                    $stat['ts'] = strtotime($info[6] . ' ' . $info[5] . ' ' . $lastyear . ' ' . $info[7]);
440                }
441            }
442        }
443
444        if ($this->options['statOwner']) {
445            $stat['owner'] = $info[2];
446            $stat['group'] = $info[3];
447            $stat['perm'] = substr($info[0], 1);
448            //
449            // if not exists owner in LS ftp ==>                    isowner = true
450            // if is defined as option : 'owner' => true            isowner = true
451            //
452            // if exist owner in LS ftp  and 'owner' => False       isowner =   result of    owner(file) == user(logged with ftp)
453            //
454            $stat['isowner'] = isset($stat['owner']) ? ($this->options['owner'] ? true : ($stat['owner'] == $this->options['user'])) : true;
455        }
456
457        $owner_computed = isset($stat['isowner']) ? $stat['isowner'] : $this->options['owner'];
458        $perm = $this->parsePermissions($info[0], $owner_computed);
459        $stat['name'] = $name;
460        $stat['mime'] = substr(strtolower($info[0]), 0, 1) == 'd' ? 'directory' : $this->mimetype($stat['name'], true);
461        $stat['size'] = $stat['mime'] == 'directory' ? 0 : $info[4];
462        $stat['read'] = $perm['read'];
463        $stat['write'] = $perm['write'];
464
465        return $stat;
466    }
467
468    /**
469     * Normalize MS-DOS style FTP LIST Raw line
470     *
471     * @param  string $raw line from FTP LIST (MS-DOS style)
472     *
473     * @return array
474     * @author Naoki Sawada
475     **/
476    protected function normalizeRawWindows($raw)
477    {
478        $info = array_pad(array(), 9, '');
479        $item = preg_replace('#\s+#', ' ', trim($raw), 3);
480        list($date, $time, $size, $name) = explode(' ', $item, 4);
481        $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
482        $dateObj = DateTime::createFromFormat($format, $date . $time);
483        $info[5] = strtotime($dateObj->format('Y-m-d H:i'));
484        $info[8] = $name;
485        if ($size === '<DIR>') {
486            $info[4] = 0;
487            $info[0] = 'drwxr-xr-x';
488        } else {
489            $info[4] = (int)$size;
490            $info[0] = '-rw-r--r--';
491        }
492        return $info;
493    }
494
495    /**
496     * Parse permissions string. Return array(read => true/false, write => true/false)
497     *
498     * @param  string $perm                        permissions string   'rwx' + 'rwx' + 'rwx'
499     *                                             ^       ^       ^
500     *                                             |       |       +->   others
501     *                                             |       +--------->   group
502     *                                             +----------------->   owner
503     *                                             The isowner parameter is computed by the caller.
504     *                                             If the owner parameter in the options is true, the user is the actual owner of all objects even if che user used in the ftp Login
505     *                                             is different from the file owner id.
506     *                                             If the owner parameter is false to understand if the user is the file owner we compare the ftp user with the file owner id.
507     * @param Boolean $isowner                     . Tell if the current user is the owner of the object.
508     *
509     * @return array
510     * @author Dmitry (dio) Levashov
511     * @author Ugo Vierucci
512     */
513    protected function parsePermissions($perm, $isowner = true)
514    {
515        $res = array();
516        $parts = array();
517        for ($i = 0, $l = strlen($perm); $i < $l; $i++) {
518            $parts[] = substr($perm, $i, 1);
519        }
520
521        $read = ($isowner && $parts[1] == 'r') || $parts[4] == 'r' || $parts[7] == 'r';
522
523        return array(
524            'read' => $parts[0] == 'd' ? $read && (($isowner && $parts[3] == 'x') || $parts[6] == 'x' || $parts[9] == 'x') : $read,
525            'write' => ($isowner && $parts[2] == 'w') || $parts[5] == 'w' || $parts[8] == 'w'
526        );
527    }
528
529    /**
530     * Cache dir contents
531     *
532     * @param  string $path dir path
533     *
534     * @return void
535     * @author Dmitry Levashov
536     **/
537    protected function cacheDir($path)
538    {
539        $this->dirsCache[$path] = array();
540        $hasDir = false;
541
542        $list = array();
543        $encPath = $this->convEncIn($path);
544        foreach ($this->ftpRawList($encPath) as $raw) {
545            if (($stat = $this->parseRaw($raw, $encPath))) {
546                $list[] = $stat;
547            }
548        }
549        $list = $this->convEncOut($list);
550        $prefix = ($path === $this->separator) ? $this->separator : $path . $this->separator;
551        $targets = array();
552        foreach ($list as $stat) {
553            $p = $prefix . $stat['name'];
554            if (isset($stat['target'])) {
555                // stat later
556                $targets[$stat['name']] = $stat['target'];
557            } else {
558                $stat = $this->updateCache($p, $stat);
559                if (empty($stat['hidden'])) {
560                    if (!$hasDir && $stat['mime'] === 'directory') {
561                        $hasDir = true;
562                    }
563                    $this->dirsCache[$path][] = $p;
564                }
565            }
566        }
567        // stat link targets
568        foreach ($targets as $name => $target) {
569            $stat = array();
570            $stat['name'] = $name;
571            $p = $prefix . $name;
572            $cacheDirTarget = $this->cacheDirTarget;
573            $this->cacheDirTarget = $this->convEncIn($target, true);
574            if ($tstat = $this->stat($target)) {
575                $stat['size'] = $tstat['size'];
576                $stat['alias'] = $target;
577                $stat['thash'] = $tstat['hash'];
578                $stat['mime'] = $tstat['mime'];
579                $stat['read'] = $tstat['read'];
580                $stat['write'] = $tstat['write'];
581
582                if (isset($tstat['ts'])) {
583                    $stat['ts'] = $tstat['ts'];
584                }
585                if (isset($tstat['owner'])) {
586                    $stat['owner'] = $tstat['owner'];
587                }
588                if (isset($tstat['group'])) {
589                    $stat['group'] = $tstat['group'];
590                }
591                if (isset($tstat['perm'])) {
592                    $stat['perm'] = $tstat['perm'];
593                }
594                if (isset($tstat['isowner'])) {
595                    $stat['isowner'] = $tstat['isowner'];
596                }
597            } else {
598
599                $stat['mime'] = 'symlink-broken';
600                $stat['read'] = false;
601                $stat['write'] = false;
602                $stat['size'] = 0;
603
604            }
605            $this->cacheDirTarget = $cacheDirTarget;
606            $stat = $this->updateCache($p, $stat);
607            if (empty($stat['hidden'])) {
608                if (!$hasDir && $stat['mime'] === 'directory') {
609                    $hasDir = true;
610                }
611                $this->dirsCache[$path][] = $p;
612            }
613        }
614
615        if (isset($this->sessionCache['subdirs'])) {
616            $this->sessionCache['subdirs'][$path] = $hasDir;
617        }
618    }
619
620    /**
621     * Return ftp transfer mode for file
622     *
623     * @param  string $path file path
624     *
625     * @return string
626     * @author Dmitry (dio) Levashov
627     **/
628    protected function ftpMode($path)
629    {
630        return strpos($this->mimetype($path), 'text/') === 0 ? FTP_ASCII : FTP_BINARY;
631    }
632
633    /*********************** paths/urls *************************/
634
635    /**
636     * Return parent directory path
637     *
638     * @param  string $path file path
639     *
640     * @return string
641     * @author Naoki Sawada
642     **/
643    protected function _dirname($path)
644    {
645        $parts = explode($this->separator, trim($path, $this->separator));
646        array_pop($parts);
647        return $this->separator . join($this->separator, $parts);
648    }
649
650    /**
651     * Return file name
652     *
653     * @param  string $path file path
654     *
655     * @return string
656     * @author Naoki Sawada
657     **/
658    protected function _basename($path)
659    {
660        $parts = explode($this->separator, trim($path, $this->separator));
661        return array_pop($parts);
662    }
663
664    /**
665     * Join dir name and file name and retur full path
666     *
667     * @param  string $dir
668     * @param  string $name
669     *
670     * @return string
671     * @author Dmitry (dio) Levashov
672     **/
673    protected function _joinPath($dir, $name)
674    {
675        return rtrim($dir, $this->separator) . $this->separator . $name;
676    }
677
678    /**
679     * Return normalized path, this works the same as os.path.normpath() in Python
680     *
681     * @param  string $path path
682     *
683     * @return string
684     * @author Troex Nevelin
685     **/
686    protected function _normpath($path)
687    {
688        if (empty($path)) {
689            $path = '.';
690        }
691        // path must be start with /
692        $path = preg_replace('|^\.\/?|', $this->separator, $path);
693        $path = preg_replace('/^([^\/])/', "/$1", $path);
694
695        if ($path[0] === $this->separator) {
696            $initial_slashes = true;
697        } else {
698            $initial_slashes = false;
699        }
700
701        if (($initial_slashes)
702            && (strpos($path, '//') === 0)
703            && (strpos($path, '///') === false)) {
704            $initial_slashes = 2;
705        }
706
707        $initial_slashes = (int)$initial_slashes;
708
709        $comps = explode($this->separator, $path);
710        $new_comps = array();
711        foreach ($comps as $comp) {
712            if (in_array($comp, array('', '.'))) {
713                continue;
714            }
715
716            if (($comp != '..')
717                || (!$initial_slashes && !$new_comps)
718                || ($new_comps && (end($new_comps) == '..'))) {
719                array_push($new_comps, $comp);
720            } elseif ($new_comps) {
721                array_pop($new_comps);
722            }
723        }
724        $comps = $new_comps;
725        $path = implode($this->separator, $comps);
726        if ($initial_slashes) {
727            $path = str_repeat($this->separator, $initial_slashes) . $path;
728        }
729
730        return $path ? $path : '.';
731    }
732
733    /**
734     * Return file path related to root dir
735     *
736     * @param  string $path file path
737     *
738     * @return string
739     * @author Dmitry (dio) Levashov
740     **/
741    protected function _relpath($path)
742    {
743        if ($path === $this->root) {
744            return '';
745        } else {
746            if (strpos($path, $this->root) === 0) {
747                return ltrim(substr($path, strlen($this->root)), $this->separator);
748            } else {
749                // for link
750                return $path;
751            }
752        }
753    }
754
755    /**
756     * Convert path related to root dir into real path
757     *
758     * @param  string $path file path
759     *
760     * @return string
761     * @author Dmitry (dio) Levashov
762     **/
763    protected function _abspath($path)
764    {
765        if ($path === $this->separator) {
766            return $this->root;
767        } else {
768            if ($path[0] === $this->separator) {
769                // for link
770                return $path;
771            } else {
772                return $this->_joinPath($this->root, $path);
773            }
774        }
775    }
776
777    /**
778     * Return fake path started from root dir
779     *
780     * @param  string $path file path
781     *
782     * @return string
783     * @author Dmitry (dio) Levashov
784     **/
785    protected function _path($path)
786    {
787        return $this->rootName . ($path == $this->root ? '' : $this->separator . $this->_relpath($path));
788    }
789
790    /**
791     * Return true if $path is children of $parent
792     *
793     * @param  string $path   path to check
794     * @param  string $parent parent path
795     *
796     * @return bool
797     * @author Dmitry (dio) Levashov
798     **/
799    protected function _inpath($path, $parent)
800    {
801        return $path == $parent || strpos($path, rtrim($parent, $this->separator) . $this->separator) === 0;
802    }
803
804    /***************** file stat ********************/
805    /**
806     * Return stat for given path.
807     * Stat contains following fields:
808     * - (int)    size    file size in b. required
809     * - (int)    ts      file modification time in unix time. required
810     * - (string) mime    mimetype. required for folders, others - optionally
811     * - (bool)   read    read permissions. required
812     * - (bool)   write   write permissions. required
813     * - (bool)   locked  is object locked. optionally
814     * - (bool)   hidden  is object hidden. optionally
815     * - (string) alias   for symlinks - link target path relative to root path. optionally
816     * - (string) target  for symlinks - link target path. optionally
817     * If file does not exists - returns empty array or false.
818     *
819     * @param  string $path file path
820     *
821     * @return array|false
822     * @author Dmitry (dio) Levashov
823     **/
824    protected function _stat($path)
825    {
826        $outPath = $this->convEncOut($path);
827        if (isset($this->cache[$outPath])) {
828            return $this->convEncIn($this->cache[$outPath]);
829        } else {
830            $this->convEncIn();
831        }
832        if (!$this->MLSTsupprt) {
833            if ($path === $this->root) {
834                $res = array(
835                    'name' => $this->root,
836                    'mime' => 'directory',
837                    'dirs' => -1
838                );
839                if ($this->needOnline && (($this->ARGS['cmd'] === 'open' && $this->ARGS['target'] === $this->encode($this->root)) || $this->isMyReload())) {
840                    $check = array(
841                        'ts' => true,
842                        'dirs' => true,
843                    );
844                    $ts = 0;
845                    foreach ($this->ftpRawList($path) as $str) {
846                        $info = preg_split('/\s+/', $str, 9);
847                        if ($info[8] === '.') {
848                            $info[8] = 'root';
849                            if ($stat = $this->parseRaw(join(' ', $info), $path)) {
850                                unset($stat['name']);
851                                $res = array_merge($res, $stat);
852                                if ($res['ts']) {
853                                    $ts = 0;
854                                    unset($check['ts']);
855                                }
856                            }
857                        }
858                        if ($check && ($stat = $this->parseRaw($str, $path))) {
859                            if (isset($stat['ts']) && !empty($stat['ts'])) {
860                                $ts = max($ts, $stat['ts']);
861                            }
862                            if (isset($stat['dirs']) && $stat['mime'] === 'directory') {
863                                $res['dirs'] = 1;
864                                unset($stat['dirs']);
865                            }
866                            if (!$check) {
867                                break;
868                            }
869                        }
870                    }
871                    if ($ts) {
872                        $res['ts'] = $ts;
873                    }
874                    $this->cache[$outPath] = $res;
875                }
876                return $res;
877            }
878
879            $pPath = $this->_dirname($path);
880            if ($this->_inPath($pPath, $this->root)) {
881                $outPPpath = $this->convEncOut($pPath);
882                if (!isset($this->dirsCache[$outPPpath])) {
883                    $parentSubdirs = null;
884                    if (isset($this->sessionCache['subdirs']) && isset($this->sessionCache['subdirs'][$outPPpath])) {
885                        $parentSubdirs = $this->sessionCache['subdirs'][$outPPpath];
886                    }
887                    $this->cacheDir($outPPpath);
888                    if ($parentSubdirs) {
889                        $this->sessionCache['subdirs'][$outPPpath] = $parentSubdirs;
890                    }
891                }
892            }
893
894            $stat = $this->convEncIn(isset($this->cache[$outPath]) ? $this->cache[$outPath] : array());
895            if (!$this->mounted) {
896                // dispose incomplete cache made by calling `stat` by 'startPath' option
897                $this->cache = array();
898            }
899            return $stat;
900        }
901        $raw = ftp_raw($this->connect, 'MLST ' . $path);
902        if (is_array($raw) && count($raw) > 1 && substr(trim($raw[0]), 0, 1) == 2) {
903            $parts = explode(';', trim($raw[1]));
904            array_pop($parts);
905            $parts = array_map('strtolower', $parts);
906            $stat = array();
907            $mode = '';
908            foreach ($parts as $part) {
909
910                list($key, $val) = explode('=', $part, 2);
911
912                switch ($key) {
913                    case 'type':
914                        if (strpos($val, 'dir') !== false) {
915                            $stat['mime'] = 'directory';
916                        } else if (strpos($val, 'link') !== false) {
917                            $stat['mime'] = 'symlink';
918                            break(2);
919                        } else {
920                            $stat['mime'] = $this->mimetype($path);
921                        }
922                        break;
923
924                    case 'size':
925                        $stat['size'] = $val;
926                        break;
927
928                    case 'modify':
929                        $ts = mktime(intval(substr($val, 8, 2)), intval(substr($val, 10, 2)), intval(substr($val, 12, 2)), intval(substr($val, 4, 2)), intval(substr($val, 6, 2)), substr($val, 0, 4));
930                        $stat['ts'] = $ts;
931                        break;
932
933                    case 'unix.mode':
934                        $mode = strval($val);
935                        break;
936
937                    case 'unix.uid':
938                        $stat['owner'] = $val;
939                        break;
940
941                    case 'unix.gid':
942                        $stat['group'] = $val;
943                        break;
944
945                    case 'perm':
946                        $val = strtolower($val);
947                        $stat['read'] = (int)preg_match('/e|l|r/', $val);
948                        $stat['write'] = (int)preg_match('/w|m|c/', $val);
949                        if (!preg_match('/f|d/', $val)) {
950                            $stat['locked'] = 1;
951                        }
952                        break;
953                }
954            }
955
956            if (empty($stat['mime'])) {
957                return array();
958            }
959
960            // do not use MLST to get stat of symlink
961            if ($stat['mime'] === 'symlink') {
962                $this->MLSTsupprt = false;
963                $res = $this->_stat($path);
964                $this->MLSTsupprt = true;
965                return $res;
966            }
967
968            if ($stat['mime'] === 'directory') {
969                $stat['size'] = 0;
970            }
971
972            if ($mode) {
973                $stat['perm'] = '';
974                if ($mode[0] === '0') {
975                    $mode = substr($mode, 1);
976                }
977
978                $perm = array();
979                for ($i = 0; $i <= 2; $i++) {
980                    $perm[$i] = array(false, false, false);
981                    $n = isset($mode[$i]) ? $mode[$i] : 0;
982
983                    if ($n - 4 >= 0) {
984                        $perm[$i][0] = true;
985                        $n = $n - 4;
986                        $stat['perm'] .= 'r';
987                    } else {
988                        $stat['perm'] .= '-';
989                    }
990
991                    if ($n - 2 >= 0) {
992                        $perm[$i][1] = true;
993                        $n = $n - 2;
994                        $stat['perm'] .= 'w';
995                    } else {
996                        $stat['perm'] .= '-';
997                    }
998
999                    if ($n - 1 == 0) {
1000                        $perm[$i][2] = true;
1001                        $stat['perm'] .= 'x';
1002                    } else {
1003                        $stat['perm'] .= '-';
1004                    }
1005                }
1006
1007                $stat['perm'] = trim($stat['perm']);
1008                //
1009                // if not exists owner in LS ftp ==>                    isowner = true
1010                // if is defined as option : 'owner' => true            isowner = true
1011                //
1012                // if exist owner in LS ftp  and 'owner' => False        isowner =   result of    owner(file) == user(logged with ftp)
1013
1014                $owner_computed = isset($stat['owner']) ? ($this->options['owner'] ? true : ($stat['owner'] == $this->options['user'])) : true;
1015
1016                $read = ($owner_computed && $perm[0][0]) || $perm[1][0] || $perm[2][0];
1017
1018                $stat['read'] = $stat['mime'] == 'directory' ? $read && (($owner_computed && $perm[0][2]) || $perm[1][2] || $perm[2][2]) : $read;
1019                $stat['write'] = ($owner_computed && $perm[0][1]) || $perm[1][1] || $perm[2][1];
1020
1021                if ($this->options['statOwner']) {
1022                    $stat['isowner'] = $owner_computed;
1023                } else {
1024                    unset($stat['owner'], $stat['group'], $stat['perm']);
1025                }
1026            }
1027
1028            return $stat;
1029
1030        }
1031
1032        return array();
1033    }
1034
1035    /**
1036     * Return true if path is dir and has at least one childs directory
1037     *
1038     * @param  string $path dir path
1039     *
1040     * @return bool
1041     * @author Dmitry (dio) Levashov
1042     **/
1043    protected function _subdirs($path)
1044    {
1045
1046        foreach ($this->ftpRawList($path) as $str) {
1047            $info = preg_split('/\s+/', $str, 9);
1048            if (!isset($this->ftpOsUnix)) {
1049                $this->ftpOsUnix = !preg_match('/\d/', substr($info[0], 0, 1));
1050            }
1051            if (!$this->ftpOsUnix) {
1052                $info = $this->normalizeRawWindows($str);
1053            }
1054            $name = isset($info[8]) ? trim($info[8]) : '';
1055            if ($name && $name !== '.' && $name !== '..' && substr(strtolower($info[0]), 0, 1) === 'd') {
1056                return true;
1057            }
1058        }
1059        return false;
1060    }
1061
1062    /**
1063     * Return object width and height
1064     * Ususaly used for images, but can be realize for video etc...
1065     *
1066     * @param  string $path file path
1067     * @param  string $mime file mime type
1068     *
1069     * @return string|false
1070     * @throws ImagickException
1071     * @throws elFinderAbortException
1072     * @author Dmitry (dio) Levashov
1073     */
1074    protected function _dimensions($path, $mime)
1075    {
1076        $ret = false;
1077        if ($imgsize = $this->getImageSize($path, $mime)) {
1078            $ret = array('dim' => $imgsize['dimensions']);
1079            if (!empty($imgsize['url'])) {
1080                $ret['url'] = $imgsize['url'];
1081            }
1082        }
1083        return $ret;
1084    }
1085
1086    /******************** file/dir content *********************/
1087
1088    /**
1089     * Return files list in directory.
1090     *
1091     * @param  string $path dir path
1092     *
1093     * @return array
1094     * @author Dmitry (dio) Levashov
1095     * @author Cem (DiscoFever)
1096     **/
1097    protected function _scandir($path)
1098    {
1099        $files = array();
1100
1101        foreach ($this->ftpRawList($path) as $str) {
1102            if (($stat = $this->parseRaw($str, $path, true))) {
1103                $files[] = $this->_joinPath($path, $stat['name']);
1104            }
1105        }
1106
1107        return $files;
1108    }
1109
1110    /**
1111     * Open file and return file pointer
1112     *
1113     * @param  string $path file path
1114     * @param string  $mode
1115     *
1116     * @return false|resource
1117     * @throws elFinderAbortException
1118     * @internal param bool $write open file for writing
1119     * @author   Dmitry (dio) Levashov
1120     */
1121    protected function _fopen($path, $mode = 'rb')
1122    {
1123        // try ftp stream wrapper
1124        if ($this->options['mode'] === 'passive' && ini_get('allow_url_fopen')) {
1125            $url = ($this->isFTPS ? 'ftps' : 'ftp') . '://' . $this->options['user'] . ':' . $this->options['pass'] . '@' . $this->options['host'] . ':' . $this->options['port'] . $path;
1126            if (strtolower($mode[0]) === 'w') {
1127                $context = stream_context_create(array('ftp' => array('overwrite' => true)));
1128                $fp = fopen($url, $mode, false, $context);
1129            } else {
1130                $fp = fopen($url, $mode);
1131            }
1132            if ($fp) {
1133                return $fp;
1134            }
1135        }
1136
1137        if ($this->tmp) {
1138            $local = $this->getTempFile($path);
1139            $fp = fopen($local, 'wb');
1140            $ret = ftp_nb_fget($this->connect, $fp, $path, FTP_BINARY);
1141            while ($ret === FTP_MOREDATA) {
1142                elFinder::extendTimeLimit();
1143                $ret = ftp_nb_continue($this->connect);
1144            }
1145            if ($ret === FTP_FINISHED) {
1146                fclose($fp);
1147                $fp = fopen($local, $mode);
1148                return $fp;
1149            }
1150            fclose($fp);
1151            is_file($local) && unlink($local);
1152        }
1153
1154        return false;
1155    }
1156
1157    /**
1158     * Close opened file
1159     *
1160     * @param  resource $fp file pointer
1161     * @param string    $path
1162     *
1163     * @return void
1164     * @author Dmitry (dio) Levashov
1165     */
1166    protected function _fclose($fp, $path = '')
1167    {
1168        is_resource($fp) && fclose($fp);
1169        if ($path) {
1170            unlink($this->getTempFile($path));
1171        }
1172    }
1173
1174    /********************  file/dir manipulations *************************/
1175
1176    /**
1177     * Create dir and return created dir path or false on failed
1178     *
1179     * @param  string $path parent dir path
1180     * @param string  $name new directory name
1181     *
1182     * @return string|bool
1183     * @author Dmitry (dio) Levashov
1184     **/
1185    protected function _mkdir($path, $name)
1186    {
1187        $path = $this->_joinPath($path, $name);
1188        if (ftp_mkdir($this->connect, $path) === false) {
1189            return false;
1190        }
1191
1192        $this->options['dirMode'] && ftp_chmod($this->connect, $this->options['dirMode'], $path);
1193        return $path;
1194    }
1195
1196    /**
1197     * Create file and return it's path or false on failed
1198     *
1199     * @param  string $path parent dir path
1200     * @param string  $name new file name
1201     *
1202     * @return string|bool
1203     * @author Dmitry (dio) Levashov
1204     **/
1205    protected function _mkfile($path, $name)
1206    {
1207        if ($this->tmp) {
1208            $path = $this->_joinPath($path, $name);
1209            $local = $this->getTempFile();
1210            $res = touch($local) && ftp_put($this->connect, $path, $local, FTP_ASCII);
1211            unlink($local);
1212            return $res ? $path : false;
1213        }
1214        return false;
1215    }
1216
1217    /**
1218     * Create symlink. FTP driver does not support symlinks.
1219     *
1220     * @param  string $target link target
1221     * @param  string $path   symlink path
1222     * @param string  $name
1223     *
1224     * @return bool
1225     * @author Dmitry (dio) Levashov
1226     */
1227    protected function _symlink($target, $path, $name)
1228    {
1229        return false;
1230    }
1231
1232    /**
1233     * Copy file into another file
1234     *
1235     * @param  string $source    source file path
1236     * @param  string $targetDir target directory path
1237     * @param  string $name      new file name
1238     *
1239     * @return bool
1240     * @author Dmitry (dio) Levashov
1241     **/
1242    protected function _copy($source, $targetDir, $name)
1243    {
1244        $res = false;
1245
1246        if ($this->tmp) {
1247            $local = $this->getTempFile();
1248            $target = $this->_joinPath($targetDir, $name);
1249
1250            if (ftp_get($this->connect, $local, $source, FTP_BINARY)
1251                && ftp_put($this->connect, $target, $local, $this->ftpMode($target))) {
1252                $res = $target;
1253            }
1254            unlink($local);
1255        }
1256
1257        return $res;
1258    }
1259
1260    /**
1261     * Move file into another parent dir.
1262     * Return new file path or false.
1263     *
1264     * @param  string $source source file path
1265     * @param         $targetDir
1266     * @param  string $name   file name
1267     *
1268     * @return bool|string
1269     * @internal param string $target target dir path
1270     * @author   Dmitry (dio) Levashov
1271     */
1272    protected function _move($source, $targetDir, $name)
1273    {
1274        $target = $this->_joinPath($targetDir, $name);
1275        return ftp_rename($this->connect, $source, $target) ? $target : false;
1276    }
1277
1278    /**
1279     * Remove file
1280     *
1281     * @param  string $path file path
1282     *
1283     * @return bool
1284     * @author Dmitry (dio) Levashov
1285     **/
1286    protected function _unlink($path)
1287    {
1288        return ftp_delete($this->connect, $path);
1289    }
1290
1291    /**
1292     * Remove dir
1293     *
1294     * @param  string $path dir path
1295     *
1296     * @return bool
1297     * @author Dmitry (dio) Levashov
1298     **/
1299    protected function _rmdir($path)
1300    {
1301        return ftp_rmdir($this->connect, $path);
1302    }
1303
1304    /**
1305     * Create new file and write into it from file pointer.
1306     * Return new file path or false on error.
1307     *
1308     * @param  resource $fp   file pointer
1309     * @param  string   $dir  target dir path
1310     * @param  string   $name file name
1311     * @param  array    $stat file stat (required by some virtual fs)
1312     *
1313     * @return bool|string
1314     * @author Dmitry (dio) Levashov
1315     **/
1316    protected function _save($fp, $dir, $name, $stat)
1317    {
1318        $path = $this->_joinPath($dir, $name);
1319        return ftp_fput($this->connect, $path, $fp, $this->ftpMode($path))
1320            ? $path
1321            : false;
1322    }
1323
1324    /**
1325     * Get file contents
1326     *
1327     * @param  string $path file path
1328     *
1329     * @return string|false
1330     * @throws elFinderAbortException
1331     * @author Dmitry (dio) Levashov
1332     */
1333    protected function _getContents($path)
1334    {
1335        $contents = '';
1336        if (($fp = $this->_fopen($path))) {
1337            while (!feof($fp)) {
1338                $contents .= fread($fp, 8192);
1339            }
1340            $this->_fclose($fp, $path);
1341            return $contents;
1342        }
1343        return false;
1344    }
1345
1346    /**
1347     * Write a string to a file
1348     *
1349     * @param  string $path    file path
1350     * @param  string $content new file content
1351     *
1352     * @return bool
1353     * @author Dmitry (dio) Levashov
1354     **/
1355    protected function _filePutContents($path, $content)
1356    {
1357        $res = false;
1358
1359        if ($this->tmp) {
1360            $local = $this->getTempFile();
1361
1362            if (file_put_contents($local, $content, LOCK_EX) !== false
1363                && ($fp = fopen($local, 'rb'))) {
1364                $file = $this->stat($this->convEncOut($path, false));
1365                if (!empty($file['thash'])) {
1366                    $path = $this->decode($file['thash']);
1367                }
1368                clearstatcache();
1369                $res = ftp_fput($this->connect, $path, $fp, $this->ftpMode($path));
1370                fclose($fp);
1371            }
1372            file_exists($local) && unlink($local);
1373        }
1374
1375        return $res;
1376    }
1377
1378    /**
1379     * Detect available archivers
1380     *
1381     * @return void
1382     * @throws elFinderAbortException
1383     */
1384    protected function _checkArchivers()
1385    {
1386        $this->archivers = $this->getArchivers();
1387        return;
1388    }
1389
1390    /**
1391     * chmod availability
1392     *
1393     * @param string $path
1394     * @param string $mode
1395     *
1396     * @return bool
1397     */
1398    protected function _chmod($path, $mode)
1399    {
1400        $modeOct = is_string($mode) ? octdec($mode) : octdec(sprintf("%04o", $mode));
1401        return ftp_chmod($this->connect, $modeOct, $path);
1402    }
1403
1404    /**
1405     * Extract files from archive
1406     *
1407     * @param  string $path archive path
1408     * @param  array  $arc  archiver command and arguments (same as in $this->archivers)
1409     *
1410     * @return true
1411     * @throws elFinderAbortException
1412     * @author Dmitry (dio) Levashov,
1413     * @author Alexey Sukhotin
1414     */
1415    protected function _extract($path, $arc)
1416    {
1417        $dir = $this->tempDir();
1418        if (!$dir) {
1419            return false;
1420        }
1421
1422        $basename = $this->_basename($path);
1423        $localPath = $dir . DIRECTORY_SEPARATOR . $basename;
1424
1425        if (!ftp_get($this->connect, $localPath, $path, FTP_BINARY)) {
1426            //cleanup
1427            $this->rmdirRecursive($dir);
1428            return false;
1429        }
1430
1431        $this->unpackArchive($localPath, $arc);
1432
1433        $this->archiveSize = 0;
1434
1435        // find symlinks and check extracted items
1436        $checkRes = $this->checkExtractItems($dir);
1437        if ($checkRes['symlinks']) {
1438            $this->rmdirRecursive($dir);
1439            return $this->setError(array_merge($this->error, array(elFinder::ERROR_ARC_SYMLINKS)));
1440        }
1441        $this->archiveSize = $checkRes['totalSize'];
1442        if ($checkRes['rmNames']) {
1443            foreach ($checkRes['rmNames'] as $name) {
1444                $this->addError(elFinder::ERROR_SAVE, $name);
1445            }
1446        }
1447
1448        $filesToProcess = self::listFilesInDirectory($dir, true);
1449
1450        // no files - extract error ?
1451        if (empty($filesToProcess)) {
1452            $this->rmdirRecursive($dir);
1453            return false;
1454        }
1455
1456        // check max files size
1457        if ($this->options['maxArcFilesSize'] > 0 && $this->options['maxArcFilesSize'] < $this->archiveSize) {
1458            $this->rmdirRecursive($dir);
1459            return $this->setError(elFinder::ERROR_ARC_MAXSIZE);
1460        }
1461
1462        $extractTo = $this->extractToNewdir; // 'auto', ture or false
1463
1464        // archive contains one item - extract in archive dir
1465        $name = '';
1466        $src = $dir . DIRECTORY_SEPARATOR . $filesToProcess[0];
1467        if (($extractTo === 'auto' || !$extractTo) && count($filesToProcess) === 1 && is_file($src)) {
1468            $name = $filesToProcess[0];
1469        } else if ($extractTo === 'auto' || $extractTo) {
1470            // for several files - create new directory
1471            // create unique name for directory
1472            $src = $dir;
1473            $splits = elFinder::splitFileExtention(basename($path));
1474            $name = $splits[0];
1475            $test = $this->_joinPath(dirname($path), $name);
1476            if ($this->stat($test)) {
1477                $name = $this->uniqueName(dirname($path), $name, '-', false);
1478            }
1479        }
1480
1481        if ($name !== '' && is_file($src)) {
1482            $result = $this->_joinPath(dirname($path), $name);
1483
1484            if (!ftp_put($this->connect, $result, $src, FTP_BINARY)) {
1485                $this->rmdirRecursive($dir);
1486                return false;
1487            }
1488        } else {
1489            $dstDir = $this->_dirname($path);
1490            $result = array();
1491            if (is_dir($src) && $name) {
1492                $target = $this->_joinPath($dstDir, $name);
1493                $_stat = $this->_stat($target);
1494                if ($_stat) {
1495                    if (!$this->options['copyJoin']) {
1496                        if ($_stat['mime'] === 'directory') {
1497                            $this->delTree($target);
1498                        } else {
1499                            $this->_unlink($target);
1500                        }
1501                        $_stat = false;
1502                    } else {
1503                        $dstDir = $target;
1504                    }
1505                }
1506                if (!$_stat && (!$dstDir = $this->_mkdir($dstDir, $name))) {
1507                    $this->rmdirRecursive($dir);
1508                    return false;
1509                }
1510                $result[] = $dstDir;
1511            }
1512            foreach ($filesToProcess as $name) {
1513                $name = rtrim($name, DIRECTORY_SEPARATOR);
1514                $src = $dir . DIRECTORY_SEPARATOR . $name;
1515                if (is_dir($src)) {
1516                    $p = dirname($name);
1517                    if ($p === '.') {
1518                        $p = '';
1519                    }
1520                    $name = basename($name);
1521                    $target = $this->_joinPath($this->_joinPath($dstDir, $p), $name);
1522                    $_stat = $this->_stat($target);
1523                    if ($_stat) {
1524                        if (!$this->options['copyJoin']) {
1525                            if ($_stat['mime'] === 'directory') {
1526                                $this->delTree($target);
1527                            } else {
1528                                $this->_unlink($target);
1529                            }
1530                            $_stat = false;
1531                        }
1532                    }
1533                    if (!$_stat && (!$target = $this->_mkdir($this->_joinPath($dstDir, $p), $name))) {
1534                        $this->rmdirRecursive($dir);
1535                        return false;
1536                    }
1537                } else {
1538                    $target = $this->_joinPath($dstDir, $name);
1539                    if (!ftp_put($this->connect, $target, $src, FTP_BINARY)) {
1540                        $this->rmdirRecursive($dir);
1541                        return false;
1542                    }
1543                }
1544                $result[] = $target;
1545            }
1546            if (!$result) {
1547                $this->rmdirRecursive($dir);
1548                return false;
1549            }
1550        }
1551
1552        is_dir($dir) && $this->rmdirRecursive($dir);
1553
1554        $this->clearcache();
1555        return $result ? $result : false;
1556    }
1557
1558    /**
1559     * Create archive and return its path
1560     *
1561     * @param  string $dir   target dir
1562     * @param  array  $files files names list
1563     * @param  string $name  archive name
1564     * @param  array  $arc   archiver options
1565     *
1566     * @return string|bool
1567     * @throws elFinderAbortException
1568     * @author Dmitry (dio) Levashov,
1569     * @author Alexey Sukhotin
1570     */
1571    protected function _archive($dir, $files, $name, $arc)
1572    {
1573        // get current directory
1574        $cwd = getcwd();
1575
1576        $tmpDir = $this->tempDir();
1577        if (!$tmpDir) {
1578            return false;
1579        }
1580
1581        //download data
1582        if (!$this->ftp_download_files($dir, $files, $tmpDir)) {
1583            //cleanup
1584            $this->rmdirRecursive($tmpDir);
1585            return false;
1586        }
1587
1588        $remoteArchiveFile = false;
1589        if ($path = $this->makeArchive($tmpDir, $files, $name, $arc)) {
1590            $remoteArchiveFile = $this->_joinPath($dir, $name);
1591            if (!ftp_put($this->connect, $remoteArchiveFile, $path, FTP_BINARY)) {
1592                $remoteArchiveFile = false;
1593            }
1594        }
1595
1596        //cleanup
1597        if (!$this->rmdirRecursive($tmpDir)) {
1598            return false;
1599        }
1600
1601        return $remoteArchiveFile;
1602    }
1603
1604    /**
1605     * Create writable temporary directory and return path to it.
1606     *
1607     * @return string path to the new temporary directory or false in case of error.
1608     */
1609    private function tempDir()
1610    {
1611        $tempPath = tempnam($this->tmp, 'elFinder');
1612        if (!$tempPath) {
1613            $this->setError(elFinder::ERROR_CREATING_TEMP_DIR, $this->tmp);
1614            return false;
1615        }
1616        $success = unlink($tempPath);
1617        if (!$success) {
1618            $this->setError(elFinder::ERROR_CREATING_TEMP_DIR, $this->tmp);
1619            return false;
1620        }
1621        $success = mkdir($tempPath, 0700, true);
1622        if (!$success) {
1623            $this->setError(elFinder::ERROR_CREATING_TEMP_DIR, $this->tmp);
1624            return false;
1625        }
1626        return $tempPath;
1627    }
1628
1629    /**
1630     * Gets an array of absolute remote FTP paths of files and
1631     * folders in $remote_directory omitting symbolic links.
1632     *
1633     * @param $remote_directory string remote FTP path to scan for file and folders recursively
1634     * @param $targets          array  Array of target item. `null` is to get all of items
1635     *
1636     * @return array of elements each of which is an array of two elements:
1637     * <ul>
1638     * <li>$item['path'] - absolute remote FTP path</li>
1639     * <li>$item['type'] - either 'f' for file or 'd' for directory</li>
1640     * </ul>
1641     */
1642    protected function ftp_scan_dir($remote_directory, $targets = null)
1643    {
1644        $buff = $this->ftpRawList($remote_directory);
1645        $items = array();
1646        if ($targets && is_array($targets)) {
1647            $targets = array_flip($targets);
1648        } else {
1649            $targets = false;
1650        }
1651        foreach ($buff as $str) {
1652            $info = preg_split("/\s+/", $str, 9);
1653            if (!isset($this->ftpOsUnix)) {
1654                $this->ftpOsUnix = !preg_match('/\d/', substr($info[0], 0, 1));
1655            }
1656            if (!$this->ftpOsUnix) {
1657                $info = $this->normalizeRawWindows($str);
1658            }
1659            $type = substr($info[0], 0, 1);
1660            $name = trim($info[8]);
1661            if ($name !== '.' && $name !== '..' && (!$targets || isset($targets[$name]))) {
1662                switch ($type) {
1663                    case 'l' : //omit symbolic links
1664                    case 'd' :
1665                        $remote_file_path = $this->_joinPath($remote_directory, $name);
1666                        $item = array();
1667                        $item['path'] = $remote_file_path;
1668                        $item['type'] = 'd'; // normal file
1669                        $items[] = $item;
1670                        $items = array_merge($items, $this->ftp_scan_dir($remote_file_path));
1671                        break;
1672                    default:
1673                        $remote_file_path = $this->_joinPath($remote_directory, $name);
1674                        $item = array();
1675                        $item['path'] = $remote_file_path;
1676                        $item['type'] = 'f'; // normal file
1677                        $items[] = $item;
1678                }
1679            }
1680        }
1681        return $items;
1682    }
1683
1684    /**
1685     * Downloads specified files from remote directory
1686     * if there is a directory among files it is downloaded recursively (omitting symbolic links).
1687     *
1688     * @param       $remote_directory     string remote FTP path to a source directory to download from.
1689     * @param array $files                list of files to download from remote directory.
1690     * @param       $dest_local_directory string destination folder to store downloaded files.
1691     *
1692     * @return bool true on success and false on failure.
1693     */
1694    private function ftp_download_files($remote_directory, array $files, $dest_local_directory)
1695    {
1696        $contents = $this->ftp_scan_dir($remote_directory, $files);
1697        if (!isset($contents)) {
1698            $this->setError(elFinder::ERROR_FTP_DOWNLOAD_FILE, $remote_directory);
1699            return false;
1700        }
1701        $remoteDirLen = strlen($remote_directory);
1702        foreach ($contents as $item) {
1703            $relative_path = substr($item['path'], $remoteDirLen);
1704            $local_path = $dest_local_directory . DIRECTORY_SEPARATOR . $relative_path;
1705            switch ($item['type']) {
1706                case 'd':
1707                    $success = mkdir($local_path);
1708                    break;
1709                case 'f':
1710                    $success = ftp_get($this->connect, $local_path, $item['path'], FTP_BINARY);
1711                    break;
1712                default:
1713                    $success = true;
1714            }
1715            if (!$success) {
1716                $this->setError(elFinder::ERROR_FTP_DOWNLOAD_FILE, $remote_directory);
1717                return false;
1718            }
1719        }
1720        return true;
1721    }
1722
1723    /**
1724     * Delete local directory recursively.
1725     *
1726     * @param $dirPath string to directory to be erased.
1727     *
1728     * @return bool true on success and false on failure.
1729     * @throws Exception
1730     */
1731    private function deleteDir($dirPath)
1732    {
1733        if (!is_dir($dirPath)) {
1734            $success = unlink($dirPath);
1735        } else {
1736            $success = true;
1737            foreach (array_reverse(elFinderVolumeFTP::listFilesInDirectory($dirPath, false)) as $path) {
1738                $path = $dirPath . DIRECTORY_SEPARATOR . $path;
1739                if (is_link($path)) {
1740                    unlink($path);
1741                } else if (is_dir($path)) {
1742                    $success = rmdir($path);
1743                } else {
1744                    $success = unlink($path);
1745                }
1746                if (!$success) {
1747                    break;
1748                }
1749            }
1750            if ($success) {
1751                $success = rmdir($dirPath);
1752            }
1753        }
1754        if (!$success) {
1755            $this->setError(elFinder::ERROR_RM, $dirPath);
1756            return false;
1757        }
1758        return $success;
1759    }
1760
1761    /**
1762     * Returns array of strings containing all files and folders in the specified local directory.
1763     *
1764     * @param        $dir
1765     * @param        $omitSymlinks
1766     * @param string $prefix
1767     *
1768     * @return array array of files and folders names relative to the $path
1769     * or an empty array if the directory $path is empty,
1770     * <br />
1771     * false if $path is not a directory or does not exist.
1772     * @throws Exception
1773     * @internal param string $path path to directory to scan.
1774     */
1775    private static function listFilesInDirectory($dir, $omitSymlinks, $prefix = '')
1776    {
1777        if (!is_dir($dir)) {
1778            return false;
1779        }
1780        $excludes = array(".", "..");
1781        $result = array();
1782        $files = self::localScandir($dir);
1783        if (!$files) {
1784            return array();
1785        }
1786        foreach ($files as $file) {
1787            if (!in_array($file, $excludes)) {
1788                $path = $dir . DIRECTORY_SEPARATOR . $file;
1789                if (is_link($path)) {
1790                    if ($omitSymlinks) {
1791                        continue;
1792                    } else {
1793                        $result[] = $prefix . $file;
1794                    }
1795                } else if (is_dir($path)) {
1796                    $result[] = $prefix . $file . DIRECTORY_SEPARATOR;
1797                    $subs = elFinderVolumeFTP::listFilesInDirectory($path, $omitSymlinks, $prefix . $file . DIRECTORY_SEPARATOR);
1798                    if ($subs) {
1799                        $result = array_merge($result, $subs);
1800                    }
1801
1802                } else {
1803                    $result[] = $prefix . $file;
1804                }
1805            }
1806        }
1807        return $result;
1808    }
1809
1810} // END class
1811