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