1<?php
2
3/**
4 * Pure-PHP implementation of SFTP.
5 *
6 * PHP version 5
7 *
8 * Supports SFTPv2/3/4/5/6. Defaults to v3.
9 *
10 * The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}.
11 *
12 * Here's a short example of how to use this library:
13 * <code>
14 * <?php
15 *    include 'vendor/autoload.php';
16 *
17 *    $sftp = new \phpseclib3\Net\SFTP('www.domain.tld');
18 *    if (!$sftp->login('username', 'password')) {
19 *        exit('Login Failed');
20 *    }
21 *
22 *    echo $sftp->pwd() . "\r\n";
23 *    $sftp->put('filename.ext', 'hello, world!');
24 *    print_r($sftp->nlist());
25 * ?>
26 * </code>
27 *
28 * @author    Jim Wigginton <terrafrost@php.net>
29 * @copyright 2009 Jim Wigginton
30 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
31 * @link      http://phpseclib.sourceforge.net
32 */
33
34namespace phpseclib3\Net;
35
36use phpseclib3\Common\Functions\Strings;
37use phpseclib3\Exception\FileNotFoundException;
38
39/**
40 * Pure-PHP implementations of SFTP.
41 *
42 * @author  Jim Wigginton <terrafrost@php.net>
43 */
44class SFTP extends SSH2
45{
46    /**
47     * SFTP channel constant
48     *
49     * \phpseclib3\Net\SSH2::exec() uses 0 and \phpseclib3\Net\SSH2::read() / \phpseclib3\Net\SSH2::write() use 1.
50     *
51     * @see \phpseclib3\Net\SSH2::send_channel_packet()
52     * @see \phpseclib3\Net\SSH2::get_channel_packet()
53     */
54    const CHANNEL = 0x100;
55
56    /**
57     * Reads data from a local file.
58     *
59     * @see \phpseclib3\Net\SFTP::put()
60     */
61    const SOURCE_LOCAL_FILE = 1;
62    /**
63     * Reads data from a string.
64     *
65     * @see \phpseclib3\Net\SFTP::put()
66     */
67    // this value isn't really used anymore but i'm keeping it reserved for historical reasons
68    const SOURCE_STRING = 2;
69    /**
70     * Reads data from callback:
71     * function callback($length) returns string to proceed, null for EOF
72     *
73     * @see \phpseclib3\Net\SFTP::put()
74     */
75    const SOURCE_CALLBACK = 16;
76    /**
77     * Resumes an upload
78     *
79     * @see \phpseclib3\Net\SFTP::put()
80     */
81    const RESUME = 4;
82    /**
83     * Append a local file to an already existing remote file
84     *
85     * @see \phpseclib3\Net\SFTP::put()
86     */
87    const RESUME_START = 8;
88
89    /**
90     * Packet Types
91     *
92     * @see self::__construct()
93     * @var array
94     * @access private
95     */
96    private static $packet_types = [];
97
98    /**
99     * Status Codes
100     *
101     * @see self::__construct()
102     * @var array
103     * @access private
104     */
105    private static $status_codes = [];
106
107    /** @var array<int, string> */
108    private static $attributes;
109
110    /** @var array<int, string> */
111    private static $open_flags;
112
113    /** @var array<int, string> */
114    private static $open_flags5;
115
116    /** @var array<int, string> */
117    private static $file_types;
118
119    /**
120     * The Request ID
121     *
122     * The request ID exists in the off chance that a packet is sent out-of-order.  Of course, this library doesn't support
123     * concurrent actions, so it's somewhat academic, here.
124     *
125     * @var boolean
126     * @see self::_send_sftp_packet()
127     */
128    private $use_request_id = false;
129
130    /**
131     * The Packet Type
132     *
133     * The request ID exists in the off chance that a packet is sent out-of-order.  Of course, this library doesn't support
134     * concurrent actions, so it's somewhat academic, here.
135     *
136     * @var int
137     * @see self::_get_sftp_packet()
138     */
139    private $packet_type = -1;
140
141    /**
142     * Packet Buffer
143     *
144     * @var string
145     * @see self::_get_sftp_packet()
146     */
147    private $packet_buffer = '';
148
149    /**
150     * Extensions supported by the server
151     *
152     * @var array
153     * @see self::_initChannel()
154     */
155    private $extensions = [];
156
157    /**
158     * Server SFTP version
159     *
160     * @var int
161     * @see self::_initChannel()
162     */
163    private $version;
164
165    /**
166     * Default Server SFTP version
167     *
168     * @var int
169     * @see self::_initChannel()
170     */
171    private $defaultVersion;
172
173    /**
174     * Preferred SFTP version
175     *
176     * @var int
177     * @see self::_initChannel()
178     */
179    private $preferredVersion = 3;
180
181    /**
182     * Current working directory
183     *
184     * @var string|bool
185     * @see self::realpath()
186     * @see self::chdir()
187     */
188    private $pwd = false;
189
190    /**
191     * Packet Type Log
192     *
193     * @see self::getLog()
194     * @var array
195     */
196    private $packet_type_log = [];
197
198    /**
199     * Packet Log
200     *
201     * @see self::getLog()
202     * @var array
203     */
204    private $packet_log = [];
205
206    /**
207     * Real-time log file pointer
208     *
209     * @see self::_append_log()
210     * @var resource|closed-resource
211     */
212    private $realtime_log_file;
213
214    /**
215     * Real-time log file size
216     *
217     * @see self::_append_log()
218     * @var int
219     */
220    private $realtime_log_size;
221
222    /**
223     * Real-time log file wrap boolean
224     *
225     * @see self::_append_log()
226     * @var bool
227     */
228    private $realtime_log_wrap;
229
230    /**
231     * Current log size
232     *
233     * Should never exceed self::LOG_MAX_SIZE
234     *
235     * @var int
236     */
237    private $log_size;
238
239    /**
240     * Error information
241     *
242     * @see self::getSFTPErrors()
243     * @see self::getLastSFTPError()
244     * @var array
245     */
246    private $sftp_errors = [];
247
248    /**
249     * Stat Cache
250     *
251     * Rather than always having to open a directory and close it immediately there after to see if a file is a directory
252     * we'll cache the results.
253     *
254     * @see self::_update_stat_cache()
255     * @see self::_remove_from_stat_cache()
256     * @see self::_query_stat_cache()
257     * @var array
258     */
259    private $stat_cache = [];
260
261    /**
262     * Max SFTP Packet Size
263     *
264     * @see self::__construct()
265     * @see self::get()
266     * @var int
267     */
268    private $max_sftp_packet;
269
270    /**
271     * Stat Cache Flag
272     *
273     * @see self::disableStatCache()
274     * @see self::enableStatCache()
275     * @var bool
276     */
277    private $use_stat_cache = true;
278
279    /**
280     * Sort Options
281     *
282     * @see self::_comparator()
283     * @see self::setListOrder()
284     * @var array
285     */
286    protected $sortOptions = [];
287
288    /**
289     * Canonicalization Flag
290     *
291     * Determines whether or not paths should be canonicalized before being
292     * passed on to the remote server.
293     *
294     * @see self::enablePathCanonicalization()
295     * @see self::disablePathCanonicalization()
296     * @see self::realpath()
297     * @var bool
298     */
299    private $canonicalize_paths = true;
300
301    /**
302     * Request Buffers
303     *
304     * @see self::_get_sftp_packet()
305     * @var array
306     */
307    private $requestBuffer = [];
308
309    /**
310     * Preserve timestamps on file downloads / uploads
311     *
312     * @see self::get()
313     * @see self::put()
314     * @var bool
315     */
316    private $preserveTime = false;
317
318    /**
319     * Arbitrary Length Packets Flag
320     *
321     * Determines whether or not packets of any length should be allowed,
322     * in cases where the server chooses the packet length (such as
323     * directory listings). By default, packets are only allowed to be
324     * 256 * 1024 bytes (SFTP_MAX_MSG_LENGTH from OpenSSH's sftp-common.h)
325     *
326     * @see self::enableArbitraryLengthPackets()
327     * @see self::_get_sftp_packet()
328     * @var bool
329     */
330    private $allow_arbitrary_length_packets = false;
331
332    /**
333     * Was the last packet due to the channels being closed or not?
334     *
335     * @see self::get()
336     * @see self::get_sftp_packet()
337     * @var bool
338     */
339    private $channel_close = false;
340
341    /**
342     * Has the SFTP channel been partially negotiated?
343     *
344     * @var bool
345     */
346    private $partial_init = false;
347
348    /**
349     * Default Constructor.
350     *
351     * Connects to an SFTP server
352     *
353     * $host can either be a string, representing the host, or a stream resource.
354     *
355     * @param mixed $host
356     * @param int $port
357     * @param int $timeout
358     */
359    public function __construct($host, $port = 22, $timeout = 10)
360    {
361        parent::__construct($host, $port, $timeout);
362
363        $this->max_sftp_packet = 1 << 15;
364
365        if (empty(self::$packet_types)) {
366            self::$packet_types = [
367                1  => 'NET_SFTP_INIT',
368                2  => 'NET_SFTP_VERSION',
369                3  => 'NET_SFTP_OPEN',
370                4  => 'NET_SFTP_CLOSE',
371                5  => 'NET_SFTP_READ',
372                6  => 'NET_SFTP_WRITE',
373                7  => 'NET_SFTP_LSTAT',
374                9  => 'NET_SFTP_SETSTAT',
375                10 => 'NET_SFTP_FSETSTAT',
376                11 => 'NET_SFTP_OPENDIR',
377                12 => 'NET_SFTP_READDIR',
378                13 => 'NET_SFTP_REMOVE',
379                14 => 'NET_SFTP_MKDIR',
380                15 => 'NET_SFTP_RMDIR',
381                16 => 'NET_SFTP_REALPATH',
382                17 => 'NET_SFTP_STAT',
383                18 => 'NET_SFTP_RENAME',
384                19 => 'NET_SFTP_READLINK',
385                20 => 'NET_SFTP_SYMLINK',
386                21 => 'NET_SFTP_LINK',
387
388                101 => 'NET_SFTP_STATUS',
389                102 => 'NET_SFTP_HANDLE',
390                103 => 'NET_SFTP_DATA',
391                104 => 'NET_SFTP_NAME',
392                105 => 'NET_SFTP_ATTRS',
393
394                200 => 'NET_SFTP_EXTENDED'
395            ];
396            self::$status_codes = [
397                0 => 'NET_SFTP_STATUS_OK',
398                1 => 'NET_SFTP_STATUS_EOF',
399                2 => 'NET_SFTP_STATUS_NO_SUCH_FILE',
400                3 => 'NET_SFTP_STATUS_PERMISSION_DENIED',
401                4 => 'NET_SFTP_STATUS_FAILURE',
402                5 => 'NET_SFTP_STATUS_BAD_MESSAGE',
403                6 => 'NET_SFTP_STATUS_NO_CONNECTION',
404                7 => 'NET_SFTP_STATUS_CONNECTION_LOST',
405                8 => 'NET_SFTP_STATUS_OP_UNSUPPORTED',
406                9 => 'NET_SFTP_STATUS_INVALID_HANDLE',
407                10 => 'NET_SFTP_STATUS_NO_SUCH_PATH',
408                11 => 'NET_SFTP_STATUS_FILE_ALREADY_EXISTS',
409                12 => 'NET_SFTP_STATUS_WRITE_PROTECT',
410                13 => 'NET_SFTP_STATUS_NO_MEDIA',
411                14 => 'NET_SFTP_STATUS_NO_SPACE_ON_FILESYSTEM',
412                15 => 'NET_SFTP_STATUS_QUOTA_EXCEEDED',
413                16 => 'NET_SFTP_STATUS_UNKNOWN_PRINCIPAL',
414                17 => 'NET_SFTP_STATUS_LOCK_CONFLICT',
415                18 => 'NET_SFTP_STATUS_DIR_NOT_EMPTY',
416                19 => 'NET_SFTP_STATUS_NOT_A_DIRECTORY',
417                20 => 'NET_SFTP_STATUS_INVALID_FILENAME',
418                21 => 'NET_SFTP_STATUS_LINK_LOOP',
419                22 => 'NET_SFTP_STATUS_CANNOT_DELETE',
420                23 => 'NET_SFTP_STATUS_INVALID_PARAMETER',
421                24 => 'NET_SFTP_STATUS_FILE_IS_A_DIRECTORY',
422                25 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_CONFLICT',
423                26 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_REFUSED',
424                27 => 'NET_SFTP_STATUS_DELETE_PENDING',
425                28 => 'NET_SFTP_STATUS_FILE_CORRUPT',
426                29 => 'NET_SFTP_STATUS_OWNER_INVALID',
427                30 => 'NET_SFTP_STATUS_GROUP_INVALID',
428                31 => 'NET_SFTP_STATUS_NO_MATCHING_BYTE_RANGE_LOCK'
429            ];
430            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1
431            // the order, in this case, matters quite a lot - see \phpseclib3\Net\SFTP::_parseAttributes() to understand why
432            self::$attributes = [
433                0x00000001 => 'NET_SFTP_ATTR_SIZE',
434                0x00000002 => 'NET_SFTP_ATTR_UIDGID',          // defined in SFTPv3, removed in SFTPv4+
435                0x00000080 => 'NET_SFTP_ATTR_OWNERGROUP',      // defined in SFTPv4+
436                0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS',
437                0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME',
438                0x00000010 => 'NET_SFTP_ATTR_CREATETIME',      // SFTPv4+
439                0x00000020 => 'NET_SFTP_ATTR_MODIFYTIME',
440                0x00000040 => 'NET_SFTP_ATTR_ACL',
441                0x00000100 => 'NET_SFTP_ATTR_SUBSECOND_TIMES',
442                0x00000200 => 'NET_SFTP_ATTR_BITS',            // SFTPv5+
443                0x00000400 => 'NET_SFTP_ATTR_ALLOCATION_SIZE', // SFTPv6+
444                0x00000800 => 'NET_SFTP_ATTR_TEXT_HINT',
445                0x00001000 => 'NET_SFTP_ATTR_MIME_TYPE',
446                0x00002000 => 'NET_SFTP_ATTR_LINK_COUNT',
447                0x00004000 => 'NET_SFTP_ATTR_UNTRANSLATED_NAME',
448                0x00008000 => 'NET_SFTP_ATTR_CTIME',
449                // 0x80000000 will yield a floating point on 32-bit systems and converting floating points to integers
450                // yields inconsistent behavior depending on how php is compiled.  so we left shift -1 (which, in
451                // two's compliment, consists of all 1 bits) by 31.  on 64-bit systems this'll yield 0xFFFFFFFF80000000.
452                // that's not a problem, however, and 'anded' and a 32-bit number, as all the leading 1 bits are ignored.
453                (PHP_INT_SIZE == 4 ? (-1 << 31) : 0x80000000) => 'NET_SFTP_ATTR_EXTENDED'
454            ];
455            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.3
456            // the flag definitions change somewhat in SFTPv5+.  if SFTPv5+ support is added to this library, maybe name
457            // the array for that $this->open5_flags and similarly alter the constant names.
458            self::$open_flags = [
459                0x00000001 => 'NET_SFTP_OPEN_READ',
460                0x00000002 => 'NET_SFTP_OPEN_WRITE',
461                0x00000004 => 'NET_SFTP_OPEN_APPEND',
462                0x00000008 => 'NET_SFTP_OPEN_CREATE',
463                0x00000010 => 'NET_SFTP_OPEN_TRUNCATE',
464                0x00000020 => 'NET_SFTP_OPEN_EXCL',
465                0x00000040 => 'NET_SFTP_OPEN_TEXT' // defined in SFTPv4
466            ];
467            // SFTPv5+ changed the flags up:
468            // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-8.1.1.3
469            self::$open_flags5 = [
470                // when SSH_FXF_ACCESS_DISPOSITION is a 3 bit field that controls how the file is opened
471                0x00000000 => 'NET_SFTP_OPEN_CREATE_NEW',
472                0x00000001 => 'NET_SFTP_OPEN_CREATE_TRUNCATE',
473                0x00000002 => 'NET_SFTP_OPEN_OPEN_EXISTING',
474                0x00000003 => 'NET_SFTP_OPEN_OPEN_OR_CREATE',
475                0x00000004 => 'NET_SFTP_OPEN_TRUNCATE_EXISTING',
476                // the rest of the flags are not supported
477                0x00000008 => 'NET_SFTP_OPEN_APPEND_DATA', // "the offset field of SS_FXP_WRITE requests is ignored"
478                0x00000010 => 'NET_SFTP_OPEN_APPEND_DATA_ATOMIC',
479                0x00000020 => 'NET_SFTP_OPEN_TEXT_MODE',
480                0x00000040 => 'NET_SFTP_OPEN_BLOCK_READ',
481                0x00000080 => 'NET_SFTP_OPEN_BLOCK_WRITE',
482                0x00000100 => 'NET_SFTP_OPEN_BLOCK_DELETE',
483                0x00000200 => 'NET_SFTP_OPEN_BLOCK_ADVISORY',
484                0x00000400 => 'NET_SFTP_OPEN_NOFOLLOW',
485                0x00000800 => 'NET_SFTP_OPEN_DELETE_ON_CLOSE',
486                0x00001000 => 'NET_SFTP_OPEN_ACCESS_AUDIT_ALARM_INFO',
487                0x00002000 => 'NET_SFTP_OPEN_ACCESS_BACKUP',
488                0x00004000 => 'NET_SFTP_OPEN_BACKUP_STREAM',
489                0x00008000 => 'NET_SFTP_OPEN_OVERRIDE_OWNER',
490            ];
491            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2
492            // see \phpseclib3\Net\SFTP::_parseLongname() for an explanation
493            self::$file_types = [
494                1 => 'NET_SFTP_TYPE_REGULAR',
495                2 => 'NET_SFTP_TYPE_DIRECTORY',
496                3 => 'NET_SFTP_TYPE_SYMLINK',
497                4 => 'NET_SFTP_TYPE_SPECIAL',
498                5 => 'NET_SFTP_TYPE_UNKNOWN',
499                // the following types were first defined for use in SFTPv5+
500                // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2
501                6 => 'NET_SFTP_TYPE_SOCKET',
502                7 => 'NET_SFTP_TYPE_CHAR_DEVICE',
503                8 => 'NET_SFTP_TYPE_BLOCK_DEVICE',
504                9 => 'NET_SFTP_TYPE_FIFO'
505            ];
506            self::define_array(
507                self::$packet_types,
508                self::$status_codes,
509                self::$attributes,
510                self::$open_flags,
511                self::$open_flags5,
512                self::$file_types
513            );
514        }
515
516        if (!defined('NET_SFTP_QUEUE_SIZE')) {
517            define('NET_SFTP_QUEUE_SIZE', 32);
518        }
519        if (!defined('NET_SFTP_UPLOAD_QUEUE_SIZE')) {
520            define('NET_SFTP_UPLOAD_QUEUE_SIZE', 1024);
521        }
522    }
523
524    /**
525     * Check a few things before SFTP functions are called
526     *
527     * @return bool
528     */
529    private function precheck()
530    {
531        if (!($this->bitmap & SSH2::MASK_LOGIN)) {
532            return false;
533        }
534
535        if ($this->pwd === false) {
536            return $this->init_sftp_connection();
537        }
538
539        return true;
540    }
541
542    /**
543     * Partially initialize an SFTP connection
544     *
545     * @throws \UnexpectedValueException on receipt of unexpected packets
546     * @return bool
547     */
548    private function partial_init_sftp_connection()
549    {
550        $response = $this->open_channel(self::CHANNEL, true);
551        if ($response === true && $this->isTimeout()) {
552            return false;
553        }
554
555        $packet = Strings::packSSH2(
556            'CNsbs',
557            NET_SSH2_MSG_CHANNEL_REQUEST,
558            $this->server_channels[self::CHANNEL],
559            'subsystem',
560            true,
561            'sftp'
562        );
563        $this->send_binary_packet($packet);
564
565        $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST;
566
567        $response = $this->get_channel_packet(self::CHANNEL, true);
568        if ($response === false) {
569            // from PuTTY's psftp.exe
570            $command = "test -x /usr/lib/sftp-server && exec /usr/lib/sftp-server\n" .
571                       "test -x /usr/local/lib/sftp-server && exec /usr/local/lib/sftp-server\n" .
572                       "exec sftp-server";
573            // we don't do $this->exec($command, false) because exec() operates on a different channel and plus the SSH_MSG_CHANNEL_OPEN that exec() does
574            // is redundant
575            $packet = Strings::packSSH2(
576                'CNsCs',
577                NET_SSH2_MSG_CHANNEL_REQUEST,
578                $this->server_channels[self::CHANNEL],
579                'exec',
580                1,
581                $command
582            );
583            $this->send_binary_packet($packet);
584
585            $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST;
586
587            $response = $this->get_channel_packet(self::CHANNEL, true);
588            if ($response === false) {
589                return false;
590            }
591        } elseif ($response === true && $this->isTimeout()) {
592            return false;
593        }
594
595        $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_DATA;
596        $this->send_sftp_packet(NET_SFTP_INIT, "\0\0\0\3");
597
598        $response = $this->get_sftp_packet();
599        if ($this->packet_type != NET_SFTP_VERSION) {
600            throw new \UnexpectedValueException('Expected NET_SFTP_VERSION. '
601                                              . 'Got packet type: ' . $this->packet_type);
602        }
603
604        $this->use_request_id = true;
605
606        list($this->defaultVersion) = Strings::unpackSSH2('N', $response);
607        while (!empty($response)) {
608            list($key, $value) = Strings::unpackSSH2('ss', $response);
609            $this->extensions[$key] = $value;
610        }
611
612        $this->partial_init = true;
613
614        return true;
615    }
616
617    /**
618     * (Re)initializes the SFTP channel
619     *
620     * @return bool
621     */
622    private function init_sftp_connection()
623    {
624        if (!$this->partial_init && !$this->partial_init_sftp_connection()) {
625            return false;
626        }
627
628        /*
629         A Note on SFTPv4/5/6 support:
630         <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.1> states the following:
631
632         "If the client wishes to interoperate with servers that support noncontiguous version
633          numbers it SHOULD send '3'"
634
635         Given that the server only sends its version number after the client has already done so, the above
636         seems to be suggesting that v3 should be the default version.  This makes sense given that v3 is the
637         most popular.
638
639         <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.5> states the following;
640
641         "If the server did not send the "versions" extension, or the version-from-list was not included, the
642          server MAY send a status response describing the failure, but MUST then close the channel without
643          processing any further requests."
644
645         So what do you do if you have a client whose initial SSH_FXP_INIT packet says it implements v3 and
646         a server whose initial SSH_FXP_VERSION reply says it implements v4 and only v4?  If it only implements
647         v4, the "versions" extension is likely not going to have been sent so version re-negotiation as discussed
648         in draft-ietf-secsh-filexfer-13 would be quite impossible.  As such, what \phpseclib3\Net\SFTP would do is close the
649         channel and reopen it with a new and updated SSH_FXP_INIT packet.
650        */
651        $this->version = $this->defaultVersion;
652        if (isset($this->extensions['versions']) && (!$this->preferredVersion || $this->preferredVersion != $this->version)) {
653            $versions = explode(',', $this->extensions['versions']);
654            $supported = [6, 5, 4];
655            if ($this->preferredVersion) {
656                $supported = array_diff($supported, [$this->preferredVersion]);
657                array_unshift($supported, $this->preferredVersion);
658            }
659            foreach ($supported as $ver) {
660                if (in_array($ver, $versions)) {
661                    if ($ver === $this->version) {
662                        break;
663                    }
664                    $this->version = (int) $ver;
665                    $packet = Strings::packSSH2('ss', 'version-select', "$ver");
666                    $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet);
667                    $response = $this->get_sftp_packet();
668                    if ($this->packet_type != NET_SFTP_STATUS) {
669                        throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
670                            . 'Got packet type: ' . $this->packet_type);
671                    }
672                    list($status) = Strings::unpackSSH2('N', $response);
673                    if ($status != NET_SFTP_STATUS_OK) {
674                        $this->logError($response, $status);
675                        throw new \UnexpectedValueException('Expected NET_SFTP_STATUS_OK. '
676                            . ' Got ' . $status);
677                    }
678                    break;
679                }
680            }
681        }
682
683        /*
684         SFTPv4+ defines a 'newline' extension.  SFTPv3 seems to have unofficial support for it via 'newline@vandyke.com',
685         however, I'm not sure what 'newline@vandyke.com' is supposed to do (the fact that it's unofficial means that it's
686         not in the official SFTPv3 specs) and 'newline@vandyke.com' / 'newline' are likely not drop-in substitutes for
687         one another due to the fact that 'newline' comes with a SSH_FXF_TEXT bitmask whereas it seems unlikely that
688         'newline@vandyke.com' would.
689        */
690        /*
691        if (isset($this->extensions['newline@vandyke.com'])) {
692            $this->extensions['newline'] = $this->extensions['newline@vandyke.com'];
693            unset($this->extensions['newline@vandyke.com']);
694        }
695        */
696        if ($this->version < 2 || $this->version > 6) {
697            return false;
698        }
699
700        $this->pwd = true;
701        try {
702            $this->pwd = $this->realpath('.');
703        } catch (\UnexpectedValueException $e) {
704            if (!$this->canonicalize_paths) {
705                throw $e;
706            }
707            $this->canonicalize_paths = false;
708            $this->reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST);
709        }
710
711        $this->update_stat_cache($this->pwd, []);
712
713        return true;
714    }
715
716    /**
717     * Disable the stat cache
718     *
719     */
720    public function disableStatCache()
721    {
722        $this->use_stat_cache = false;
723    }
724
725    /**
726     * Enable the stat cache
727     *
728     */
729    public function enableStatCache()
730    {
731        $this->use_stat_cache = true;
732    }
733
734    /**
735     * Clear the stat cache
736     *
737     */
738    public function clearStatCache()
739    {
740        $this->stat_cache = [];
741    }
742
743    /**
744     * Enable path canonicalization
745     *
746     */
747    public function enablePathCanonicalization()
748    {
749        $this->canonicalize_paths = true;
750    }
751
752    /**
753     * Disable path canonicalization
754     *
755     * If this is enabled then $sftp->pwd() will not return the canonicalized absolute path
756     *
757     */
758    public function disablePathCanonicalization()
759    {
760        $this->canonicalize_paths = false;
761    }
762
763    /**
764     * Enable arbitrary length packets
765     *
766     */
767    public function enableArbitraryLengthPackets()
768    {
769        $this->allow_arbitrary_length_packets = true;
770    }
771
772    /**
773     * Disable arbitrary length packets
774     *
775     */
776    public function disableArbitraryLengthPackets()
777    {
778        $this->allow_arbitrary_length_packets = false;
779    }
780
781    /**
782     * Returns the current directory name
783     *
784     * @return string|bool
785     */
786    public function pwd()
787    {
788        if (!$this->precheck()) {
789            return false;
790        }
791
792        return $this->pwd;
793    }
794
795    /**
796     * Logs errors
797     *
798     * @param string $response
799     * @param int $status
800     */
801    private function logError($response, $status = -1)
802    {
803        if ($status == -1) {
804            list($status) = Strings::unpackSSH2('N', $response);
805        }
806
807        $error = self::$status_codes[$status];
808
809        if ($this->version > 2) {
810            list($message) = Strings::unpackSSH2('s', $response);
811            $this->sftp_errors[] = "$error: $message";
812        } else {
813            $this->sftp_errors[] = $error;
814        }
815    }
816
817    /**
818     * Canonicalize the Server-Side Path Name
819     *
820     * SFTP doesn't provide a mechanism by which the current working directory can be changed, so we'll emulate it.  Returns
821     * the absolute (canonicalized) path.
822     *
823     * If canonicalize_paths has been disabled using disablePathCanonicalization(), $path is returned as-is.
824     *
825     * @see self::chdir()
826     * @see self::disablePathCanonicalization()
827     * @param string $path
828     * @throws \UnexpectedValueException on receipt of unexpected packets
829     * @return mixed
830     */
831    public function realpath($path)
832    {
833        if ($this->precheck() === false) {
834            return false;
835        }
836
837        if (!$this->canonicalize_paths) {
838            if ($this->pwd === true) {
839                return '.';
840            }
841            if (!strlen($path) || $path[0] != '/') {
842                $path = $this->pwd . '/' . $path;
843            }
844            $parts = explode('/', $path);
845            $afterPWD = $beforePWD = [];
846            foreach ($parts as $part) {
847                switch ($part) {
848                    //case '': // some SFTP servers /require/ double /'s. see https://github.com/phpseclib/phpseclib/pull/1137
849                    case '.':
850                        break;
851                    case '..':
852                        if (!empty($afterPWD)) {
853                            array_pop($afterPWD);
854                        } else {
855                            $beforePWD[] = '..';
856                        }
857                        break;
858                    default:
859                        $afterPWD[] = $part;
860                }
861            }
862            $beforePWD = count($beforePWD) ? implode('/', $beforePWD) : '.';
863            return $beforePWD . '/' . implode('/', $afterPWD);
864        }
865
866        if ($this->pwd === true) {
867            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9
868            $this->send_sftp_packet(NET_SFTP_REALPATH, Strings::packSSH2('s', $path));
869
870            $response = $this->get_sftp_packet();
871            switch ($this->packet_type) {
872                case NET_SFTP_NAME:
873                    // although SSH_FXP_NAME is implemented differently in SFTPv3 than it is in SFTPv4+, the following
874                    // should work on all SFTP versions since the only part of the SSH_FXP_NAME packet the following looks
875                    // at is the first part and that part is defined the same in SFTP versions 3 through 6.
876                    list(, $filename) = Strings::unpackSSH2('Ns', $response);
877                    return $filename;
878                case NET_SFTP_STATUS:
879                    $this->logError($response);
880                    return false;
881                default:
882                    throw new \UnexpectedValueException('Expected NET_SFTP_NAME or NET_SFTP_STATUS. '
883                                                      . 'Got packet type: ' . $this->packet_type);
884            }
885        }
886
887        if (!strlen($path) || $path[0] != '/') {
888            $path = $this->pwd . '/' . $path;
889        }
890
891        $path = explode('/', $path);
892        $new = [];
893        foreach ($path as $dir) {
894            if (!strlen($dir)) {
895                continue;
896            }
897            switch ($dir) {
898                case '..':
899                    array_pop($new);
900                    // fall-through
901                case '.':
902                    break;
903                default:
904                    $new[] = $dir;
905            }
906        }
907
908        return '/' . implode('/', $new);
909    }
910
911    /**
912     * Changes the current directory
913     *
914     * @param string $dir
915     * @throws \UnexpectedValueException on receipt of unexpected packets
916     * @return bool
917     */
918    public function chdir($dir)
919    {
920        if (!$this->precheck()) {
921            return false;
922        }
923
924        // assume current dir if $dir is empty
925        if ($dir === '') {
926            $dir = './';
927        // suffix a slash if needed
928        } elseif ($dir[strlen($dir) - 1] != '/') {
929            $dir .= '/';
930        }
931
932        $dir = $this->realpath($dir);
933
934        // confirm that $dir is, in fact, a valid directory
935        if ($this->use_stat_cache && is_array($this->query_stat_cache($dir))) {
936            $this->pwd = $dir;
937            return true;
938        }
939
940        // we could do a stat on the alleged $dir to see if it's a directory but that doesn't tell us
941        // the currently logged in user has the appropriate permissions or not. maybe you could see if
942        // the file's uid / gid match the currently logged in user's uid / gid but how there's no easy
943        // way to get those with SFTP
944
945        $this->send_sftp_packet(NET_SFTP_OPENDIR, Strings::packSSH2('s', $dir));
946
947        // see \phpseclib3\Net\SFTP::nlist() for a more thorough explanation of the following
948        $response = $this->get_sftp_packet();
949        switch ($this->packet_type) {
950            case NET_SFTP_HANDLE:
951                $handle = substr($response, 4);
952                break;
953            case NET_SFTP_STATUS:
954                $this->logError($response);
955                return false;
956            default:
957                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS' .
958                                                    'Got packet type: ' . $this->packet_type);
959        }
960
961        if (!$this->close_handle($handle)) {
962            return false;
963        }
964
965        $this->update_stat_cache($dir, []);
966
967        $this->pwd = $dir;
968        return true;
969    }
970
971    /**
972     * Returns a list of files in the given directory
973     *
974     * @param string $dir
975     * @param bool $recursive
976     * @return array|false
977     */
978    public function nlist($dir = '.', $recursive = false)
979    {
980        return $this->nlist_helper($dir, $recursive, '');
981    }
982
983    /**
984     * Helper method for nlist
985     *
986     * @param string $dir
987     * @param bool $recursive
988     * @param string $relativeDir
989     * @return array|false
990     */
991    private function nlist_helper($dir, $recursive, $relativeDir)
992    {
993        $files = $this->readlist($dir, false);
994
995        // If we get an int back, then that is an "unexpected" status.
996        // We do not have a file list, so return false.
997        if (is_int($files)) {
998            return false;
999        }
1000
1001        if (!$recursive || $files === false) {
1002            return $files;
1003        }
1004
1005        $result = [];
1006        foreach ($files as $value) {
1007            if ($value == '.' || $value == '..') {
1008                $result[] = $relativeDir . $value;
1009                continue;
1010            }
1011            if (is_array($this->query_stat_cache($this->realpath($dir . '/' . $value)))) {
1012                $temp = $this->nlist_helper($dir . '/' . $value, true, $relativeDir . $value . '/');
1013                $temp = is_array($temp) ? $temp : [];
1014                $result = array_merge($result, $temp);
1015            } else {
1016                $result[] = $relativeDir . $value;
1017            }
1018        }
1019
1020        return $result;
1021    }
1022
1023    /**
1024     * Returns a detailed list of files in the given directory
1025     *
1026     * @param string $dir
1027     * @param bool $recursive
1028     * @return array|false
1029     */
1030    public function rawlist($dir = '.', $recursive = false)
1031    {
1032        $files = $this->readlist($dir, true);
1033
1034        // If we get an int back, then that is an "unexpected" status.
1035        // We do not have a file list, so return false.
1036        if (is_int($files)) {
1037            return false;
1038        }
1039
1040        if (!$recursive || $files === false) {
1041            return $files;
1042        }
1043
1044        static $depth = 0;
1045
1046        foreach ($files as $key => $value) {
1047            if ($depth != 0 && $key == '..') {
1048                unset($files[$key]);
1049                continue;
1050            }
1051            $is_directory = false;
1052            if ($key != '.' && $key != '..') {
1053                if ($this->use_stat_cache) {
1054                    $is_directory = is_array($this->query_stat_cache($this->realpath($dir . '/' . $key)));
1055                } else {
1056                    $stat = $this->lstat($dir . '/' . $key);
1057                    $is_directory = $stat && $stat['type'] === NET_SFTP_TYPE_DIRECTORY;
1058                }
1059            }
1060
1061            if ($is_directory) {
1062                $depth++;
1063                $files[$key] = $this->rawlist($dir . '/' . $key, true);
1064                $depth--;
1065            } else {
1066                $files[$key] = (object) $value;
1067            }
1068        }
1069
1070        return $files;
1071    }
1072
1073    /**
1074     * Reads a list, be it detailed or not, of files in the given directory
1075     *
1076     * @param string $dir
1077     * @param bool $raw
1078     * @return array|false
1079     * @throws \UnexpectedValueException on receipt of unexpected packets
1080     */
1081    private function readlist($dir, $raw = true)
1082    {
1083        if (!$this->precheck()) {
1084            return false;
1085        }
1086
1087        $dir = $this->realpath($dir . '/');
1088        if ($dir === false) {
1089            return false;
1090        }
1091
1092        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.2
1093        $this->send_sftp_packet(NET_SFTP_OPENDIR, Strings::packSSH2('s', $dir));
1094
1095        $response = $this->get_sftp_packet();
1096        switch ($this->packet_type) {
1097            case NET_SFTP_HANDLE:
1098                // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.2
1099                // since 'handle' is the last field in the SSH_FXP_HANDLE packet, we'll just remove the first four bytes that
1100                // represent the length of the string and leave it at that
1101                $handle = substr($response, 4);
1102                break;
1103            case NET_SFTP_STATUS:
1104                // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
1105                list($status) = Strings::unpackSSH2('N', $response);
1106                $this->logError($response, $status);
1107                return $status;
1108            default:
1109                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
1110                                                  . 'Got packet type: ' . $this->packet_type);
1111        }
1112
1113        $this->update_stat_cache($dir, []);
1114
1115        $contents = [];
1116        while (true) {
1117            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.2
1118            // why multiple SSH_FXP_READDIR packets would be sent when the response to a single one can span arbitrarily many
1119            // SSH_MSG_CHANNEL_DATA messages is not known to me.
1120            $this->send_sftp_packet(NET_SFTP_READDIR, Strings::packSSH2('s', $handle));
1121
1122            $response = $this->get_sftp_packet();
1123            switch ($this->packet_type) {
1124                case NET_SFTP_NAME:
1125                    list($count) = Strings::unpackSSH2('N', $response);
1126                    for ($i = 0; $i < $count; $i++) {
1127                        list($shortname) = Strings::unpackSSH2('s', $response);
1128                        // SFTPv4 "removed the long filename from the names structure-- it can now be
1129                        //         built from information available in the attrs structure."
1130                        if ($this->version < 4) {
1131                            list($longname) = Strings::unpackSSH2('s', $response);
1132                        }
1133                        $attributes = $this->parseAttributes($response);
1134                        if (!isset($attributes['type']) && $this->version < 4) {
1135                            $fileType = $this->parseLongname($longname);
1136                            if ($fileType) {
1137                                $attributes['type'] = $fileType;
1138                            }
1139                        }
1140                        $contents[$shortname] = $attributes + ['filename' => $shortname];
1141
1142                        if (isset($attributes['type']) && $attributes['type'] == NET_SFTP_TYPE_DIRECTORY && ($shortname != '.' && $shortname != '..')) {
1143                            $this->update_stat_cache($dir . '/' . $shortname, []);
1144                        } else {
1145                            if ($shortname == '..') {
1146                                $temp = $this->realpath($dir . '/..') . '/.';
1147                            } else {
1148                                $temp = $dir . '/' . $shortname;
1149                            }
1150                            $this->update_stat_cache($temp, (object) ['lstat' => $attributes]);
1151                        }
1152                        // SFTPv6 has an optional boolean end-of-list field, but we'll ignore that, since the
1153                        // final SSH_FXP_STATUS packet should tell us that, already.
1154                    }
1155                    break;
1156                case NET_SFTP_STATUS:
1157                    list($status) = Strings::unpackSSH2('N', $response);
1158                    if ($status != NET_SFTP_STATUS_EOF) {
1159                        $this->logError($response, $status);
1160                        return $status;
1161                    }
1162                    break 2;
1163                default:
1164                    throw new \UnexpectedValueException('Expected NET_SFTP_NAME or NET_SFTP_STATUS. '
1165                                                      . 'Got packet type: ' . $this->packet_type);
1166            }
1167        }
1168
1169        if (!$this->close_handle($handle)) {
1170            return false;
1171        }
1172
1173        if (count($this->sortOptions)) {
1174            uasort($contents, [&$this, 'comparator']);
1175        }
1176
1177        return $raw ? $contents : array_map('strval', array_keys($contents));
1178    }
1179
1180    /**
1181     * Compares two rawlist entries using parameters set by setListOrder()
1182     *
1183     * Intended for use with uasort()
1184     *
1185     * @param array $a
1186     * @param array $b
1187     * @return int
1188     */
1189    private function comparator(array $a, array $b)
1190    {
1191        switch (true) {
1192            case $a['filename'] === '.' || $b['filename'] === '.':
1193                if ($a['filename'] === $b['filename']) {
1194                    return 0;
1195                }
1196                return $a['filename'] === '.' ? -1 : 1;
1197            case $a['filename'] === '..' || $b['filename'] === '..':
1198                if ($a['filename'] === $b['filename']) {
1199                    return 0;
1200                }
1201                return $a['filename'] === '..' ? -1 : 1;
1202            case isset($a['type']) && $a['type'] === NET_SFTP_TYPE_DIRECTORY:
1203                if (!isset($b['type'])) {
1204                    return 1;
1205                }
1206                if ($b['type'] !== $a['type']) {
1207                    return -1;
1208                }
1209                break;
1210            case isset($b['type']) && $b['type'] === NET_SFTP_TYPE_DIRECTORY:
1211                return 1;
1212        }
1213        foreach ($this->sortOptions as $sort => $order) {
1214            if (!isset($a[$sort]) || !isset($b[$sort])) {
1215                if (isset($a[$sort])) {
1216                    return -1;
1217                }
1218                if (isset($b[$sort])) {
1219                    return 1;
1220                }
1221                return 0;
1222            }
1223            switch ($sort) {
1224                case 'filename':
1225                    $result = strcasecmp($a['filename'], $b['filename']);
1226                    if ($result) {
1227                        return $order === SORT_DESC ? -$result : $result;
1228                    }
1229                    break;
1230                case 'mode':
1231                    $a[$sort] &= 07777;
1232                    $b[$sort] &= 07777;
1233                    // fall-through
1234                default:
1235                    if ($a[$sort] === $b[$sort]) {
1236                        break;
1237                    }
1238                    return $order === SORT_ASC ? $a[$sort] - $b[$sort] : $b[$sort] - $a[$sort];
1239            }
1240        }
1241    }
1242
1243    /**
1244     * Defines how nlist() and rawlist() will be sorted - if at all.
1245     *
1246     * If sorting is enabled directories and files will be sorted independently with
1247     * directories appearing before files in the resultant array that is returned.
1248     *
1249     * Any parameter returned by stat is a valid sort parameter for this function.
1250     * Filename comparisons are case insensitive.
1251     *
1252     * Examples:
1253     *
1254     * $sftp->setListOrder('filename', SORT_ASC);
1255     * $sftp->setListOrder('size', SORT_DESC, 'filename', SORT_ASC);
1256     * $sftp->setListOrder(true);
1257     *    Separates directories from files but doesn't do any sorting beyond that
1258     * $sftp->setListOrder();
1259     *    Don't do any sort of sorting
1260     *
1261     * @param string ...$args
1262     */
1263    public function setListOrder(...$args)
1264    {
1265        $this->sortOptions = [];
1266        if (empty($args)) {
1267            return;
1268        }
1269        $len = count($args) & 0x7FFFFFFE;
1270        for ($i = 0; $i < $len; $i += 2) {
1271            $this->sortOptions[$args[$i]] = $args[$i + 1];
1272        }
1273        if (!count($this->sortOptions)) {
1274            $this->sortOptions = ['bogus' => true];
1275        }
1276    }
1277
1278    /**
1279     * Save files / directories to cache
1280     *
1281     * @param string $path
1282     * @param mixed $value
1283     */
1284    private function update_stat_cache($path, $value)
1285    {
1286        if ($this->use_stat_cache === false) {
1287            return;
1288        }
1289
1290        // preg_replace('#^/|/(?=/)|/$#', '', $dir) == str_replace('//', '/', trim($path, '/'))
1291        $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path));
1292
1293        $temp = &$this->stat_cache;
1294        $max = count($dirs) - 1;
1295        foreach ($dirs as $i => $dir) {
1296            // if $temp is an object that means one of two things.
1297            //  1. a file was deleted and changed to a directory behind phpseclib's back
1298            //  2. it's a symlink. when lstat is done it's unclear what it's a symlink to
1299            if (is_object($temp)) {
1300                $temp = [];
1301            }
1302            if (!isset($temp[$dir])) {
1303                $temp[$dir] = [];
1304            }
1305            if ($i === $max) {
1306                if (is_object($temp[$dir]) && is_object($value)) {
1307                    if (!isset($value->stat) && isset($temp[$dir]->stat)) {
1308                        $value->stat = $temp[$dir]->stat;
1309                    }
1310                    if (!isset($value->lstat) && isset($temp[$dir]->lstat)) {
1311                        $value->lstat = $temp[$dir]->lstat;
1312                    }
1313                }
1314                $temp[$dir] = $value;
1315                break;
1316            }
1317            $temp = &$temp[$dir];
1318        }
1319    }
1320
1321    /**
1322     * Remove files / directories from cache
1323     *
1324     * @param string $path
1325     * @return bool
1326     */
1327    private function remove_from_stat_cache($path)
1328    {
1329        $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path));
1330
1331        $temp = &$this->stat_cache;
1332        $max = count($dirs) - 1;
1333        foreach ($dirs as $i => $dir) {
1334            if (!is_array($temp)) {
1335                return false;
1336            }
1337            if ($i === $max) {
1338                unset($temp[$dir]);
1339                return true;
1340            }
1341            if (!isset($temp[$dir])) {
1342                return false;
1343            }
1344            $temp = &$temp[$dir];
1345        }
1346    }
1347
1348    /**
1349     * Checks cache for path
1350     *
1351     * Mainly used by file_exists
1352     *
1353     * @param string $path
1354     * @return mixed
1355     */
1356    private function query_stat_cache($path)
1357    {
1358        $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path));
1359
1360        $temp = &$this->stat_cache;
1361        foreach ($dirs as $dir) {
1362            if (!is_array($temp)) {
1363                return null;
1364            }
1365            if (!isset($temp[$dir])) {
1366                return null;
1367            }
1368            $temp = &$temp[$dir];
1369        }
1370        return $temp;
1371    }
1372
1373    /**
1374     * Returns general information about a file.
1375     *
1376     * Returns an array on success and false otherwise.
1377     *
1378     * @param string $filename
1379     * @return array|false
1380     */
1381    public function stat($filename)
1382    {
1383        if (!$this->precheck()) {
1384            return false;
1385        }
1386
1387        $filename = $this->realpath($filename);
1388        if ($filename === false) {
1389            return false;
1390        }
1391
1392        if ($this->use_stat_cache) {
1393            $result = $this->query_stat_cache($filename);
1394            if (is_array($result) && isset($result['.']) && isset($result['.']->stat)) {
1395                return $result['.']->stat;
1396            }
1397            if (is_object($result) && isset($result->stat)) {
1398                return $result->stat;
1399            }
1400        }
1401
1402        $stat = $this->stat_helper($filename, NET_SFTP_STAT);
1403        if ($stat === false) {
1404            $this->remove_from_stat_cache($filename);
1405            return false;
1406        }
1407        if (isset($stat['type'])) {
1408            if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) {
1409                $filename .= '/.';
1410            }
1411            $this->update_stat_cache($filename, (object) ['stat' => $stat]);
1412            return $stat;
1413        }
1414
1415        $pwd = $this->pwd;
1416        $stat['type'] = $this->chdir($filename) ?
1417            NET_SFTP_TYPE_DIRECTORY :
1418            NET_SFTP_TYPE_REGULAR;
1419        $this->pwd = $pwd;
1420
1421        if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) {
1422            $filename .= '/.';
1423        }
1424        $this->update_stat_cache($filename, (object) ['stat' => $stat]);
1425
1426        return $stat;
1427    }
1428
1429    /**
1430     * Returns general information about a file or symbolic link.
1431     *
1432     * Returns an array on success and false otherwise.
1433     *
1434     * @param string $filename
1435     * @return array|false
1436     */
1437    public function lstat($filename)
1438    {
1439        if (!$this->precheck()) {
1440            return false;
1441        }
1442
1443        $filename = $this->realpath($filename);
1444        if ($filename === false) {
1445            return false;
1446        }
1447
1448        if ($this->use_stat_cache) {
1449            $result = $this->query_stat_cache($filename);
1450            if (is_array($result) && isset($result['.']) && isset($result['.']->lstat)) {
1451                return $result['.']->lstat;
1452            }
1453            if (is_object($result) && isset($result->lstat)) {
1454                return $result->lstat;
1455            }
1456        }
1457
1458        $lstat = $this->stat_helper($filename, NET_SFTP_LSTAT);
1459        if ($lstat === false) {
1460            $this->remove_from_stat_cache($filename);
1461            return false;
1462        }
1463        if (isset($lstat['type'])) {
1464            if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) {
1465                $filename .= '/.';
1466            }
1467            $this->update_stat_cache($filename, (object) ['lstat' => $lstat]);
1468            return $lstat;
1469        }
1470
1471        $stat = $this->stat_helper($filename, NET_SFTP_STAT);
1472
1473        if ($lstat != $stat) {
1474            $lstat = array_merge($lstat, ['type' => NET_SFTP_TYPE_SYMLINK]);
1475            $this->update_stat_cache($filename, (object) ['lstat' => $lstat]);
1476            return $stat;
1477        }
1478
1479        $pwd = $this->pwd;
1480        $lstat['type'] = $this->chdir($filename) ?
1481            NET_SFTP_TYPE_DIRECTORY :
1482            NET_SFTP_TYPE_REGULAR;
1483        $this->pwd = $pwd;
1484
1485        if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) {
1486            $filename .= '/.';
1487        }
1488        $this->update_stat_cache($filename, (object) ['lstat' => $lstat]);
1489
1490        return $lstat;
1491    }
1492
1493    /**
1494     * Returns general information about a file or symbolic link
1495     *
1496     * Determines information without calling \phpseclib3\Net\SFTP::realpath().
1497     * The second parameter can be either NET_SFTP_STAT or NET_SFTP_LSTAT.
1498     *
1499     * @param string $filename
1500     * @param int $type
1501     * @throws \UnexpectedValueException on receipt of unexpected packets
1502     * @return array|false
1503     */
1504    private function stat_helper($filename, $type)
1505    {
1506        // SFTPv4+ adds an additional 32-bit integer field - flags - to the following:
1507        $packet = Strings::packSSH2('s', $filename);
1508        $this->send_sftp_packet($type, $packet);
1509
1510        $response = $this->get_sftp_packet();
1511        switch ($this->packet_type) {
1512            case NET_SFTP_ATTRS:
1513                return $this->parseAttributes($response);
1514            case NET_SFTP_STATUS:
1515                $this->logError($response);
1516                return false;
1517        }
1518
1519        throw new \UnexpectedValueException('Expected NET_SFTP_ATTRS or NET_SFTP_STATUS. '
1520                                          . 'Got packet type: ' . $this->packet_type);
1521    }
1522
1523    /**
1524     * Truncates a file to a given length
1525     *
1526     * @param string $filename
1527     * @param int $new_size
1528     * @return bool
1529     */
1530    public function truncate($filename, $new_size)
1531    {
1532        $attr = Strings::packSSH2('NQ', NET_SFTP_ATTR_SIZE, $new_size);
1533
1534        return $this->setstat($filename, $attr, false);
1535    }
1536
1537    /**
1538     * Sets access and modification time of file.
1539     *
1540     * If the file does not exist, it will be created.
1541     *
1542     * @param string $filename
1543     * @param int $time
1544     * @param int $atime
1545     * @throws \UnexpectedValueException on receipt of unexpected packets
1546     * @return bool
1547     */
1548    public function touch($filename, $time = null, $atime = null)
1549    {
1550        if (!$this->precheck()) {
1551            return false;
1552        }
1553
1554        $filename = $this->realpath($filename);
1555        if ($filename === false) {
1556            return false;
1557        }
1558
1559        if (!isset($time)) {
1560            $time = time();
1561        }
1562        if (!isset($atime)) {
1563            $atime = $time;
1564        }
1565
1566        $attr = $this->version < 4 ?
1567            pack('N3', NET_SFTP_ATTR_ACCESSTIME, $atime, $time) :
1568            Strings::packSSH2('NQ2', NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, $atime, $time);
1569
1570        $packet = Strings::packSSH2('s', $filename);
1571        $packet .= $this->version >= 5 ?
1572            pack('N2', 0, NET_SFTP_OPEN_OPEN_EXISTING) :
1573            pack('N', NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_EXCL);
1574        $packet .= $attr;
1575
1576        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
1577
1578        $response = $this->get_sftp_packet();
1579        switch ($this->packet_type) {
1580            case NET_SFTP_HANDLE:
1581                return $this->close_handle(substr($response, 4));
1582            case NET_SFTP_STATUS:
1583                $this->logError($response);
1584                break;
1585            default:
1586                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
1587                                                  . 'Got packet type: ' . $this->packet_type);
1588        }
1589
1590        return $this->setstat($filename, $attr, false);
1591    }
1592
1593    /**
1594     * Changes file or directory owner
1595     *
1596     * $uid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string
1597     * would be of the form "user@dns_domain" but it does not need to be.
1598     * `$sftp->getSupportedVersions()['version']` will return the specific version
1599     * that's being used.
1600     *
1601     * Returns true on success or false on error.
1602     *
1603     * @param string $filename
1604     * @param int|string $uid
1605     * @param bool $recursive
1606     * @return bool
1607     */
1608    public function chown($filename, $uid, $recursive = false)
1609    {
1610        /*
1611         quoting <https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.5>,
1612
1613         "To avoid a representation that is tied to a particular underlying
1614          implementation at the client or server, the use of UTF-8 strings has
1615          been chosen.  The string should be of the form "user@dns_domain".
1616          This will allow for a client and server that do not use the same
1617          local representation the ability to translate to a common syntax that
1618          can be interpreted by both.  In the case where there is no
1619          translation available to the client or server, the attribute value
1620          must be constructed without the "@"."
1621
1622         phpseclib _could_ auto append the dns_domain to $uid BUT what if it shouldn't
1623         have one? phpseclib would have no way of knowing so rather than guess phpseclib
1624         will just use whatever value the user provided
1625       */
1626
1627        $attr = $this->version < 4 ?
1628            // quoting <http://www.kernel.org/doc/man-pages/online/pages/man2/chown.2.html>,
1629            // "if the owner or group is specified as -1, then that ID is not changed"
1630            pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1) :
1631            // quoting <https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.5>,
1632            // "If either the owner or group field is zero length, the field should be
1633            //  considered absent, and no change should be made to that specific field
1634            //  during a modification operation"
1635            Strings::packSSH2('Nss', NET_SFTP_ATTR_OWNERGROUP, $uid, '');
1636
1637        return $this->setstat($filename, $attr, $recursive);
1638    }
1639
1640    /**
1641     * Changes file or directory group
1642     *
1643     * $gid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string
1644     * would be of the form "user@dns_domain" but it does not need to be.
1645     * `$sftp->getSupportedVersions()['version']` will return the specific version
1646     * that's being used.
1647     *
1648     * Returns true on success or false on error.
1649     *
1650     * @param string $filename
1651     * @param int|string $gid
1652     * @param bool $recursive
1653     * @return bool
1654     */
1655    public function chgrp($filename, $gid, $recursive = false)
1656    {
1657        $attr = $this->version < 4 ?
1658            pack('N3', NET_SFTP_ATTR_UIDGID, -1, $gid) :
1659            Strings::packSSH2('Nss', NET_SFTP_ATTR_OWNERGROUP, '', $gid);
1660
1661        return $this->setstat($filename, $attr, $recursive);
1662    }
1663
1664    /**
1665     * Set permissions on a file.
1666     *
1667     * Returns the new file permissions on success or false on error.
1668     * If $recursive is true than this just returns true or false.
1669     *
1670     * @param int $mode
1671     * @param string $filename
1672     * @param bool $recursive
1673     * @throws \UnexpectedValueException on receipt of unexpected packets
1674     * @return mixed
1675     */
1676    public function chmod($mode, $filename, $recursive = false)
1677    {
1678        if (is_string($mode) && is_int($filename)) {
1679            $temp = $mode;
1680            $mode = $filename;
1681            $filename = $temp;
1682        }
1683
1684        $attr = pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777);
1685        if (!$this->setstat($filename, $attr, $recursive)) {
1686            return false;
1687        }
1688        if ($recursive) {
1689            return true;
1690        }
1691
1692        $filename = $this->realpath($filename);
1693        // rather than return what the permissions *should* be, we'll return what they actually are.  this will also
1694        // tell us if the file actually exists.
1695        // incidentally, SFTPv4+ adds an additional 32-bit integer field - flags - to the following:
1696        $packet = pack('Na*', strlen($filename), $filename);
1697        $this->send_sftp_packet(NET_SFTP_STAT, $packet);
1698
1699        $response = $this->get_sftp_packet();
1700        switch ($this->packet_type) {
1701            case NET_SFTP_ATTRS:
1702                $attrs = $this->parseAttributes($response);
1703                return $attrs['mode'];
1704            case NET_SFTP_STATUS:
1705                $this->logError($response);
1706                return false;
1707        }
1708
1709        throw new \UnexpectedValueException('Expected NET_SFTP_ATTRS or NET_SFTP_STATUS. '
1710                                          . 'Got packet type: ' . $this->packet_type);
1711    }
1712
1713    /**
1714     * Sets information about a file
1715     *
1716     * @param string $filename
1717     * @param string $attr
1718     * @param bool $recursive
1719     * @throws \UnexpectedValueException on receipt of unexpected packets
1720     * @return bool
1721     */
1722    private function setstat($filename, $attr, $recursive)
1723    {
1724        if (!$this->precheck()) {
1725            return false;
1726        }
1727
1728        $filename = $this->realpath($filename);
1729        if ($filename === false) {
1730            return false;
1731        }
1732
1733        $this->remove_from_stat_cache($filename);
1734
1735        if ($recursive) {
1736            $i = 0;
1737            $result = $this->setstat_recursive($filename, $attr, $i);
1738            $this->read_put_responses($i);
1739            return $result;
1740        }
1741
1742        $packet = Strings::packSSH2('s', $filename);
1743        $packet .= $this->version >= 4 ?
1744            pack('a*Ca*', substr($attr, 0, 4), NET_SFTP_TYPE_UNKNOWN, substr($attr, 4)) :
1745            $attr;
1746        $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet);
1747
1748        /*
1749         "Because some systems must use separate system calls to set various attributes, it is possible that a failure
1750          response will be returned, but yet some of the attributes may be have been successfully modified.  If possible,
1751          servers SHOULD avoid this situation; however, clients MUST be aware that this is possible."
1752
1753          -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.6
1754        */
1755        $response = $this->get_sftp_packet();
1756        if ($this->packet_type != NET_SFTP_STATUS) {
1757            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
1758                                              . 'Got packet type: ' . $this->packet_type);
1759        }
1760
1761        list($status) = Strings::unpackSSH2('N', $response);
1762        if ($status != NET_SFTP_STATUS_OK) {
1763            $this->logError($response, $status);
1764            return false;
1765        }
1766
1767        return true;
1768    }
1769
1770    /**
1771     * Recursively sets information on directories on the SFTP server
1772     *
1773     * Minimizes directory lookups and SSH_FXP_STATUS requests for speed.
1774     *
1775     * @param string $path
1776     * @param string $attr
1777     * @param int $i
1778     * @return bool
1779     */
1780    private function setstat_recursive($path, $attr, &$i)
1781    {
1782        if (!$this->read_put_responses($i)) {
1783            return false;
1784        }
1785        $i = 0;
1786        $entries = $this->readlist($path, true);
1787
1788        if ($entries === false || is_int($entries)) {
1789            return $this->setstat($path, $attr, false);
1790        }
1791
1792        // normally $entries would have at least . and .. but it might not if the directories
1793        // permissions didn't allow reading
1794        if (empty($entries)) {
1795            return false;
1796        }
1797
1798        unset($entries['.'], $entries['..']);
1799        foreach ($entries as $filename => $props) {
1800            if (!isset($props['type'])) {
1801                return false;
1802            }
1803
1804            $temp = $path . '/' . $filename;
1805            if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) {
1806                if (!$this->setstat_recursive($temp, $attr, $i)) {
1807                    return false;
1808                }
1809            } else {
1810                $packet = Strings::packSSH2('s', $temp);
1811                $packet .= $this->version >= 4 ?
1812                    pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) :
1813                    $attr;
1814                $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet);
1815
1816                $i++;
1817
1818                if ($i >= NET_SFTP_QUEUE_SIZE) {
1819                    if (!$this->read_put_responses($i)) {
1820                        return false;
1821                    }
1822                    $i = 0;
1823                }
1824            }
1825        }
1826
1827        $packet = Strings::packSSH2('s', $path);
1828        $packet .= $this->version >= 4 ?
1829            pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) :
1830            $attr;
1831        $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet);
1832
1833        $i++;
1834
1835        if ($i >= NET_SFTP_QUEUE_SIZE) {
1836            if (!$this->read_put_responses($i)) {
1837                return false;
1838            }
1839            $i = 0;
1840        }
1841
1842        return true;
1843    }
1844
1845    /**
1846     * Return the target of a symbolic link
1847     *
1848     * @param string $link
1849     * @throws \UnexpectedValueException on receipt of unexpected packets
1850     * @return mixed
1851     */
1852    public function readlink($link)
1853    {
1854        if (!$this->precheck()) {
1855            return false;
1856        }
1857
1858        $link = $this->realpath($link);
1859
1860        $this->send_sftp_packet(NET_SFTP_READLINK, Strings::packSSH2('s', $link));
1861
1862        $response = $this->get_sftp_packet();
1863        switch ($this->packet_type) {
1864            case NET_SFTP_NAME:
1865                break;
1866            case NET_SFTP_STATUS:
1867                $this->logError($response);
1868                return false;
1869            default:
1870                throw new \UnexpectedValueException('Expected NET_SFTP_NAME or NET_SFTP_STATUS. '
1871                                                  . 'Got packet type: ' . $this->packet_type);
1872        }
1873
1874        list($count) = Strings::unpackSSH2('N', $response);
1875        // the file isn't a symlink
1876        if (!$count) {
1877            return false;
1878        }
1879
1880        list($filename) = Strings::unpackSSH2('s', $response);
1881
1882        return $filename;
1883    }
1884
1885    /**
1886     * Create a symlink
1887     *
1888     * symlink() creates a symbolic link to the existing target with the specified name link.
1889     *
1890     * @param string $target
1891     * @param string $link
1892     * @throws \UnexpectedValueException on receipt of unexpected packets
1893     * @return bool
1894     */
1895    public function symlink($target, $link)
1896    {
1897        if (!$this->precheck()) {
1898            return false;
1899        }
1900
1901        //$target = $this->realpath($target);
1902        $link = $this->realpath($link);
1903
1904        /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-12.1 :
1905
1906           Changed the SYMLINK packet to be LINK and give it the ability to
1907           create hard links.  Also change it's packet number because many
1908           implementation implemented SYMLINK with the arguments reversed.
1909           Hopefully the new argument names make it clear which way is which.
1910        */
1911        if ($this->version == 6) {
1912            $type = NET_SFTP_LINK;
1913            $packet = Strings::packSSH2('ssC', $link, $target, 1);
1914        } else {
1915            $type = NET_SFTP_SYMLINK;
1916            /* quoting http://bxr.su/OpenBSD/usr.bin/ssh/PROTOCOL#347 :
1917
1918               3.1. sftp: Reversal of arguments to SSH_FXP_SYMLINK
1919
1920               When OpenSSH's sftp-server was implemented, the order of the arguments
1921               to the SSH_FXP_SYMLINK method was inadvertently reversed. Unfortunately,
1922               the reversal was not noticed until the server was widely deployed. Since
1923               fixing this to follow the specification would cause incompatibility, the
1924               current order was retained. For correct operation, clients should send
1925               SSH_FXP_SYMLINK as follows:
1926
1927                   uint32      id
1928                   string      targetpath
1929                   string      linkpath */
1930            $packet = substr($this->server_identifier, 0, 15) == 'SSH-2.0-OpenSSH' ?
1931                Strings::packSSH2('ss', $target, $link) :
1932                Strings::packSSH2('ss', $link, $target);
1933        }
1934        $this->send_sftp_packet($type, $packet);
1935
1936        $response = $this->get_sftp_packet();
1937        if ($this->packet_type != NET_SFTP_STATUS) {
1938            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
1939                                              . 'Got packet type: ' . $this->packet_type);
1940        }
1941
1942        list($status) = Strings::unpackSSH2('N', $response);
1943        if ($status != NET_SFTP_STATUS_OK) {
1944            $this->logError($response, $status);
1945            return false;
1946        }
1947
1948        return true;
1949    }
1950
1951    /**
1952     * Creates a directory.
1953     *
1954     * @param string $dir
1955     * @param int $mode
1956     * @param bool $recursive
1957     * @return bool
1958     */
1959    public function mkdir($dir, $mode = -1, $recursive = false)
1960    {
1961        if (!$this->precheck()) {
1962            return false;
1963        }
1964
1965        $dir = $this->realpath($dir);
1966
1967        if ($recursive) {
1968            $dirs = explode('/', preg_replace('#/(?=/)|/$#', '', $dir));
1969            if (empty($dirs[0])) {
1970                array_shift($dirs);
1971                $dirs[0] = '/' . $dirs[0];
1972            }
1973            for ($i = 0; $i < count($dirs); $i++) {
1974                $temp = array_slice($dirs, 0, $i + 1);
1975                $temp = implode('/', $temp);
1976                $result = $this->mkdir_helper($temp, $mode);
1977            }
1978            return $result;
1979        }
1980
1981        return $this->mkdir_helper($dir, $mode);
1982    }
1983
1984    /**
1985     * Helper function for directory creation
1986     *
1987     * @param string $dir
1988     * @param int $mode
1989     * @return bool
1990     */
1991    private function mkdir_helper($dir, $mode)
1992    {
1993        // send SSH_FXP_MKDIR without any attributes (that's what the \0\0\0\0 is doing)
1994        $this->send_sftp_packet(NET_SFTP_MKDIR, Strings::packSSH2('s', $dir) . "\0\0\0\0");
1995
1996        $response = $this->get_sftp_packet();
1997        if ($this->packet_type != NET_SFTP_STATUS) {
1998            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
1999                                              . 'Got packet type: ' . $this->packet_type);
2000        }
2001
2002        list($status) = Strings::unpackSSH2('N', $response);
2003        if ($status != NET_SFTP_STATUS_OK) {
2004            $this->logError($response, $status);
2005            return false;
2006        }
2007
2008        if ($mode !== -1) {
2009            $this->chmod($mode, $dir);
2010        }
2011
2012        return true;
2013    }
2014
2015    /**
2016     * Removes a directory.
2017     *
2018     * @param string $dir
2019     * @throws \UnexpectedValueException on receipt of unexpected packets
2020     * @return bool
2021     */
2022    public function rmdir($dir)
2023    {
2024        if (!$this->precheck()) {
2025            return false;
2026        }
2027
2028        $dir = $this->realpath($dir);
2029        if ($dir === false) {
2030            return false;
2031        }
2032
2033        $this->send_sftp_packet(NET_SFTP_RMDIR, Strings::packSSH2('s', $dir));
2034
2035        $response = $this->get_sftp_packet();
2036        if ($this->packet_type != NET_SFTP_STATUS) {
2037            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2038                                              . 'Got packet type: ' . $this->packet_type);
2039        }
2040
2041        list($status) = Strings::unpackSSH2('N', $response);
2042        if ($status != NET_SFTP_STATUS_OK) {
2043            // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED?
2044            $this->logError($response, $status);
2045            return false;
2046        }
2047
2048        $this->remove_from_stat_cache($dir);
2049        // the following will do a soft delete, which would be useful if you deleted a file
2050        // and then tried to do a stat on the deleted file. the above, in contrast, does
2051        // a hard delete
2052        //$this->update_stat_cache($dir, false);
2053
2054        return true;
2055    }
2056
2057    /**
2058     * Uploads a file to the SFTP server.
2059     *
2060     * By default, \phpseclib3\Net\SFTP::put() does not read from the local filesystem.  $data is dumped directly into $remote_file.
2061     * So, for example, if you set $data to 'filename.ext' and then do \phpseclib3\Net\SFTP::get(), you will get a file, twelve bytes
2062     * long, containing 'filename.ext' as its contents.
2063     *
2064     * Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior.  With self::SOURCE_LOCAL_FILE, $remote_file will
2065     * contain as many bytes as filename.ext does on your local filesystem.  If your filename.ext is 1MB then that is how
2066     * large $remote_file will be, as well.
2067     *
2068     * Setting $mode to self::SOURCE_CALLBACK will use $data as callback function, which gets only one parameter -- number
2069     * of bytes to return, and returns a string if there is some data or null if there is no more data
2070     *
2071     * If $data is a resource then it'll be used as a resource instead.
2072     *
2073     * Currently, only binary mode is supported.  As such, if the line endings need to be adjusted, you will need to take
2074     * care of that, yourself.
2075     *
2076     * $mode can take an additional two parameters - self::RESUME and self::RESUME_START. These are bitwise AND'd with
2077     * $mode. So if you want to resume upload of a 300mb file on the local file system you'd set $mode to the following:
2078     *
2079     * self::SOURCE_LOCAL_FILE | self::RESUME
2080     *
2081     * If you wanted to simply append the full contents of a local file to the full contents of a remote file you'd replace
2082     * self::RESUME with self::RESUME_START.
2083     *
2084     * If $mode & (self::RESUME | self::RESUME_START) then self::RESUME_START will be assumed.
2085     *
2086     * $start and $local_start give you more fine grained control over this process and take precident over self::RESUME
2087     * when they're non-negative. ie. $start could let you write at the end of a file (like self::RESUME) or in the middle
2088     * of one. $local_start could let you start your reading from the end of a file (like self::RESUME_START) or in the
2089     * middle of one.
2090     *
2091     * Setting $local_start to > 0 or $mode | self::RESUME_START doesn't do anything unless $mode | self::SOURCE_LOCAL_FILE.
2092     *
2093     * {@internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - \phpseclib3\Net\SFTP::setMode().}
2094     *
2095     * @param string $remote_file
2096     * @param string|resource $data
2097     * @param int $mode
2098     * @param int $start
2099     * @param int $local_start
2100     * @param callable|null $progressCallback
2101     * @throws \UnexpectedValueException on receipt of unexpected packets
2102     * @throws \BadFunctionCallException if you're uploading via a callback and the callback function is invalid
2103     * @throws \phpseclib3\Exception\FileNotFoundException if you're uploading via a file and the file doesn't exist
2104     * @return bool
2105     */
2106    public function put($remote_file, $data, $mode = self::SOURCE_STRING, $start = -1, $local_start = -1, $progressCallback = null)
2107    {
2108        if (!$this->precheck()) {
2109            return false;
2110        }
2111
2112        $remote_file = $this->realpath($remote_file);
2113        if ($remote_file === false) {
2114            return false;
2115        }
2116
2117        $this->remove_from_stat_cache($remote_file);
2118
2119        if ($this->version >= 5) {
2120            $flags = NET_SFTP_OPEN_OPEN_OR_CREATE;
2121        } else {
2122            $flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE;
2123            // according to the SFTP specs, NET_SFTP_OPEN_APPEND should "force all writes to append data at the end of the file."
2124            // in practice, it doesn't seem to do that.
2125            //$flags|= ($mode & self::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE;
2126        }
2127
2128        if ($start >= 0) {
2129            $offset = $start;
2130        } elseif ($mode & (self::RESUME | self::RESUME_START)) {
2131            // if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called
2132            $stat = $this->stat($remote_file);
2133            $offset = $stat !== false && $stat['size'] ? $stat['size'] : 0;
2134        } else {
2135            $offset = 0;
2136            if ($this->version >= 5) {
2137                $flags = NET_SFTP_OPEN_CREATE_TRUNCATE;
2138            } else {
2139                $flags |= NET_SFTP_OPEN_TRUNCATE;
2140            }
2141        }
2142
2143        $this->remove_from_stat_cache($remote_file);
2144
2145        $packet = Strings::packSSH2('s', $remote_file);
2146        $packet .= $this->version >= 5 ?
2147            pack('N3', 0, $flags, 0) :
2148            pack('N2', $flags, 0);
2149        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2150
2151        $response = $this->get_sftp_packet();
2152        switch ($this->packet_type) {
2153            case NET_SFTP_HANDLE:
2154                $handle = substr($response, 4);
2155                break;
2156            case NET_SFTP_STATUS:
2157                $this->logError($response);
2158                return false;
2159            default:
2160                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
2161                                                  . 'Got packet type: ' . $this->packet_type);
2162        }
2163
2164        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3
2165        $dataCallback = false;
2166        switch (true) {
2167            case $mode & self::SOURCE_CALLBACK:
2168                if (!is_callable($data)) {
2169                    throw new \BadFunctionCallException("\$data should be is_callable() if you specify SOURCE_CALLBACK flag");
2170                }
2171                $dataCallback = $data;
2172                // do nothing
2173                break;
2174            case is_resource($data):
2175                $mode = $mode & ~self::SOURCE_LOCAL_FILE;
2176                $info = stream_get_meta_data($data);
2177                if (isset($info['wrapper_type']) && $info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') {
2178                    $fp = fopen('php://memory', 'w+');
2179                    stream_copy_to_stream($data, $fp);
2180                    rewind($fp);
2181                } else {
2182                    $fp = $data;
2183                }
2184                break;
2185            case $mode & self::SOURCE_LOCAL_FILE:
2186                if (!is_file($data)) {
2187                    throw new FileNotFoundException("$data is not a valid file");
2188                }
2189                $fp = @fopen($data, 'rb');
2190                if (!$fp) {
2191                    return false;
2192                }
2193        }
2194
2195        if (isset($fp)) {
2196            $stat = fstat($fp);
2197            $size = !empty($stat) ? $stat['size'] : 0;
2198
2199            if ($local_start >= 0) {
2200                fseek($fp, $local_start);
2201                $size -= $local_start;
2202            } elseif ($mode & self::RESUME) {
2203                fseek($fp, $offset);
2204                $size -= $offset;
2205            }
2206        } elseif ($dataCallback) {
2207            $size = 0;
2208        } else {
2209            $size = strlen($data);
2210        }
2211
2212        $sent = 0;
2213        $size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size;
2214
2215        $sftp_packet_size = $this->max_sftp_packet;
2216        // make the SFTP packet be exactly the SFTP packet size by including the bytes in the NET_SFTP_WRITE packets "header"
2217        $sftp_packet_size -= strlen($handle) + 25;
2218        $i = $j = 0;
2219        while ($dataCallback || ($size === 0 || $sent < $size)) {
2220            if ($dataCallback) {
2221                $temp = $dataCallback($sftp_packet_size);
2222                if (is_null($temp)) {
2223                    break;
2224                }
2225            } else {
2226                $temp = isset($fp) ? fread($fp, $sftp_packet_size) : substr($data, $sent, $sftp_packet_size);
2227                if ($temp === false || $temp === '') {
2228                    break;
2229                }
2230            }
2231
2232            $subtemp = $offset + $sent;
2233            $packet = pack('Na*N3a*', strlen($handle), $handle, $subtemp / 4294967296, $subtemp, strlen($temp), $temp);
2234            try {
2235                $this->send_sftp_packet(NET_SFTP_WRITE, $packet, $j);
2236            } catch (\Exception $e) {
2237                if ($mode & self::SOURCE_LOCAL_FILE) {
2238                    fclose($fp);
2239                }
2240                throw $e;
2241            }
2242            $sent += strlen($temp);
2243            if (is_callable($progressCallback)) {
2244                $progressCallback($sent);
2245            }
2246
2247            $i++;
2248            $j++;
2249            if ($i == NET_SFTP_UPLOAD_QUEUE_SIZE) {
2250                if (!$this->read_put_responses($i)) {
2251                    $i = 0;
2252                    break;
2253                }
2254                $i = 0;
2255            }
2256        }
2257
2258        $result = $this->close_handle($handle);
2259
2260        if (!$this->read_put_responses($i)) {
2261            if ($mode & self::SOURCE_LOCAL_FILE) {
2262                fclose($fp);
2263            }
2264            $this->close_handle($handle);
2265            return false;
2266        }
2267
2268        if ($mode & SFTP::SOURCE_LOCAL_FILE) {
2269            if (isset($fp) && is_resource($fp)) {
2270                fclose($fp);
2271            }
2272
2273            if ($this->preserveTime) {
2274                $stat = stat($data);
2275                $attr = $this->version < 4 ?
2276                    pack('N3', NET_SFTP_ATTR_ACCESSTIME, $stat['atime'], $stat['mtime']) :
2277                    Strings::packSSH2('NQ2', NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, $stat['atime'], $stat['mtime']);
2278                if (!$this->setstat($remote_file, $attr, false)) {
2279                    throw new \RuntimeException('Error setting file time');
2280                }
2281            }
2282        }
2283
2284        return $result;
2285    }
2286
2287    /**
2288     * Reads multiple successive SSH_FXP_WRITE responses
2289     *
2290     * Sending an SSH_FXP_WRITE packet and immediately reading its response isn't as efficient as blindly sending out $i
2291     * SSH_FXP_WRITEs, in succession, and then reading $i responses.
2292     *
2293     * @param int $i
2294     * @return bool
2295     * @throws \UnexpectedValueException on receipt of unexpected packets
2296     */
2297    private function read_put_responses($i)
2298    {
2299        while ($i--) {
2300            $response = $this->get_sftp_packet();
2301            if ($this->packet_type != NET_SFTP_STATUS) {
2302                throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2303                                                  . 'Got packet type: ' . $this->packet_type);
2304            }
2305
2306            list($status) = Strings::unpackSSH2('N', $response);
2307            if ($status != NET_SFTP_STATUS_OK) {
2308                $this->logError($response, $status);
2309                break;
2310            }
2311        }
2312
2313        return $i < 0;
2314    }
2315
2316    /**
2317     * Close handle
2318     *
2319     * @param string $handle
2320     * @return bool
2321     * @throws \UnexpectedValueException on receipt of unexpected packets
2322     */
2323    private function close_handle($handle)
2324    {
2325        $this->send_sftp_packet(NET_SFTP_CLOSE, pack('Na*', strlen($handle), $handle));
2326
2327        // "The client MUST release all resources associated with the handle regardless of the status."
2328        //  -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.3
2329        $response = $this->get_sftp_packet();
2330        if ($this->packet_type != NET_SFTP_STATUS) {
2331            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2332                                              . 'Got packet type: ' . $this->packet_type);
2333        }
2334
2335        list($status) = Strings::unpackSSH2('N', $response);
2336        if ($status != NET_SFTP_STATUS_OK) {
2337            $this->logError($response, $status);
2338            return false;
2339        }
2340
2341        return true;
2342    }
2343
2344    /**
2345     * Downloads a file from the SFTP server.
2346     *
2347     * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if
2348     * the operation was unsuccessful.  If $local_file is defined, returns true or false depending on the success of the
2349     * operation.
2350     *
2351     * $offset and $length can be used to download files in chunks.
2352     *
2353     * @param string $remote_file
2354     * @param string|bool|resource|callable $local_file
2355     * @param int $offset
2356     * @param int $length
2357     * @param callable|null $progressCallback
2358     * @throws \UnexpectedValueException on receipt of unexpected packets
2359     * @return string|bool
2360     */
2361    public function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null)
2362    {
2363        if (!$this->precheck()) {
2364            return false;
2365        }
2366
2367        $remote_file = $this->realpath($remote_file);
2368        if ($remote_file === false) {
2369            return false;
2370        }
2371
2372        $packet = Strings::packSSH2('s', $remote_file);
2373        $packet .= $this->version >= 5 ?
2374            pack('N3', 0, NET_SFTP_OPEN_OPEN_EXISTING, 0) :
2375            pack('N2', NET_SFTP_OPEN_READ, 0);
2376        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2377
2378        $response = $this->get_sftp_packet();
2379        switch ($this->packet_type) {
2380            case NET_SFTP_HANDLE:
2381                $handle = substr($response, 4);
2382                break;
2383            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2384                $this->logError($response);
2385                return false;
2386            default:
2387                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
2388                                                  . 'Got packet type: ' . $this->packet_type);
2389        }
2390
2391        if (is_resource($local_file)) {
2392            $fp = $local_file;
2393            $stat = fstat($fp);
2394            $res_offset = $stat['size'];
2395        } else {
2396            $res_offset = 0;
2397            if ($local_file !== false && !is_callable($local_file)) {
2398                $fp = fopen($local_file, 'wb');
2399                if (!$fp) {
2400                    return false;
2401                }
2402            } else {
2403                $content = '';
2404            }
2405        }
2406
2407        $fclose_check = $local_file !== false && !is_callable($local_file) && !is_resource($local_file);
2408
2409        $start = $offset;
2410        $read = 0;
2411        while (true) {
2412            $i = 0;
2413
2414            while ($i < NET_SFTP_QUEUE_SIZE && ($length < 0 || $read < $length)) {
2415                $tempoffset = $start + $read;
2416
2417                $packet_size = $length > 0 ? min($this->max_sftp_packet, $length - $read) : $this->max_sftp_packet;
2418
2419                $packet = Strings::packSSH2('sN3', $handle, $tempoffset / 4294967296, $tempoffset, $packet_size);
2420                try {
2421                    $this->send_sftp_packet(NET_SFTP_READ, $packet, $i);
2422                } catch (\Exception $e) {
2423                    if ($fclose_check) {
2424                        fclose($fp);
2425                    }
2426                    throw $e;
2427                }
2428                $packet = null;
2429                $read += $packet_size;
2430                $i++;
2431            }
2432
2433            if (!$i) {
2434                break;
2435            }
2436
2437            $packets_sent = $i - 1;
2438
2439            $clear_responses = false;
2440            while ($i > 0) {
2441                $i--;
2442
2443                if ($clear_responses) {
2444                    $this->get_sftp_packet($packets_sent - $i);
2445                    continue;
2446                } else {
2447                    $response = $this->get_sftp_packet($packets_sent - $i);
2448                }
2449
2450                switch ($this->packet_type) {
2451                    case NET_SFTP_DATA:
2452                        $temp = substr($response, 4);
2453                        $offset += strlen($temp);
2454                        if ($local_file === false) {
2455                            $content .= $temp;
2456                        } elseif (is_callable($local_file)) {
2457                            $local_file($temp);
2458                        } else {
2459                            fputs($fp, $temp);
2460                        }
2461                        if (is_callable($progressCallback)) {
2462                            call_user_func($progressCallback, $offset);
2463                        }
2464                        $temp = null;
2465                        break;
2466                    case NET_SFTP_STATUS:
2467                        // could, in theory, return false if !strlen($content) but we'll hold off for the time being
2468                        $this->logError($response);
2469                        $clear_responses = true; // don't break out of the loop yet, so we can read the remaining responses
2470                        break;
2471                    default:
2472                        if ($fclose_check) {
2473                            fclose($fp);
2474                        }
2475                        if ($this->channel_close) {
2476                            $this->partial_init = false;
2477                            $this->init_sftp_connection();
2478                            return false;
2479                        } else {
2480                            throw new \UnexpectedValueException('Expected NET_SFTP_DATA or NET_SFTP_STATUS. '
2481                                                              . 'Got packet type: ' . $this->packet_type);
2482                        }
2483                }
2484                $response = null;
2485            }
2486
2487            if ($clear_responses) {
2488                break;
2489            }
2490        }
2491
2492        if ($fclose_check) {
2493            fclose($fp);
2494
2495            if ($this->preserveTime) {
2496                $stat = $this->stat($remote_file);
2497                touch($local_file, $stat['mtime'], $stat['atime']);
2498            }
2499        }
2500
2501        if (!$this->close_handle($handle)) {
2502            return false;
2503        }
2504
2505        // if $content isn't set that means a file was written to
2506        return isset($content) ? $content : true;
2507    }
2508
2509    /**
2510     * Deletes a file on the SFTP server.
2511     *
2512     * @param string $path
2513     * @param bool $recursive
2514     * @return bool
2515     * @throws \UnexpectedValueException on receipt of unexpected packets
2516     */
2517    public function delete($path, $recursive = true)
2518    {
2519        if (!$this->precheck()) {
2520            return false;
2521        }
2522
2523        if (is_object($path)) {
2524            // It's an object. Cast it as string before we check anything else.
2525            $path = (string) $path;
2526        }
2527
2528        if (!is_string($path) || $path == '') {
2529            return false;
2530        }
2531
2532        $path = $this->realpath($path);
2533        if ($path === false) {
2534            return false;
2535        }
2536
2537        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3
2538        $this->send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($path), $path));
2539
2540        $response = $this->get_sftp_packet();
2541        if ($this->packet_type != NET_SFTP_STATUS) {
2542            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2543                                              . 'Got packet type: ' . $this->packet_type);
2544        }
2545
2546        // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2547        list($status) = Strings::unpackSSH2('N', $response);
2548        if ($status != NET_SFTP_STATUS_OK) {
2549            $this->logError($response, $status);
2550            if (!$recursive) {
2551                return false;
2552            }
2553
2554            $i = 0;
2555            $result = $this->delete_recursive($path, $i);
2556            $this->read_put_responses($i);
2557            return $result;
2558        }
2559
2560        $this->remove_from_stat_cache($path);
2561
2562        return true;
2563    }
2564
2565    /**
2566     * Recursively deletes directories on the SFTP server
2567     *
2568     * Minimizes directory lookups and SSH_FXP_STATUS requests for speed.
2569     *
2570     * @param string $path
2571     * @param int $i
2572     * @return bool
2573     */
2574    private function delete_recursive($path, &$i)
2575    {
2576        if (!$this->read_put_responses($i)) {
2577            return false;
2578        }
2579        $i = 0;
2580        $entries = $this->readlist($path, true);
2581
2582        // The folder does not exist at all, so we cannot delete it.
2583        if ($entries === NET_SFTP_STATUS_NO_SUCH_FILE) {
2584            return false;
2585        }
2586
2587        // Normally $entries would have at least . and .. but it might not if the directories
2588        // permissions didn't allow reading. If this happens then default to an empty list of files.
2589        if ($entries === false || is_int($entries)) {
2590            $entries = [];
2591        }
2592
2593        unset($entries['.'], $entries['..']);
2594        foreach ($entries as $filename => $props) {
2595            if (!isset($props['type'])) {
2596                return false;
2597            }
2598
2599            $temp = $path . '/' . $filename;
2600            if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) {
2601                if (!$this->delete_recursive($temp, $i)) {
2602                    return false;
2603                }
2604            } else {
2605                $this->send_sftp_packet(NET_SFTP_REMOVE, Strings::packSSH2('s', $temp));
2606                $this->remove_from_stat_cache($temp);
2607
2608                $i++;
2609
2610                if ($i >= NET_SFTP_QUEUE_SIZE) {
2611                    if (!$this->read_put_responses($i)) {
2612                        return false;
2613                    }
2614                    $i = 0;
2615                }
2616            }
2617        }
2618
2619        $this->send_sftp_packet(NET_SFTP_RMDIR, Strings::packSSH2('s', $path));
2620        $this->remove_from_stat_cache($path);
2621
2622        $i++;
2623
2624        if ($i >= NET_SFTP_QUEUE_SIZE) {
2625            if (!$this->read_put_responses($i)) {
2626                return false;
2627            }
2628            $i = 0;
2629        }
2630
2631        return true;
2632    }
2633
2634    /**
2635     * Checks whether a file or directory exists
2636     *
2637     * @param string $path
2638     * @return bool
2639     */
2640    public function file_exists($path)
2641    {
2642        if ($this->use_stat_cache) {
2643            if (!$this->precheck()) {
2644                return false;
2645            }
2646
2647            $path = $this->realpath($path);
2648
2649            $result = $this->query_stat_cache($path);
2650
2651            if (isset($result)) {
2652                // return true if $result is an array or if it's an stdClass object
2653                return $result !== false;
2654            }
2655        }
2656
2657        return $this->stat($path) !== false;
2658    }
2659
2660    /**
2661     * Tells whether the filename is a directory
2662     *
2663     * @param string $path
2664     * @return bool
2665     */
2666    public function is_dir($path)
2667    {
2668        $result = $this->get_stat_cache_prop($path, 'type');
2669        if ($result === false) {
2670            return false;
2671        }
2672        return $result === NET_SFTP_TYPE_DIRECTORY;
2673    }
2674
2675    /**
2676     * Tells whether the filename is a regular file
2677     *
2678     * @param string $path
2679     * @return bool
2680     */
2681    public function is_file($path)
2682    {
2683        $result = $this->get_stat_cache_prop($path, 'type');
2684        if ($result === false) {
2685            return false;
2686        }
2687        return $result === NET_SFTP_TYPE_REGULAR;
2688    }
2689
2690    /**
2691     * Tells whether the filename is a symbolic link
2692     *
2693     * @param string $path
2694     * @return bool
2695     */
2696    public function is_link($path)
2697    {
2698        $result = $this->get_lstat_cache_prop($path, 'type');
2699        if ($result === false) {
2700            return false;
2701        }
2702        return $result === NET_SFTP_TYPE_SYMLINK;
2703    }
2704
2705    /**
2706     * Tells whether a file exists and is readable
2707     *
2708     * @param string $path
2709     * @return bool
2710     */
2711    public function is_readable($path)
2712    {
2713        if (!$this->precheck()) {
2714            return false;
2715        }
2716
2717        $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_READ, 0);
2718        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2719
2720        $response = $this->get_sftp_packet();
2721        switch ($this->packet_type) {
2722            case NET_SFTP_HANDLE:
2723                return true;
2724            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2725                return false;
2726            default:
2727                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
2728                                                  . 'Got packet type: ' . $this->packet_type);
2729        }
2730    }
2731
2732    /**
2733     * Tells whether the filename is writable
2734     *
2735     * @param string $path
2736     * @return bool
2737     */
2738    public function is_writable($path)
2739    {
2740        if (!$this->precheck()) {
2741            return false;
2742        }
2743
2744        $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_WRITE, 0);
2745        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2746
2747        $response = $this->get_sftp_packet();
2748        switch ($this->packet_type) {
2749            case NET_SFTP_HANDLE:
2750                return true;
2751            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2752                return false;
2753            default:
2754                throw new \UnexpectedValueException('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS. '
2755                                                  . 'Got packet type: ' . $this->packet_type);
2756        }
2757    }
2758
2759    /**
2760     * Tells whether the filename is writeable
2761     *
2762     * Alias of is_writable
2763     *
2764     * @param string $path
2765     * @return bool
2766     */
2767    public function is_writeable($path)
2768    {
2769        return $this->is_writable($path);
2770    }
2771
2772    /**
2773     * Gets last access time of file
2774     *
2775     * @param string $path
2776     * @return mixed
2777     */
2778    public function fileatime($path)
2779    {
2780        return $this->get_stat_cache_prop($path, 'atime');
2781    }
2782
2783    /**
2784     * Gets file modification time
2785     *
2786     * @param string $path
2787     * @return mixed
2788     */
2789    public function filemtime($path)
2790    {
2791        return $this->get_stat_cache_prop($path, 'mtime');
2792    }
2793
2794    /**
2795     * Gets file permissions
2796     *
2797     * @param string $path
2798     * @return mixed
2799     */
2800    public function fileperms($path)
2801    {
2802        return $this->get_stat_cache_prop($path, 'mode');
2803    }
2804
2805    /**
2806     * Gets file owner
2807     *
2808     * @param string $path
2809     * @return mixed
2810     */
2811    public function fileowner($path)
2812    {
2813        return $this->get_stat_cache_prop($path, 'uid');
2814    }
2815
2816    /**
2817     * Gets file group
2818     *
2819     * @param string $path
2820     * @return mixed
2821     */
2822    public function filegroup($path)
2823    {
2824        return $this->get_stat_cache_prop($path, 'gid');
2825    }
2826
2827    /**
2828     * Recursively go through rawlist() output to get the total filesize
2829     *
2830     * @return int
2831     */
2832    private static function recursiveFilesize(array $files)
2833    {
2834        $size = 0;
2835        foreach ($files as $name => $file) {
2836            if ($name == '.' || $name == '..') {
2837                continue;
2838            }
2839            $size += is_array($file) ?
2840                self::recursiveFilesize($file) :
2841                $file->size;
2842        }
2843        return $size;
2844    }
2845
2846    /**
2847     * Gets file size
2848     *
2849     * @param string $path
2850     * @param bool $recursive
2851     * @return mixed
2852     */
2853    public function filesize($path, $recursive = false)
2854    {
2855        return !$recursive || $this->filetype($path) != 'dir' ?
2856            $this->get_stat_cache_prop($path, 'size') :
2857            self::recursiveFilesize($this->rawlist($path, true));
2858    }
2859
2860    /**
2861     * Gets file type
2862     *
2863     * @param string $path
2864     * @return string|false
2865     */
2866    public function filetype($path)
2867    {
2868        $type = $this->get_stat_cache_prop($path, 'type');
2869        if ($type === false) {
2870            return false;
2871        }
2872
2873        switch ($type) {
2874            case NET_SFTP_TYPE_BLOCK_DEVICE:
2875                return 'block';
2876            case NET_SFTP_TYPE_CHAR_DEVICE:
2877                return 'char';
2878            case NET_SFTP_TYPE_DIRECTORY:
2879                return 'dir';
2880            case NET_SFTP_TYPE_FIFO:
2881                return 'fifo';
2882            case NET_SFTP_TYPE_REGULAR:
2883                return 'file';
2884            case NET_SFTP_TYPE_SYMLINK:
2885                return 'link';
2886            default:
2887                return false;
2888        }
2889    }
2890
2891    /**
2892     * Return a stat properity
2893     *
2894     * Uses cache if appropriate.
2895     *
2896     * @param string $path
2897     * @param string $prop
2898     * @return mixed
2899     */
2900    private function get_stat_cache_prop($path, $prop)
2901    {
2902        return $this->get_xstat_cache_prop($path, $prop, 'stat');
2903    }
2904
2905    /**
2906     * Return an lstat properity
2907     *
2908     * Uses cache if appropriate.
2909     *
2910     * @param string $path
2911     * @param string $prop
2912     * @return mixed
2913     */
2914    private function get_lstat_cache_prop($path, $prop)
2915    {
2916        return $this->get_xstat_cache_prop($path, $prop, 'lstat');
2917    }
2918
2919    /**
2920     * Return a stat or lstat properity
2921     *
2922     * Uses cache if appropriate.
2923     *
2924     * @param string $path
2925     * @param string $prop
2926     * @param string $type
2927     * @return mixed
2928     */
2929    private function get_xstat_cache_prop($path, $prop, $type)
2930    {
2931        if (!$this->precheck()) {
2932            return false;
2933        }
2934
2935        if ($this->use_stat_cache) {
2936            $path = $this->realpath($path);
2937
2938            $result = $this->query_stat_cache($path);
2939
2940            if (is_object($result) && isset($result->$type)) {
2941                return $result->{$type}[$prop];
2942            }
2943        }
2944
2945        $result = $this->$type($path);
2946
2947        if ($result === false || !isset($result[$prop])) {
2948            return false;
2949        }
2950
2951        return $result[$prop];
2952    }
2953
2954    /**
2955     * Renames a file or a directory on the SFTP server.
2956     *
2957     * If the file already exists this will return false
2958     *
2959     * @param string $oldname
2960     * @param string $newname
2961     * @return bool
2962     * @throws \UnexpectedValueException on receipt of unexpected packets
2963     */
2964    public function rename($oldname, $newname)
2965    {
2966        if (!$this->precheck()) {
2967            return false;
2968        }
2969
2970        $oldname = $this->realpath($oldname);
2971        $newname = $this->realpath($newname);
2972        if ($oldname === false || $newname === false) {
2973            return false;
2974        }
2975
2976        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3
2977        $packet = Strings::packSSH2('ss', $oldname, $newname);
2978        if ($this->version >= 5) {
2979            /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-6.5 ,
2980
2981               'flags' is 0 or a combination of:
2982
2983                   SSH_FXP_RENAME_OVERWRITE  0x00000001
2984                   SSH_FXP_RENAME_ATOMIC     0x00000002
2985                   SSH_FXP_RENAME_NATIVE     0x00000004
2986
2987               (none of these are currently supported) */
2988            $packet .= "\0\0\0\0";
2989        }
2990        $this->send_sftp_packet(NET_SFTP_RENAME, $packet);
2991
2992        $response = $this->get_sftp_packet();
2993        if ($this->packet_type != NET_SFTP_STATUS) {
2994            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2995                                              . 'Got packet type: ' . $this->packet_type);
2996        }
2997
2998        // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2999        list($status) = Strings::unpackSSH2('N', $response);
3000        if ($status != NET_SFTP_STATUS_OK) {
3001            $this->logError($response, $status);
3002            return false;
3003        }
3004
3005        // don't move the stat cache entry over since this operation could very well change the
3006        // atime and mtime attributes
3007        //$this->update_stat_cache($newname, $this->query_stat_cache($oldname));
3008        $this->remove_from_stat_cache($oldname);
3009        $this->remove_from_stat_cache($newname);
3010
3011        return true;
3012    }
3013
3014    /**
3015     * Parse Time
3016     *
3017     * See '7.7.  Times' of draft-ietf-secsh-filexfer-13 for more info.
3018     *
3019     * @param string $key
3020     * @param int $flags
3021     * @param string $response
3022     * @return array
3023     */
3024    private function parseTime($key, $flags, &$response)
3025    {
3026        $attr = [];
3027        list($attr[$key]) = Strings::unpackSSH2('Q', $response);
3028        if ($flags & NET_SFTP_ATTR_SUBSECOND_TIMES) {
3029            list($attr[$key . '-nseconds']) = Strings::unpackSSH2('N', $response);
3030        }
3031        return $attr;
3032    }
3033
3034    /**
3035     * Parse Attributes
3036     *
3037     * See '7.  File Attributes' of draft-ietf-secsh-filexfer-13 for more info.
3038     *
3039     * @param string $response
3040     * @return array
3041     */
3042    protected function parseAttributes(&$response)
3043    {
3044        if ($this->version >= 4) {
3045            list($flags, $attr['type']) = Strings::unpackSSH2('NC', $response);
3046        } else {
3047            list($flags) = Strings::unpackSSH2('N', $response);
3048        }
3049
3050        foreach (self::$attributes as $key => $value) {
3051            switch ($flags & $key) {
3052                case NET_SFTP_ATTR_UIDGID:
3053                    if ($this->version > 3) {
3054                        continue 2;
3055                    }
3056                    break;
3057                case NET_SFTP_ATTR_CREATETIME:
3058                case NET_SFTP_ATTR_MODIFYTIME:
3059                case NET_SFTP_ATTR_ACL:
3060                case NET_SFTP_ATTR_OWNERGROUP:
3061                case NET_SFTP_ATTR_SUBSECOND_TIMES:
3062                    if ($this->version < 4) {
3063                        continue 2;
3064                    }
3065                    break;
3066                case NET_SFTP_ATTR_BITS:
3067                    if ($this->version < 5) {
3068                        continue 2;
3069                    }
3070                    break;
3071                case NET_SFTP_ATTR_ALLOCATION_SIZE:
3072                case NET_SFTP_ATTR_TEXT_HINT:
3073                case NET_SFTP_ATTR_MIME_TYPE:
3074                case NET_SFTP_ATTR_LINK_COUNT:
3075                case NET_SFTP_ATTR_UNTRANSLATED_NAME:
3076                case NET_SFTP_ATTR_CTIME:
3077                    if ($this->version < 6) {
3078                        continue 2;
3079                    }
3080            }
3081            switch ($flags & $key) {
3082                case NET_SFTP_ATTR_SIZE:             // 0x00000001
3083                    // The size attribute is defined as an unsigned 64-bit integer.
3084                    // The following will use floats on 32-bit platforms, if necessary.
3085                    // As can be seen in the BigInteger class, floats are generally
3086                    // IEEE 754 binary64 "double precision" on such platforms and
3087                    // as such can represent integers of at least 2^50 without loss
3088                    // of precision. Interpreted in filesize, 2^50 bytes = 1024 TiB.
3089                    list($attr['size']) = Strings::unpackSSH2('Q', $response);
3090                    break;
3091                case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 only)
3092                    list($attr['uid'], $attr['gid']) = Strings::unpackSSH2('NN', $response);
3093                    break;
3094                case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004
3095                    list($attr['mode']) = Strings::unpackSSH2('N', $response);
3096                    $fileType = $this->parseMode($attr['mode']);
3097                    if ($this->version < 4 && $fileType !== false) {
3098                        $attr += ['type' => $fileType];
3099                    }
3100                    break;
3101                case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008
3102                    if ($this->version >= 4) {
3103                        $attr += $this->parseTime('atime', $flags, $response);
3104                        break;
3105                    }
3106                    list($attr['atime'], $attr['mtime']) = Strings::unpackSSH2('NN', $response);
3107                    break;
3108                case NET_SFTP_ATTR_CREATETIME:       // 0x00000010 (SFTPv4+)
3109                    $attr += $this->parseTime('createtime', $flags, $response);
3110                    break;
3111                case NET_SFTP_ATTR_MODIFYTIME:       // 0x00000020
3112                    $attr += $this->parseTime('mtime', $flags, $response);
3113                    break;
3114                case NET_SFTP_ATTR_ACL:              // 0x00000040
3115                    // access control list
3116                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-04#section-5.7
3117                    // currently unsupported
3118                    list($count) = Strings::unpackSSH2('N', $response);
3119                    for ($i = 0; $i < $count; $i++) {
3120                        list($type, $flag, $mask, $who) = Strings::unpackSSH2('N3s', $result);
3121                    }
3122                    break;
3123                case NET_SFTP_ATTR_OWNERGROUP:       // 0x00000080
3124                    list($attr['owner'], $attr['$group']) = Strings::unpackSSH2('ss', $response);
3125                    break;
3126                case NET_SFTP_ATTR_SUBSECOND_TIMES:  // 0x00000100
3127                    break;
3128                case NET_SFTP_ATTR_BITS:             // 0x00000200 (SFTPv5+)
3129                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-5.8
3130                    // currently unsupported
3131                    // tells if you file is:
3132                    // readonly, system, hidden, case inensitive, archive, encrypted, compressed, sparse
3133                    // append only, immutable, sync
3134                    list($attrib_bits, $attrib_bits_valid) = Strings::unpackSSH2('N2', $response);
3135                    // if we were actually gonna implement the above it ought to be
3136                    // $attr['attrib-bits'] and $attr['attrib-bits-valid']
3137                    // eg. - instead of _
3138                    break;
3139                case NET_SFTP_ATTR_ALLOCATION_SIZE:  // 0x00000400 (SFTPv6+)
3140                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.4
3141                    // represents the number of bytes that the file consumes on the disk. will
3142                    // usually be larger than the 'size' field
3143                    list($attr['allocation-size']) = Strings::unpackSSH2('Q', $response);
3144                    break;
3145                case NET_SFTP_ATTR_TEXT_HINT:        // 0x00000800
3146                    // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.10
3147                    // currently unsupported
3148                    // tells if file is "known text", "guessed text", "known binary", "guessed binary"
3149                    list($text_hint) = Strings::unpackSSH2('C', $response);
3150                    // the above should be $attr['text-hint']
3151                    break;
3152                case NET_SFTP_ATTR_MIME_TYPE:        // 0x00001000
3153                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.11
3154                    list($attr['mime-type']) = Strings::unpackSSH2('s', $response);
3155                    break;
3156                case NET_SFTP_ATTR_LINK_COUNT:       // 0x00002000
3157                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.12
3158                    list($attr['link-count']) = Strings::unpackSSH2('N', $response);
3159                    break;
3160                case NET_SFTP_ATTR_UNTRANSLATED_NAME:// 0x00004000
3161                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.13
3162                    list($attr['untranslated-name']) = Strings::unpackSSH2('s', $response);
3163                    break;
3164                case NET_SFTP_ATTR_CTIME:            // 0x00008000
3165                    // 'ctime' contains the last time the file attributes were changed.  The
3166                    // exact meaning of this field depends on the server.
3167                    $attr += $this->parseTime('ctime', $flags, $response);
3168                    break;
3169                case NET_SFTP_ATTR_EXTENDED: // 0x80000000
3170                    list($count) = Strings::unpackSSH2('N', $response);
3171                    for ($i = 0; $i < $count; $i++) {
3172                        list($key, $value) = Strings::unpackSSH2('ss', $response);
3173                        $attr[$key] = $value;
3174                    }
3175            }
3176        }
3177        return $attr;
3178    }
3179
3180    /**
3181     * Attempt to identify the file type
3182     *
3183     * Quoting the SFTP RFC, "Implementations MUST NOT send bits that are not defined" but they seem to anyway
3184     *
3185     * @param int $mode
3186     * @return int
3187     */
3188    private function parseMode($mode)
3189    {
3190        // values come from http://lxr.free-electrons.com/source/include/uapi/linux/stat.h#L12
3191        // see, also, http://linux.die.net/man/2/stat
3192        switch ($mode & 0170000) {// ie. 1111 0000 0000 0000
3193            case 0000000: // no file type specified - figure out the file type using alternative means
3194                return false;
3195            case 0040000:
3196                return NET_SFTP_TYPE_DIRECTORY;
3197            case 0100000:
3198                return NET_SFTP_TYPE_REGULAR;
3199            case 0120000:
3200                return NET_SFTP_TYPE_SYMLINK;
3201            // new types introduced in SFTPv5+
3202            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2
3203            case 0010000: // named pipe (fifo)
3204                return NET_SFTP_TYPE_FIFO;
3205            case 0020000: // character special
3206                return NET_SFTP_TYPE_CHAR_DEVICE;
3207            case 0060000: // block special
3208                return NET_SFTP_TYPE_BLOCK_DEVICE;
3209            case 0140000: // socket
3210                return NET_SFTP_TYPE_SOCKET;
3211            case 0160000: // whiteout
3212                // "SPECIAL should be used for files that are of
3213                //  a known type which cannot be expressed in the protocol"
3214                return NET_SFTP_TYPE_SPECIAL;
3215            default:
3216                return NET_SFTP_TYPE_UNKNOWN;
3217        }
3218    }
3219
3220    /**
3221     * Parse Longname
3222     *
3223     * SFTPv3 doesn't provide any easy way of identifying a file type.  You could try to open
3224     * a file as a directory and see if an error is returned or you could try to parse the
3225     * SFTPv3-specific longname field of the SSH_FXP_NAME packet.  That's what this function does.
3226     * The result is returned using the
3227     * {@link http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 SFTPv4 type constants}.
3228     *
3229     * If the longname is in an unrecognized format bool(false) is returned.
3230     *
3231     * @param string $longname
3232     * @return mixed
3233     */
3234    private function parseLongname($longname)
3235    {
3236        // http://en.wikipedia.org/wiki/Unix_file_types
3237        // http://en.wikipedia.org/wiki/Filesystem_permissions#Notation_of_traditional_Unix_permissions
3238        if (preg_match('#^[^/]([r-][w-][xstST-]){3}#', $longname)) {
3239            switch ($longname[0]) {
3240                case '-':
3241                    return NET_SFTP_TYPE_REGULAR;
3242                case 'd':
3243                    return NET_SFTP_TYPE_DIRECTORY;
3244                case 'l':
3245                    return NET_SFTP_TYPE_SYMLINK;
3246                default:
3247                    return NET_SFTP_TYPE_SPECIAL;
3248            }
3249        }
3250
3251        return false;
3252    }
3253
3254    /**
3255     * Sends SFTP Packets
3256     *
3257     * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info.
3258     *
3259     * @param int $type
3260     * @param string $data
3261     * @param int $request_id
3262     * @see self::_get_sftp_packet()
3263     * @see self::send_channel_packet()
3264     * @return void
3265     */
3266    private function send_sftp_packet($type, $data, $request_id = 1)
3267    {
3268        // in SSH2.php the timeout is cumulative per function call. eg. exec() will
3269        // timeout after 10s. but for SFTP.php it's cumulative per packet
3270        $this->curTimeout = $this->timeout;
3271
3272        $packet = $this->use_request_id ?
3273            pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) :
3274            pack('NCa*', strlen($data) + 1, $type, $data);
3275
3276        $start = microtime(true);
3277        $this->send_channel_packet(self::CHANNEL, $packet);
3278        $stop = microtime(true);
3279
3280        if (defined('NET_SFTP_LOGGING')) {
3281            $packet_type = '-> ' . self::$packet_types[$type] .
3282                           ' (' . round($stop - $start, 4) . 's)';
3283            $this->append_log($packet_type, $data);
3284        }
3285    }
3286
3287    /**
3288     * Resets a connection for re-use
3289     *
3290     * @param int $reason
3291     */
3292    protected function reset_connection($reason)
3293    {
3294        parent::reset_connection($reason);
3295        $this->use_request_id = false;
3296        $this->pwd = false;
3297        $this->requestBuffer = [];
3298        $this->partial_init = false;
3299    }
3300
3301    /**
3302     * Receives SFTP Packets
3303     *
3304     * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info.
3305     *
3306     * Incidentally, the number of SSH_MSG_CHANNEL_DATA messages has no bearing on the number of SFTP packets present.
3307     * There can be one SSH_MSG_CHANNEL_DATA messages containing two SFTP packets or there can be two SSH_MSG_CHANNEL_DATA
3308     * messages containing one SFTP packet.
3309     *
3310     * @see self::_send_sftp_packet()
3311     * @return string
3312     */
3313    private function get_sftp_packet($request_id = null)
3314    {
3315        $this->channel_close = false;
3316
3317        if (isset($request_id) && isset($this->requestBuffer[$request_id])) {
3318            $this->packet_type = $this->requestBuffer[$request_id]['packet_type'];
3319            $temp = $this->requestBuffer[$request_id]['packet'];
3320            unset($this->requestBuffer[$request_id]);
3321            return $temp;
3322        }
3323
3324        // in SSH2.php the timeout is cumulative per function call. eg. exec() will
3325        // timeout after 10s. but for SFTP.php it's cumulative per packet
3326        $this->curTimeout = $this->timeout;
3327
3328        $start = microtime(true);
3329
3330        // SFTP packet length
3331        while (strlen($this->packet_buffer) < 4) {
3332            $temp = $this->get_channel_packet(self::CHANNEL, true);
3333            if ($temp === true) {
3334                if ($this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) {
3335                    $this->channel_close = true;
3336                }
3337                $this->packet_type = false;
3338                $this->packet_buffer = '';
3339                return false;
3340            }
3341            $this->packet_buffer .= $temp;
3342        }
3343        if (strlen($this->packet_buffer) < 4) {
3344            throw new \RuntimeException('Packet is too small');
3345        }
3346        extract(unpack('Nlength', Strings::shift($this->packet_buffer, 4)));
3347        /** @var integer $length */
3348
3349        $tempLength = $length;
3350        $tempLength -= strlen($this->packet_buffer);
3351
3352        // 256 * 1024 is what SFTP_MAX_MSG_LENGTH is set to in OpenSSH's sftp-common.h
3353        if (!$this->allow_arbitrary_length_packets && !$this->use_request_id && $tempLength > 256 * 1024) {
3354            throw new \RuntimeException('Invalid Size');
3355        }
3356
3357        // SFTP packet type and data payload
3358        while ($tempLength > 0) {
3359            $temp = $this->get_channel_packet(self::CHANNEL, true);
3360            if ($temp === true) {
3361                if ($this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) {
3362                    $this->channel_close = true;
3363                }
3364                $this->packet_type = false;
3365                $this->packet_buffer = '';
3366                return false;
3367            }
3368            $this->packet_buffer .= $temp;
3369            $tempLength -= strlen($temp);
3370        }
3371
3372        $stop = microtime(true);
3373
3374        $this->packet_type = ord(Strings::shift($this->packet_buffer));
3375
3376        if ($this->use_request_id) {
3377            extract(unpack('Npacket_id', Strings::shift($this->packet_buffer, 4))); // remove the request id
3378            $length -= 5; // account for the request id and the packet type
3379        } else {
3380            $length -= 1; // account for the packet type
3381        }
3382
3383        $packet = Strings::shift($this->packet_buffer, $length);
3384
3385        if (defined('NET_SFTP_LOGGING')) {
3386            $packet_type = '<- ' . self::$packet_types[$this->packet_type] .
3387                           ' (' . round($stop - $start, 4) . 's)';
3388            $this->append_log($packet_type, $packet);
3389        }
3390
3391        if (isset($request_id) && $this->use_request_id && $packet_id != $request_id) {
3392            $this->requestBuffer[$packet_id] = [
3393                'packet_type' => $this->packet_type,
3394                'packet' => $packet
3395            ];
3396            return $this->get_sftp_packet($request_id);
3397        }
3398
3399        return $packet;
3400    }
3401
3402    /**
3403     * Logs data packets
3404     *
3405     * Makes sure that only the last 1MB worth of packets will be logged
3406     *
3407     * @param string $message_number
3408     * @param string $message
3409     */
3410    private function append_log($message_number, $message)
3411    {
3412        $this->append_log_helper(
3413            NET_SFTP_LOGGING,
3414            $message_number,
3415            $message,
3416            $this->packet_type_log,
3417            $this->packet_log,
3418            $this->log_size,
3419            $this->realtime_log_file,
3420            $this->realtime_log_wrap,
3421            $this->realtime_log_size
3422        );
3423    }
3424
3425    /**
3426     * Returns a log of the packets that have been sent and received.
3427     *
3428     * Returns a string if NET_SFTP_LOGGING == self::LOG_COMPLEX, an array if NET_SFTP_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SFTP_LOGGING')
3429     *
3430     * @return array|string|false
3431     */
3432    public function getSFTPLog()
3433    {
3434        if (!defined('NET_SFTP_LOGGING')) {
3435            return false;
3436        }
3437
3438        switch (NET_SFTP_LOGGING) {
3439            case self::LOG_COMPLEX:
3440                return $this->format_log($this->packet_log, $this->packet_type_log);
3441                break;
3442            //case self::LOG_SIMPLE:
3443            default:
3444                return $this->packet_type_log;
3445        }
3446    }
3447
3448    /**
3449     * Returns all errors on the SFTP layer
3450     *
3451     * @return array
3452     */
3453    public function getSFTPErrors()
3454    {
3455        return $this->sftp_errors;
3456    }
3457
3458    /**
3459     * Returns the last error on the SFTP layer
3460     *
3461     * @return string
3462     */
3463    public function getLastSFTPError()
3464    {
3465        return count($this->sftp_errors) ? $this->sftp_errors[count($this->sftp_errors) - 1] : '';
3466    }
3467
3468    /**
3469     * Get supported SFTP versions
3470     *
3471     * @return array
3472     */
3473    public function getSupportedVersions()
3474    {
3475        if (!($this->bitmap & SSH2::MASK_LOGIN)) {
3476            return false;
3477        }
3478
3479        if (!$this->partial_init) {
3480            $this->partial_init_sftp_connection();
3481        }
3482
3483        $temp = ['version' => $this->defaultVersion];
3484        if (isset($this->extensions['versions'])) {
3485            $temp['extensions'] = $this->extensions['versions'];
3486        }
3487        return $temp;
3488    }
3489
3490    /**
3491     * Get supported SFTP versions
3492     *
3493     * @return int|false
3494     */
3495    public function getNegotiatedVersion()
3496    {
3497        if (!$this->precheck()) {
3498            return false;
3499        }
3500
3501        return $this->version;
3502    }
3503
3504    /**
3505     * Set preferred version
3506     *
3507     * If you're preferred version isn't supported then the highest supported
3508     * version of SFTP will be utilized. Set to null or false or int(0) to
3509     * unset the preferred version
3510     *
3511     * @param int $version
3512     */
3513    public function setPreferredVersion($version)
3514    {
3515        $this->preferredVersion = $version;
3516    }
3517
3518    /**
3519     * Disconnect
3520     *
3521     * @param int $reason
3522     * @return false
3523     */
3524    protected function disconnect_helper($reason)
3525    {
3526        $this->pwd = false;
3527        return parent::disconnect_helper($reason);
3528    }
3529
3530    /**
3531     * Enable Date Preservation
3532     *
3533     */
3534    public function enableDatePreservation()
3535    {
3536        $this->preserveTime = true;
3537    }
3538
3539    /**
3540     * Disable Date Preservation
3541     *
3542     */
3543    public function disableDatePreservation()
3544    {
3545        $this->preserveTime = false;
3546    }
3547}
3548