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