xref: /dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php (revision 7a48b45e8159fda5cdf0bf07c87cff9744ba1a9c)
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     * @changed in phpseclib 4.0.0
1685     */
1686    public function chmod($mode, $filename, $recursive = false)
1687    {
1688        if (is_string($mode) && is_int($filename)) {
1689            $temp = $mode;
1690            $mode = $filename;
1691            $filename = $temp;
1692        }
1693
1694        $attr = pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777);
1695        if (!$this->setstat($filename, $attr, $recursive)) {
1696            return false;
1697        }
1698        if ($recursive) {
1699            return true;
1700        }
1701
1702        $filename = $this->realpath($filename);
1703        // rather than return what the permissions *should* be, we'll return what they actually are.  this will also
1704        // tell us if the file actually exists.
1705        // incidentally, SFTPv4+ adds an additional 32-bit integer field - flags - to the following:
1706        $packet = pack('Na*', strlen($filename), $filename);
1707        $this->send_sftp_packet(NET_SFTP_STAT, $packet);
1708
1709        $response = $this->get_sftp_packet();
1710        switch ($this->packet_type) {
1711            case NET_SFTP_ATTRS:
1712                $attrs = $this->parseAttributes($response);
1713                return $attrs['mode'];
1714            case NET_SFTP_STATUS:
1715                $this->logError($response);
1716                return false;
1717        }
1718
1719        throw new \UnexpectedValueException('Expected NET_SFTP_ATTRS or NET_SFTP_STATUS. '
1720                                          . 'Got packet type: ' . $this->packet_type);
1721    }
1722
1723    /**
1724     * Sets information about a file
1725     *
1726     * @param string $filename
1727     * @param string $attr
1728     * @param bool $recursive
1729     * @throws \UnexpectedValueException on receipt of unexpected packets
1730     * @return bool
1731     */
1732    private function setstat($filename, $attr, $recursive)
1733    {
1734        if (!$this->precheck()) {
1735            return false;
1736        }
1737
1738        $filename = $this->realpath($filename);
1739        if ($filename === false) {
1740            return false;
1741        }
1742
1743        $this->remove_from_stat_cache($filename);
1744
1745        if ($recursive) {
1746            $i = 0;
1747            $result = $this->setstat_recursive($filename, $attr, $i);
1748            $this->read_put_responses($i);
1749            return $result;
1750        }
1751
1752        $packet = Strings::packSSH2('s', $filename);
1753        $packet .= $this->version >= 4 ?
1754            pack('a*Ca*', substr($attr, 0, 4), NET_SFTP_TYPE_UNKNOWN, substr($attr, 4)) :
1755            $attr;
1756        $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet);
1757
1758        /*
1759         "Because some systems must use separate system calls to set various attributes, it is possible that a failure
1760          response will be returned, but yet some of the attributes may be have been successfully modified.  If possible,
1761          servers SHOULD avoid this situation; however, clients MUST be aware that this is possible."
1762
1763          -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.6
1764        */
1765        $response = $this->get_sftp_packet();
1766        if ($this->packet_type != NET_SFTP_STATUS) {
1767            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
1768                                              . 'Got packet type: ' . $this->packet_type);
1769        }
1770
1771        list($status) = Strings::unpackSSH2('N', $response);
1772        if ($status != NET_SFTP_STATUS_OK) {
1773            $this->logError($response, $status);
1774            return false;
1775        }
1776
1777        return true;
1778    }
1779
1780    /**
1781     * Recursively sets information on directories on the SFTP server
1782     *
1783     * Minimizes directory lookups and SSH_FXP_STATUS requests for speed.
1784     *
1785     * @param string $path
1786     * @param string $attr
1787     * @param int $i
1788     * @return bool
1789     */
1790    private function setstat_recursive($path, $attr, &$i)
1791    {
1792        if (!$this->read_put_responses($i)) {
1793            return false;
1794        }
1795        $i = 0;
1796        $entries = $this->readlist($path, true);
1797
1798        if ($entries === false || is_int($entries)) {
1799            return $this->setstat($path, $attr, false);
1800        }
1801
1802        // normally $entries would have at least . and .. but it might not if the directories
1803        // permissions didn't allow reading
1804        if (empty($entries)) {
1805            return false;
1806        }
1807
1808        unset($entries['.'], $entries['..']);
1809        foreach ($entries as $filename => $props) {
1810            if (!isset($props['type'])) {
1811                return false;
1812            }
1813
1814            $temp = $path . '/' . $filename;
1815            if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) {
1816                if (!$this->setstat_recursive($temp, $attr, $i)) {
1817                    return false;
1818                }
1819            } else {
1820                $packet = Strings::packSSH2('s', $temp);
1821                $packet .= $this->version >= 4 ?
1822                    pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) :
1823                    $attr;
1824                $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet);
1825
1826                $i++;
1827
1828                if ($i >= NET_SFTP_QUEUE_SIZE) {
1829                    if (!$this->read_put_responses($i)) {
1830                        return false;
1831                    }
1832                    $i = 0;
1833                }
1834            }
1835        }
1836
1837        $packet = Strings::packSSH2('s', $path);
1838        $packet .= $this->version >= 4 ?
1839            pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) :
1840            $attr;
1841        $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet);
1842
1843        $i++;
1844
1845        if ($i >= NET_SFTP_QUEUE_SIZE) {
1846            if (!$this->read_put_responses($i)) {
1847                return false;
1848            }
1849            $i = 0;
1850        }
1851
1852        return true;
1853    }
1854
1855    /**
1856     * Return the target of a symbolic link
1857     *
1858     * @param string $link
1859     * @throws \UnexpectedValueException on receipt of unexpected packets
1860     * @return mixed
1861     */
1862    public function readlink($link)
1863    {
1864        if (!$this->precheck()) {
1865            return false;
1866        }
1867
1868        $link = $this->realpath($link);
1869
1870        $this->send_sftp_packet(NET_SFTP_READLINK, Strings::packSSH2('s', $link));
1871
1872        $response = $this->get_sftp_packet();
1873        switch ($this->packet_type) {
1874            case NET_SFTP_NAME:
1875                break;
1876            case NET_SFTP_STATUS:
1877                $this->logError($response);
1878                return false;
1879            default:
1880                throw new \UnexpectedValueException('Expected NET_SFTP_NAME or NET_SFTP_STATUS. '
1881                                                  . 'Got packet type: ' . $this->packet_type);
1882        }
1883
1884        list($count) = Strings::unpackSSH2('N', $response);
1885        // the file isn't a symlink
1886        if (!$count) {
1887            return false;
1888        }
1889
1890        list($filename) = Strings::unpackSSH2('s', $response);
1891
1892        return $filename;
1893    }
1894
1895    /**
1896     * Create a symlink
1897     *
1898     * symlink() creates a symbolic link to the existing target with the specified name link.
1899     *
1900     * @param string $target
1901     * @param string $link
1902     * @throws \UnexpectedValueException on receipt of unexpected packets
1903     * @return bool
1904     */
1905    public function symlink($target, $link)
1906    {
1907        if (!$this->precheck()) {
1908            return false;
1909        }
1910
1911        //$target = $this->realpath($target);
1912        $link = $this->realpath($link);
1913
1914        /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-12.1 :
1915
1916           Changed the SYMLINK packet to be LINK and give it the ability to
1917           create hard links.  Also change it's packet number because many
1918           implementation implemented SYMLINK with the arguments reversed.
1919           Hopefully the new argument names make it clear which way is which.
1920        */
1921        if ($this->version == 6) {
1922            $type = NET_SFTP_LINK;
1923            $packet = Strings::packSSH2('ssC', $link, $target, 1);
1924        } else {
1925            $type = NET_SFTP_SYMLINK;
1926            /* quoting http://bxr.su/OpenBSD/usr.bin/ssh/PROTOCOL#347 :
1927
1928               3.1. sftp: Reversal of arguments to SSH_FXP_SYMLINK
1929
1930               When OpenSSH's sftp-server was implemented, the order of the arguments
1931               to the SSH_FXP_SYMLINK method was inadvertently reversed. Unfortunately,
1932               the reversal was not noticed until the server was widely deployed. Since
1933               fixing this to follow the specification would cause incompatibility, the
1934               current order was retained. For correct operation, clients should send
1935               SSH_FXP_SYMLINK as follows:
1936
1937                   uint32      id
1938                   string      targetpath
1939                   string      linkpath */
1940            $packet = substr($this->server_identifier, 0, 15) == 'SSH-2.0-OpenSSH' ?
1941                Strings::packSSH2('ss', $target, $link) :
1942                Strings::packSSH2('ss', $link, $target);
1943        }
1944        $this->send_sftp_packet($type, $packet);
1945
1946        $response = $this->get_sftp_packet();
1947        if ($this->packet_type != NET_SFTP_STATUS) {
1948            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
1949                                              . 'Got packet type: ' . $this->packet_type);
1950        }
1951
1952        list($status) = Strings::unpackSSH2('N', $response);
1953        if ($status != NET_SFTP_STATUS_OK) {
1954            $this->logError($response, $status);
1955            return false;
1956        }
1957
1958        return true;
1959    }
1960
1961    /**
1962     * Creates a directory.
1963     *
1964     * @param string $dir
1965     * @param int $mode
1966     * @param bool $recursive
1967     * @return bool
1968     */
1969    public function mkdir($dir, $mode = -1, $recursive = false)
1970    {
1971        if (!$this->precheck()) {
1972            return false;
1973        }
1974
1975        $dir = $this->realpath($dir);
1976
1977        if ($recursive) {
1978            $dirs = explode('/', preg_replace('#/(?=/)|/$#', '', $dir));
1979            if (empty($dirs[0])) {
1980                array_shift($dirs);
1981                $dirs[0] = '/' . $dirs[0];
1982            }
1983            for ($i = 0; $i < count($dirs); $i++) {
1984                $temp = array_slice($dirs, 0, $i + 1);
1985                $temp = implode('/', $temp);
1986                $result = $this->mkdir_helper($temp, $mode);
1987            }
1988            return $result;
1989        }
1990
1991        return $this->mkdir_helper($dir, $mode);
1992    }
1993
1994    /**
1995     * Helper function for directory creation
1996     *
1997     * @param string $dir
1998     * @param int $mode
1999     * @return bool
2000     */
2001    private function mkdir_helper($dir, $mode)
2002    {
2003        // send SSH_FXP_MKDIR without any attributes (that's what the \0\0\0\0 is doing)
2004        $this->send_sftp_packet(NET_SFTP_MKDIR, Strings::packSSH2('s', $dir) . "\0\0\0\0");
2005
2006        $response = $this->get_sftp_packet();
2007        if ($this->packet_type != NET_SFTP_STATUS) {
2008            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2009                                              . 'Got packet type: ' . $this->packet_type);
2010        }
2011
2012        list($status) = Strings::unpackSSH2('N', $response);
2013        if ($status != NET_SFTP_STATUS_OK) {
2014            $this->logError($response, $status);
2015            return false;
2016        }
2017
2018        if ($mode !== -1) {
2019            $this->chmod($mode, $dir);
2020        }
2021
2022        return true;
2023    }
2024
2025    /**
2026     * Removes a directory.
2027     *
2028     * @param string $dir
2029     * @throws \UnexpectedValueException on receipt of unexpected packets
2030     * @return bool
2031     */
2032    public function rmdir($dir)
2033    {
2034        if (!$this->precheck()) {
2035            return false;
2036        }
2037
2038        $dir = $this->realpath($dir);
2039        if ($dir === false) {
2040            return false;
2041        }
2042
2043        $this->send_sftp_packet(NET_SFTP_RMDIR, Strings::packSSH2('s', $dir));
2044
2045        $response = $this->get_sftp_packet();
2046        if ($this->packet_type != NET_SFTP_STATUS) {
2047            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2048                                              . 'Got packet type: ' . $this->packet_type);
2049        }
2050
2051        list($status) = Strings::unpackSSH2('N', $response);
2052        if ($status != NET_SFTP_STATUS_OK) {
2053            // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED?
2054            $this->logError($response, $status);
2055            return false;
2056        }
2057
2058        $this->remove_from_stat_cache($dir);
2059        // the following will do a soft delete, which would be useful if you deleted a file
2060        // and then tried to do a stat on the deleted file. the above, in contrast, does
2061        // a hard delete
2062        //$this->update_stat_cache($dir, false);
2063
2064        return true;
2065    }
2066
2067    /**
2068     * Uploads a file to the SFTP server.
2069     *
2070     * By default, \phpseclib3\Net\SFTP::put() does not read from the local filesystem.  $data is dumped directly into $remote_file.
2071     * So, for example, if you set $data to 'filename.ext' and then do \phpseclib3\Net\SFTP::get(), you will get a file, twelve bytes
2072     * long, containing 'filename.ext' as its contents.
2073     *
2074     * Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior.  With self::SOURCE_LOCAL_FILE, $remote_file will
2075     * contain as many bytes as filename.ext does on your local filesystem.  If your filename.ext is 1MB then that is how
2076     * large $remote_file will be, as well.
2077     *
2078     * Setting $mode to self::SOURCE_CALLBACK will use $data as callback function, which gets only one parameter -- number
2079     * of bytes to return, and returns a string if there is some data or null if there is no more data
2080     *
2081     * If $data is a resource then it'll be used as a resource instead.
2082     *
2083     * Currently, only binary mode is supported.  As such, if the line endings need to be adjusted, you will need to take
2084     * care of that, yourself.
2085     *
2086     * $mode can take an additional two parameters - self::RESUME and self::RESUME_START. These are bitwise AND'd with
2087     * $mode. So if you want to resume upload of a 300mb file on the local file system you'd set $mode to the following:
2088     *
2089     * self::SOURCE_LOCAL_FILE | self::RESUME
2090     *
2091     * If you wanted to simply append the full contents of a local file to the full contents of a remote file you'd replace
2092     * self::RESUME with self::RESUME_START.
2093     *
2094     * If $mode & (self::RESUME | self::RESUME_START) then self::RESUME_START will be assumed.
2095     *
2096     * $start and $local_start give you more fine grained control over this process and take precident over self::RESUME
2097     * when they're non-negative. ie. $start could let you write at the end of a file (like self::RESUME) or in the middle
2098     * of one. $local_start could let you start your reading from the end of a file (like self::RESUME_START) or in the
2099     * middle of one.
2100     *
2101     * Setting $local_start to > 0 or $mode | self::RESUME_START doesn't do anything unless $mode | self::SOURCE_LOCAL_FILE.
2102     *
2103     * {@internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - \phpseclib3\Net\SFTP::setMode().}
2104     *
2105     * @param string $remote_file
2106     * @param string|resource $data
2107     * @param int $mode
2108     * @param int $start
2109     * @param int $local_start
2110     * @param callable|null $progressCallback
2111     * @throws \UnexpectedValueException on receipt of unexpected packets
2112     * @throws \BadFunctionCallException if you're uploading via a callback and the callback function is invalid
2113     * @throws FileNotFoundException if you're uploading via a file and the file doesn't exist
2114     * @return bool
2115     */
2116    public function put($remote_file, $data, $mode = self::SOURCE_STRING, $start = -1, $local_start = -1, $progressCallback = null)
2117    {
2118        if (!$this->precheck()) {
2119            return false;
2120        }
2121
2122        $remote_file = $this->realpath($remote_file);
2123        if ($remote_file === false) {
2124            return false;
2125        }
2126
2127        $this->remove_from_stat_cache($remote_file);
2128
2129        if ($this->version >= 5) {
2130            $flags = NET_SFTP_OPEN_OPEN_OR_CREATE;
2131        } else {
2132            $flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE;
2133            // according to the SFTP specs, NET_SFTP_OPEN_APPEND should "force all writes to append data at the end of the file."
2134            // in practice, it doesn't seem to do that.
2135            //$flags|= ($mode & self::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE;
2136        }
2137
2138        if ($start >= 0) {
2139            $offset = $start;
2140        } elseif ($mode & (self::RESUME | self::RESUME_START)) {
2141            // if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called
2142            $stat = $this->stat($remote_file);
2143            $offset = $stat !== false && $stat['size'] ? $stat['size'] : 0;
2144        } else {
2145            $offset = 0;
2146            if ($this->version >= 5) {
2147                $flags = NET_SFTP_OPEN_CREATE_TRUNCATE;
2148            } else {
2149                $flags |= NET_SFTP_OPEN_TRUNCATE;
2150            }
2151        }
2152
2153        $this->remove_from_stat_cache($remote_file);
2154
2155        $packet = Strings::packSSH2('s', $remote_file);
2156        $packet .= $this->version >= 5 ?
2157            pack('N3', 0, $flags, 0) :
2158            pack('N2', $flags, 0);
2159        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2160
2161        $response = $this->get_sftp_packet();
2162        switch ($this->packet_type) {
2163            case NET_SFTP_HANDLE:
2164                $handle = substr($response, 4);
2165                break;
2166            case NET_SFTP_STATUS:
2167                $this->logError($response);
2168                return false;
2169            default:
2170                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
2171                                                  . 'Got packet type: ' . $this->packet_type);
2172        }
2173
2174        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3
2175        $dataCallback = false;
2176        switch (true) {
2177            case $mode & self::SOURCE_CALLBACK:
2178                if (!is_callable($data)) {
2179                    throw new \BadFunctionCallException("\$data should be is_callable() if you specify SOURCE_CALLBACK flag");
2180                }
2181                $dataCallback = $data;
2182                // do nothing
2183                break;
2184            case is_resource($data):
2185                $mode = $mode & ~self::SOURCE_LOCAL_FILE;
2186                $info = stream_get_meta_data($data);
2187                if (isset($info['wrapper_type']) && $info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') {
2188                    $fp = fopen('php://memory', 'w+');
2189                    stream_copy_to_stream($data, $fp);
2190                    rewind($fp);
2191                } else {
2192                    $fp = $data;
2193                }
2194                break;
2195            case $mode & self::SOURCE_LOCAL_FILE:
2196                if (!is_file($data)) {
2197                    throw new FileNotFoundException("$data is not a valid file");
2198                }
2199                $fp = @fopen($data, 'rb');
2200                if (!$fp) {
2201                    return false;
2202                }
2203        }
2204
2205        if (isset($fp)) {
2206            $stat = fstat($fp);
2207            $size = !empty($stat) ? $stat['size'] : 0;
2208
2209            if ($local_start >= 0) {
2210                fseek($fp, $local_start);
2211                $size -= $local_start;
2212            } elseif ($mode & self::RESUME) {
2213                fseek($fp, $offset);
2214                $size -= $offset;
2215            }
2216        } elseif ($dataCallback) {
2217            $size = 0;
2218        } else {
2219            $size = strlen($data);
2220        }
2221
2222        $sent = 0;
2223        $size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size;
2224
2225        $sftp_packet_size = $this->max_sftp_packet;
2226        // make the SFTP packet be exactly the SFTP packet size by including the bytes in the NET_SFTP_WRITE packets "header"
2227        $sftp_packet_size -= strlen($handle) + 25;
2228        $i = $j = 0;
2229        while ($dataCallback || ($size === 0 || $sent < $size)) {
2230            if ($dataCallback) {
2231                $temp = $dataCallback($sftp_packet_size);
2232                if (is_null($temp)) {
2233                    break;
2234                }
2235            } else {
2236                $temp = isset($fp) ? fread($fp, $sftp_packet_size) : substr($data, $sent, $sftp_packet_size);
2237                if ($temp === false || $temp === '') {
2238                    break;
2239                }
2240            }
2241
2242            $subtemp = $offset + $sent;
2243            $packet = pack('Na*N3a*', strlen($handle), $handle, $subtemp / 4294967296, $subtemp, strlen($temp), $temp);
2244            try {
2245                $this->send_sftp_packet(NET_SFTP_WRITE, $packet, $j);
2246            } catch (\Exception $e) {
2247                if ($mode & self::SOURCE_LOCAL_FILE) {
2248                    fclose($fp);
2249                }
2250                throw $e;
2251            }
2252            $sent += strlen($temp);
2253            if (is_callable($progressCallback)) {
2254                $progressCallback($sent);
2255            }
2256
2257            $i++;
2258            $j++;
2259            if ($i == NET_SFTP_UPLOAD_QUEUE_SIZE) {
2260                if (!$this->read_put_responses($i)) {
2261                    $i = 0;
2262                    break;
2263                }
2264                $i = 0;
2265            }
2266        }
2267
2268        $result = $this->close_handle($handle);
2269
2270        if (!$this->read_put_responses($i)) {
2271            if ($mode & self::SOURCE_LOCAL_FILE) {
2272                fclose($fp);
2273            }
2274            $this->close_handle($handle);
2275            return false;
2276        }
2277
2278        if ($mode & SFTP::SOURCE_LOCAL_FILE) {
2279            if (isset($fp) && is_resource($fp)) {
2280                fclose($fp);
2281            }
2282
2283            if ($this->preserveTime) {
2284                $stat = stat($data);
2285                $attr = $this->version < 4 ?
2286                    pack('N3', NET_SFTP_ATTR_ACCESSTIME, $stat['atime'], $stat['mtime']) :
2287                    Strings::packSSH2('NQ2', NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, $stat['atime'], $stat['mtime']);
2288                if (!$this->setstat($remote_file, $attr, false)) {
2289                    throw new \RuntimeException('Error setting file time');
2290                }
2291            }
2292        }
2293
2294        return $result;
2295    }
2296
2297    /**
2298     * Reads multiple successive SSH_FXP_WRITE responses
2299     *
2300     * Sending an SSH_FXP_WRITE packet and immediately reading its response isn't as efficient as blindly sending out $i
2301     * SSH_FXP_WRITEs, in succession, and then reading $i responses.
2302     *
2303     * @param int $i
2304     * @return bool
2305     * @throws \UnexpectedValueException on receipt of unexpected packets
2306     */
2307    private function read_put_responses($i)
2308    {
2309        while ($i--) {
2310            $response = $this->get_sftp_packet();
2311            if ($this->packet_type != NET_SFTP_STATUS) {
2312                throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2313                                                  . 'Got packet type: ' . $this->packet_type);
2314            }
2315
2316            list($status) = Strings::unpackSSH2('N', $response);
2317            if ($status != NET_SFTP_STATUS_OK) {
2318                $this->logError($response, $status);
2319                break;
2320            }
2321        }
2322
2323        return $i < 0;
2324    }
2325
2326    /**
2327     * Close handle
2328     *
2329     * @param string $handle
2330     * @return bool
2331     * @throws \UnexpectedValueException on receipt of unexpected packets
2332     */
2333    private function close_handle($handle)
2334    {
2335        $this->send_sftp_packet(NET_SFTP_CLOSE, pack('Na*', strlen($handle), $handle));
2336
2337        // "The client MUST release all resources associated with the handle regardless of the status."
2338        //  -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.3
2339        $response = $this->get_sftp_packet();
2340        if ($this->packet_type != NET_SFTP_STATUS) {
2341            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2342                                              . 'Got packet type: ' . $this->packet_type);
2343        }
2344
2345        list($status) = Strings::unpackSSH2('N', $response);
2346        if ($status != NET_SFTP_STATUS_OK) {
2347            $this->logError($response, $status);
2348            return false;
2349        }
2350
2351        return true;
2352    }
2353
2354    /**
2355     * Downloads a file from the SFTP server.
2356     *
2357     * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if
2358     * the operation was unsuccessful.  If $local_file is defined, returns true or false depending on the success of the
2359     * operation.
2360     *
2361     * $offset and $length can be used to download files in chunks.
2362     *
2363     * @param string $remote_file
2364     * @param string|bool|resource|callable $local_file
2365     * @param int $offset
2366     * @param int $length
2367     * @param callable|null $progressCallback
2368     * @throws \UnexpectedValueException on receipt of unexpected packets
2369     * @return string|bool
2370     */
2371    public function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null)
2372    {
2373        if (!$this->precheck()) {
2374            return false;
2375        }
2376
2377        $remote_file = $this->realpath($remote_file);
2378        if ($remote_file === false) {
2379            return false;
2380        }
2381
2382        $packet = Strings::packSSH2('s', $remote_file);
2383        $packet .= $this->version >= 5 ?
2384            pack('N3', 0, NET_SFTP_OPEN_OPEN_EXISTING, 0) :
2385            pack('N2', NET_SFTP_OPEN_READ, 0);
2386        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2387
2388        $response = $this->get_sftp_packet();
2389        switch ($this->packet_type) {
2390            case NET_SFTP_HANDLE:
2391                $handle = substr($response, 4);
2392                break;
2393            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2394                $this->logError($response);
2395                return false;
2396            default:
2397                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
2398                                                  . 'Got packet type: ' . $this->packet_type);
2399        }
2400
2401        if (is_resource($local_file)) {
2402            $fp = $local_file;
2403            $stat = fstat($fp);
2404            $res_offset = $stat['size'];
2405        } else {
2406            $res_offset = 0;
2407            if ($local_file !== false && !is_callable($local_file)) {
2408                $fp = fopen($local_file, 'wb');
2409                if (!$fp) {
2410                    return false;
2411                }
2412            } else {
2413                $content = '';
2414            }
2415        }
2416
2417        $fclose_check = $local_file !== false && !is_callable($local_file) && !is_resource($local_file);
2418
2419        $start = $offset;
2420        $read = 0;
2421        while (true) {
2422            $i = 0;
2423
2424            while ($i < NET_SFTP_QUEUE_SIZE && ($length < 0 || $read < $length)) {
2425                $tempoffset = $start + $read;
2426
2427                $packet_size = $length > 0 ? min($this->max_sftp_packet, $length - $read) : $this->max_sftp_packet;
2428
2429                $packet = Strings::packSSH2('sN3', $handle, $tempoffset / 4294967296, $tempoffset, $packet_size);
2430                try {
2431                    $this->send_sftp_packet(NET_SFTP_READ, $packet, $i);
2432                } catch (\Exception $e) {
2433                    if ($fclose_check) {
2434                        fclose($fp);
2435                    }
2436                    throw $e;
2437                }
2438                $packet = null;
2439                $read += $packet_size;
2440                $i++;
2441            }
2442
2443            if (!$i) {
2444                break;
2445            }
2446
2447            $packets_sent = $i - 1;
2448
2449            $clear_responses = false;
2450            while ($i > 0) {
2451                $i--;
2452
2453                if ($clear_responses) {
2454                    $this->get_sftp_packet($packets_sent - $i);
2455                    continue;
2456                } else {
2457                    $response = $this->get_sftp_packet($packets_sent - $i);
2458                }
2459
2460                switch ($this->packet_type) {
2461                    case NET_SFTP_DATA:
2462                        $temp = substr($response, 4);
2463                        $offset += strlen($temp);
2464                        if ($local_file === false) {
2465                            $content .= $temp;
2466                        } elseif (is_callable($local_file)) {
2467                            $local_file($temp);
2468                        } else {
2469                            fputs($fp, $temp);
2470                        }
2471                        if (is_callable($progressCallback)) {
2472                            call_user_func($progressCallback, $offset);
2473                        }
2474                        $temp = null;
2475                        break;
2476                    case NET_SFTP_STATUS:
2477                        // could, in theory, return false if !strlen($content) but we'll hold off for the time being
2478                        $this->logError($response);
2479                        $clear_responses = true; // don't break out of the loop yet, so we can read the remaining responses
2480                        break;
2481                    default:
2482                        if ($fclose_check) {
2483                            fclose($fp);
2484                        }
2485                        if ($this->channel_close) {
2486                            $this->partial_init = false;
2487                            $this->init_sftp_connection();
2488                            return false;
2489                        } else {
2490                            throw new \UnexpectedValueException('Expected NET_SFTP_DATA or NET_SFTP_STATUS. '
2491                                                              . 'Got packet type: ' . $this->packet_type);
2492                        }
2493                }
2494                $response = null;
2495            }
2496
2497            if ($clear_responses) {
2498                break;
2499            }
2500        }
2501
2502        if ($fclose_check) {
2503            fclose($fp);
2504
2505            if ($this->preserveTime) {
2506                $stat = $this->stat($remote_file);
2507                touch($local_file, $stat['mtime'], $stat['atime']);
2508            }
2509        }
2510
2511        if (!$this->close_handle($handle)) {
2512            return false;
2513        }
2514
2515        // if $content isn't set that means a file was written to
2516        return isset($content) ? $content : true;
2517    }
2518
2519    /**
2520     * Deletes a file on the SFTP server.
2521     *
2522     * @param string $path
2523     * @param bool $recursive
2524     * @return bool
2525     * @throws \UnexpectedValueException on receipt of unexpected packets
2526     */
2527    public function delete($path, $recursive = true)
2528    {
2529        if (!$this->precheck()) {
2530            return false;
2531        }
2532
2533        if (is_object($path)) {
2534            // It's an object. Cast it as string before we check anything else.
2535            $path = (string) $path;
2536        }
2537
2538        if (!is_string($path) || $path == '') {
2539            return false;
2540        }
2541
2542        $path = $this->realpath($path);
2543        if ($path === false) {
2544            return false;
2545        }
2546
2547        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3
2548        $this->send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($path), $path));
2549
2550        $response = $this->get_sftp_packet();
2551        if ($this->packet_type != NET_SFTP_STATUS) {
2552            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
2553                                              . 'Got packet type: ' . $this->packet_type);
2554        }
2555
2556        // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2557        list($status) = Strings::unpackSSH2('N', $response);
2558        if ($status != NET_SFTP_STATUS_OK) {
2559            $this->logError($response, $status);
2560            if (!$recursive) {
2561                return false;
2562            }
2563
2564            $i = 0;
2565            $result = $this->delete_recursive($path, $i);
2566            $this->read_put_responses($i);
2567            return $result;
2568        }
2569
2570        $this->remove_from_stat_cache($path);
2571
2572        return true;
2573    }
2574
2575    /**
2576     * Recursively deletes directories on the SFTP server
2577     *
2578     * Minimizes directory lookups and SSH_FXP_STATUS requests for speed.
2579     *
2580     * @param string $path
2581     * @param int $i
2582     * @return bool
2583     */
2584    private function delete_recursive($path, &$i)
2585    {
2586        if (!$this->read_put_responses($i)) {
2587            return false;
2588        }
2589        $i = 0;
2590        $entries = $this->readlist($path, true);
2591
2592        // The folder does not exist at all, so we cannot delete it.
2593        if ($entries === NET_SFTP_STATUS_NO_SUCH_FILE) {
2594            return false;
2595        }
2596
2597        // Normally $entries would have at least . and .. but it might not if the directories
2598        // permissions didn't allow reading. If this happens then default to an empty list of files.
2599        if ($entries === false || is_int($entries)) {
2600            $entries = [];
2601        }
2602
2603        unset($entries['.'], $entries['..']);
2604        foreach ($entries as $filename => $props) {
2605            if (!isset($props['type'])) {
2606                return false;
2607            }
2608
2609            $temp = $path . '/' . $filename;
2610            if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) {
2611                if (!$this->delete_recursive($temp, $i)) {
2612                    return false;
2613                }
2614            } else {
2615                $this->send_sftp_packet(NET_SFTP_REMOVE, Strings::packSSH2('s', $temp));
2616                $this->remove_from_stat_cache($temp);
2617
2618                $i++;
2619
2620                if ($i >= NET_SFTP_QUEUE_SIZE) {
2621                    if (!$this->read_put_responses($i)) {
2622                        return false;
2623                    }
2624                    $i = 0;
2625                }
2626            }
2627        }
2628
2629        $this->send_sftp_packet(NET_SFTP_RMDIR, Strings::packSSH2('s', $path));
2630        $this->remove_from_stat_cache($path);
2631
2632        $i++;
2633
2634        if ($i >= NET_SFTP_QUEUE_SIZE) {
2635            if (!$this->read_put_responses($i)) {
2636                return false;
2637            }
2638            $i = 0;
2639        }
2640
2641        return true;
2642    }
2643
2644    /**
2645     * Checks whether a file or directory exists
2646     *
2647     * @param string $path
2648     * @return bool
2649     */
2650    public function file_exists($path)
2651    {
2652        if ($this->use_stat_cache) {
2653            if (!$this->precheck()) {
2654                return false;
2655            }
2656
2657            $path = $this->realpath($path);
2658
2659            $result = $this->query_stat_cache($path);
2660
2661            if (isset($result)) {
2662                // return true if $result is an array or if it's an stdClass object
2663                return $result !== false;
2664            }
2665        }
2666
2667        return $this->stat($path) !== false;
2668    }
2669
2670    /**
2671     * Tells whether the filename is a directory
2672     *
2673     * @param string $path
2674     * @return bool
2675     */
2676    public function is_dir($path)
2677    {
2678        $result = $this->get_stat_cache_prop($path, 'type');
2679        if ($result === false) {
2680            return false;
2681        }
2682        return $result === NET_SFTP_TYPE_DIRECTORY;
2683    }
2684
2685    /**
2686     * Tells whether the filename is a regular file
2687     *
2688     * @param string $path
2689     * @return bool
2690     */
2691    public function is_file($path)
2692    {
2693        $result = $this->get_stat_cache_prop($path, 'type');
2694        if ($result === false) {
2695            return false;
2696        }
2697        return $result === NET_SFTP_TYPE_REGULAR;
2698    }
2699
2700    /**
2701     * Tells whether the filename is a symbolic link
2702     *
2703     * @param string $path
2704     * @return bool
2705     */
2706    public function is_link($path)
2707    {
2708        $result = $this->get_lstat_cache_prop($path, 'type');
2709        if ($result === false) {
2710            return false;
2711        }
2712        return $result === NET_SFTP_TYPE_SYMLINK;
2713    }
2714
2715    /**
2716     * Tells whether a file exists and is readable
2717     *
2718     * @param string $path
2719     * @return bool
2720     */
2721    public function is_readable($path)
2722    {
2723        if (!$this->precheck()) {
2724            return false;
2725        }
2726
2727        $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_READ, 0);
2728        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2729
2730        $response = $this->get_sftp_packet();
2731        switch ($this->packet_type) {
2732            case NET_SFTP_HANDLE:
2733                return true;
2734            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2735                return false;
2736            default:
2737                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
2738                                                  . 'Got packet type: ' . $this->packet_type);
2739        }
2740    }
2741
2742    /**
2743     * Tells whether the filename is writable
2744     *
2745     * @param string $path
2746     * @return bool
2747     */
2748    public function is_writable($path)
2749    {
2750        if (!$this->precheck()) {
2751            return false;
2752        }
2753
2754        $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_WRITE, 0);
2755        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
2756
2757        $response = $this->get_sftp_packet();
2758        switch ($this->packet_type) {
2759            case NET_SFTP_HANDLE:
2760                return true;
2761            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
2762                return false;
2763            default:
2764                throw new \UnexpectedValueException('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS. '
2765                                                  . 'Got packet type: ' . $this->packet_type);
2766        }
2767    }
2768
2769    /**
2770     * Tells whether the filename is writeable
2771     *
2772     * Alias of is_writable
2773     *
2774     * @param string $path
2775     * @return bool
2776     */
2777    public function is_writeable($path)
2778    {
2779        return $this->is_writable($path);
2780    }
2781
2782    /**
2783     * Gets last access time of file
2784     *
2785     * @param string $path
2786     * @return mixed
2787     */
2788    public function fileatime($path)
2789    {
2790        return $this->get_stat_cache_prop($path, 'atime');
2791    }
2792
2793    /**
2794     * Gets file modification time
2795     *
2796     * @param string $path
2797     * @return mixed
2798     */
2799    public function filemtime($path)
2800    {
2801        return $this->get_stat_cache_prop($path, 'mtime');
2802    }
2803
2804    /**
2805     * Gets file permissions
2806     *
2807     * @param string $path
2808     * @return mixed
2809     */
2810    public function fileperms($path)
2811    {
2812        return $this->get_stat_cache_prop($path, 'mode');
2813    }
2814
2815    /**
2816     * Gets file owner
2817     *
2818     * @param string $path
2819     * @return mixed
2820     */
2821    public function fileowner($path)
2822    {
2823        return $this->get_stat_cache_prop($path, 'uid');
2824    }
2825
2826    /**
2827     * Gets file group
2828     *
2829     * @param string $path
2830     * @return mixed
2831     */
2832    public function filegroup($path)
2833    {
2834        return $this->get_stat_cache_prop($path, 'gid');
2835    }
2836
2837    /**
2838     * Recursively go through rawlist() output to get the total filesize
2839     *
2840     * @return int
2841     */
2842    private static function recursiveFilesize(array $files)
2843    {
2844        $size = 0;
2845        foreach ($files as $name => $file) {
2846            if ($name == '.' || $name == '..') {
2847                continue;
2848            }
2849            $size += is_array($file) ?
2850                self::recursiveFilesize($file) :
2851                $file->size;
2852        }
2853        return $size;
2854    }
2855
2856    /**
2857     * Gets file size
2858     *
2859     * @param string $path
2860     * @param bool $recursive
2861     * @return mixed
2862     */
2863    public function filesize($path, $recursive = false)
2864    {
2865        return !$recursive || $this->filetype($path) != 'dir' ?
2866            $this->get_stat_cache_prop($path, 'size') :
2867            self::recursiveFilesize($this->rawlist($path, true));
2868    }
2869
2870    /**
2871     * Gets file type
2872     *
2873     * @param string $path
2874     * @return string|false
2875     */
2876    public function filetype($path)
2877    {
2878        $type = $this->get_stat_cache_prop($path, 'type');
2879        if ($type === false) {
2880            return false;
2881        }
2882
2883        switch ($type) {
2884            case NET_SFTP_TYPE_BLOCK_DEVICE:
2885                return 'block';
2886            case NET_SFTP_TYPE_CHAR_DEVICE:
2887                return 'char';
2888            case NET_SFTP_TYPE_DIRECTORY:
2889                return 'dir';
2890            case NET_SFTP_TYPE_FIFO:
2891                return 'fifo';
2892            case NET_SFTP_TYPE_REGULAR:
2893                return 'file';
2894            case NET_SFTP_TYPE_SYMLINK:
2895                return 'link';
2896            default:
2897                return false;
2898        }
2899    }
2900
2901    /**
2902     * Return a stat properity
2903     *
2904     * Uses cache if appropriate.
2905     *
2906     * @param string $path
2907     * @param string $prop
2908     * @return mixed
2909     */
2910    private function get_stat_cache_prop($path, $prop)
2911    {
2912        return $this->get_xstat_cache_prop($path, $prop, 'stat');
2913    }
2914
2915    /**
2916     * Return an lstat properity
2917     *
2918     * Uses cache if appropriate.
2919     *
2920     * @param string $path
2921     * @param string $prop
2922     * @return mixed
2923     */
2924    private function get_lstat_cache_prop($path, $prop)
2925    {
2926        return $this->get_xstat_cache_prop($path, $prop, 'lstat');
2927    }
2928
2929    /**
2930     * Return a stat or lstat properity
2931     *
2932     * Uses cache if appropriate.
2933     *
2934     * @param string $path
2935     * @param string $prop
2936     * @param string $type
2937     * @return mixed
2938     */
2939    private function get_xstat_cache_prop($path, $prop, $type)
2940    {
2941        if (!$this->precheck()) {
2942            return false;
2943        }
2944
2945        if ($this->use_stat_cache) {
2946            $path = $this->realpath($path);
2947
2948            $result = $this->query_stat_cache($path);
2949
2950            if (is_object($result) && isset($result->$type)) {
2951                return $result->{$type}[$prop];
2952            }
2953        }
2954
2955        $result = $this->$type($path);
2956
2957        if ($result === false || !isset($result[$prop])) {
2958            return false;
2959        }
2960
2961        return $result[$prop];
2962    }
2963
2964    /**
2965     * Renames a file or a directory on the SFTP server.
2966     *
2967     * If the file already exists this will return false
2968     *
2969     * @param string $oldname
2970     * @param string $newname
2971     * @return bool
2972     * @throws \UnexpectedValueException on receipt of unexpected packets
2973     */
2974    public function rename($oldname, $newname)
2975    {
2976        if (!$this->precheck()) {
2977            return false;
2978        }
2979
2980        $oldname = $this->realpath($oldname);
2981        $newname = $this->realpath($newname);
2982        if ($oldname === false || $newname === false) {
2983            return false;
2984        }
2985
2986        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3
2987        $packet = Strings::packSSH2('ss', $oldname, $newname);
2988        if ($this->version >= 5) {
2989            /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-6.5 ,
2990
2991               'flags' is 0 or a combination of:
2992
2993                   SSH_FXP_RENAME_OVERWRITE  0x00000001
2994                   SSH_FXP_RENAME_ATOMIC     0x00000002
2995                   SSH_FXP_RENAME_NATIVE     0x00000004
2996
2997               (none of these are currently supported) */
2998            $packet .= "\0\0\0\0";
2999        }
3000        $this->send_sftp_packet(NET_SFTP_RENAME, $packet);
3001
3002        $response = $this->get_sftp_packet();
3003        if ($this->packet_type != NET_SFTP_STATUS) {
3004            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
3005                                              . 'Got packet type: ' . $this->packet_type);
3006        }
3007
3008        // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
3009        list($status) = Strings::unpackSSH2('N', $response);
3010        if ($status != NET_SFTP_STATUS_OK) {
3011            $this->logError($response, $status);
3012            return false;
3013        }
3014
3015        // don't move the stat cache entry over since this operation could very well change the
3016        // atime and mtime attributes
3017        //$this->update_stat_cache($newname, $this->query_stat_cache($oldname));
3018        $this->remove_from_stat_cache($oldname);
3019        $this->remove_from_stat_cache($newname);
3020
3021        return true;
3022    }
3023
3024    /**
3025     * Parse Time
3026     *
3027     * See '7.7.  Times' of draft-ietf-secsh-filexfer-13 for more info.
3028     *
3029     * @param string $key
3030     * @param int $flags
3031     * @param string $response
3032     * @return array
3033     */
3034    private function parseTime($key, $flags, &$response)
3035    {
3036        $attr = [];
3037        list($attr[$key]) = Strings::unpackSSH2('Q', $response);
3038        if ($flags & NET_SFTP_ATTR_SUBSECOND_TIMES) {
3039            list($attr[$key . '-nseconds']) = Strings::unpackSSH2('N', $response);
3040        }
3041        return $attr;
3042    }
3043
3044    /**
3045     * Parse Attributes
3046     *
3047     * See '7.  File Attributes' of draft-ietf-secsh-filexfer-13 for more info.
3048     *
3049     * @param string $response
3050     * @return array
3051     */
3052    protected function parseAttributes(&$response)
3053    {
3054        $attr = [];
3055
3056        if ($this->version >= 4) {
3057            list($flags, $attr['type']) = Strings::unpackSSH2('NC', $response);
3058        } else {
3059            list($flags) = Strings::unpackSSH2('N', $response);
3060        }
3061
3062        foreach (self::$attributes as $key => $value) {
3063            switch ($flags & $key) {
3064                case NET_SFTP_ATTR_UIDGID:
3065                    if ($this->version > 3) {
3066                        continue 2;
3067                    }
3068                    break;
3069                case NET_SFTP_ATTR_CREATETIME:
3070                case NET_SFTP_ATTR_MODIFYTIME:
3071                case NET_SFTP_ATTR_ACL:
3072                case NET_SFTP_ATTR_OWNERGROUP:
3073                case NET_SFTP_ATTR_SUBSECOND_TIMES:
3074                    if ($this->version < 4) {
3075                        continue 2;
3076                    }
3077                    break;
3078                case NET_SFTP_ATTR_BITS:
3079                    if ($this->version < 5) {
3080                        continue 2;
3081                    }
3082                    break;
3083                case NET_SFTP_ATTR_ALLOCATION_SIZE:
3084                case NET_SFTP_ATTR_TEXT_HINT:
3085                case NET_SFTP_ATTR_MIME_TYPE:
3086                case NET_SFTP_ATTR_LINK_COUNT:
3087                case NET_SFTP_ATTR_UNTRANSLATED_NAME:
3088                case NET_SFTP_ATTR_CTIME:
3089                    if ($this->version < 6) {
3090                        continue 2;
3091                    }
3092            }
3093            switch ($flags & $key) {
3094                case NET_SFTP_ATTR_SIZE:             // 0x00000001
3095                    // The size attribute is defined as an unsigned 64-bit integer.
3096                    // The following will use floats on 32-bit platforms, if necessary.
3097                    // As can be seen in the BigInteger class, floats are generally
3098                    // IEEE 754 binary64 "double precision" on such platforms and
3099                    // as such can represent integers of at least 2^50 without loss
3100                    // of precision. Interpreted in filesize, 2^50 bytes = 1024 TiB.
3101                    list($attr['size']) = Strings::unpackSSH2('Q', $response);
3102                    break;
3103                case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 only)
3104                    list($attr['uid'], $attr['gid']) = Strings::unpackSSH2('NN', $response);
3105                    break;
3106                case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004
3107                    list($attr['mode']) = Strings::unpackSSH2('N', $response);
3108                    $fileType = $this->parseMode($attr['mode']);
3109                    if ($this->version < 4 && $fileType !== false) {
3110                        $attr += ['type' => $fileType];
3111                    }
3112                    break;
3113                case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008
3114                    if ($this->version >= 4) {
3115                        $attr += $this->parseTime('atime', $flags, $response);
3116                        break;
3117                    }
3118                    list($attr['atime'], $attr['mtime']) = Strings::unpackSSH2('NN', $response);
3119                    break;
3120                case NET_SFTP_ATTR_CREATETIME:       // 0x00000010 (SFTPv4+)
3121                    $attr += $this->parseTime('createtime', $flags, $response);
3122                    break;
3123                case NET_SFTP_ATTR_MODIFYTIME:       // 0x00000020
3124                    $attr += $this->parseTime('mtime', $flags, $response);
3125                    break;
3126                case NET_SFTP_ATTR_ACL:              // 0x00000040
3127                    // access control list
3128                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-04#section-5.7
3129                    // currently unsupported
3130                    list($count) = Strings::unpackSSH2('N', $response);
3131                    for ($i = 0; $i < $count; $i++) {
3132                        list($type, $flag, $mask, $who) = Strings::unpackSSH2('N3s', $result);
3133                    }
3134                    break;
3135                case NET_SFTP_ATTR_OWNERGROUP:       // 0x00000080
3136                    list($attr['owner'], $attr['$group']) = Strings::unpackSSH2('ss', $response);
3137                    break;
3138                case NET_SFTP_ATTR_SUBSECOND_TIMES:  // 0x00000100
3139                    break;
3140                case NET_SFTP_ATTR_BITS:             // 0x00000200 (SFTPv5+)
3141                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-5.8
3142                    // currently unsupported
3143                    // tells if you file is:
3144                    // readonly, system, hidden, case inensitive, archive, encrypted, compressed, sparse
3145                    // append only, immutable, sync
3146                    list($attrib_bits, $attrib_bits_valid) = Strings::unpackSSH2('N2', $response);
3147                    // if we were actually gonna implement the above it ought to be
3148                    // $attr['attrib-bits'] and $attr['attrib-bits-valid']
3149                    // eg. - instead of _
3150                    break;
3151                case NET_SFTP_ATTR_ALLOCATION_SIZE:  // 0x00000400 (SFTPv6+)
3152                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.4
3153                    // represents the number of bytes that the file consumes on the disk. will
3154                    // usually be larger than the 'size' field
3155                    list($attr['allocation-size']) = Strings::unpackSSH2('Q', $response);
3156                    break;
3157                case NET_SFTP_ATTR_TEXT_HINT:        // 0x00000800
3158                    // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.10
3159                    // currently unsupported
3160                    // tells if file is "known text", "guessed text", "known binary", "guessed binary"
3161                    list($text_hint) = Strings::unpackSSH2('C', $response);
3162                    // the above should be $attr['text-hint']
3163                    break;
3164                case NET_SFTP_ATTR_MIME_TYPE:        // 0x00001000
3165                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.11
3166                    list($attr['mime-type']) = Strings::unpackSSH2('s', $response);
3167                    break;
3168                case NET_SFTP_ATTR_LINK_COUNT:       // 0x00002000
3169                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.12
3170                    list($attr['link-count']) = Strings::unpackSSH2('N', $response);
3171                    break;
3172                case NET_SFTP_ATTR_UNTRANSLATED_NAME:// 0x00004000
3173                    // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.13
3174                    list($attr['untranslated-name']) = Strings::unpackSSH2('s', $response);
3175                    break;
3176                case NET_SFTP_ATTR_CTIME:            // 0x00008000
3177                    // 'ctime' contains the last time the file attributes were changed.  The
3178                    // exact meaning of this field depends on the server.
3179                    $attr += $this->parseTime('ctime', $flags, $response);
3180                    break;
3181                case NET_SFTP_ATTR_EXTENDED: // 0x80000000
3182                    list($count) = Strings::unpackSSH2('N', $response);
3183                    for ($i = 0; $i < $count; $i++) {
3184                        list($key, $value) = Strings::unpackSSH2('ss', $response);
3185                        $attr[$key] = $value;
3186                    }
3187            }
3188        }
3189        return $attr;
3190    }
3191
3192    /**
3193     * Attempt to identify the file type
3194     *
3195     * Quoting the SFTP RFC, "Implementations MUST NOT send bits that are not defined" but they seem to anyway
3196     *
3197     * @param int $mode
3198     * @return int
3199     */
3200    private function parseMode($mode)
3201    {
3202        // values come from http://lxr.free-electrons.com/source/include/uapi/linux/stat.h#L12
3203        // see, also, http://linux.die.net/man/2/stat
3204        switch ($mode & 0170000) {// ie. 1111 0000 0000 0000
3205            case 0000000: // no file type specified - figure out the file type using alternative means
3206                return false;
3207            case 0040000:
3208                return NET_SFTP_TYPE_DIRECTORY;
3209            case 0100000:
3210                return NET_SFTP_TYPE_REGULAR;
3211            case 0120000:
3212                return NET_SFTP_TYPE_SYMLINK;
3213            // new types introduced in SFTPv5+
3214            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2
3215            case 0010000: // named pipe (fifo)
3216                return NET_SFTP_TYPE_FIFO;
3217            case 0020000: // character special
3218                return NET_SFTP_TYPE_CHAR_DEVICE;
3219            case 0060000: // block special
3220                return NET_SFTP_TYPE_BLOCK_DEVICE;
3221            case 0140000: // socket
3222                return NET_SFTP_TYPE_SOCKET;
3223            case 0160000: // whiteout
3224                // "SPECIAL should be used for files that are of
3225                //  a known type which cannot be expressed in the protocol"
3226                return NET_SFTP_TYPE_SPECIAL;
3227            default:
3228                return NET_SFTP_TYPE_UNKNOWN;
3229        }
3230    }
3231
3232    /**
3233     * Parse Longname
3234     *
3235     * SFTPv3 doesn't provide any easy way of identifying a file type.  You could try to open
3236     * a file as a directory and see if an error is returned or you could try to parse the
3237     * SFTPv3-specific longname field of the SSH_FXP_NAME packet.  That's what this function does.
3238     * The result is returned using the
3239     * {@link http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 SFTPv4 type constants}.
3240     *
3241     * If the longname is in an unrecognized format bool(false) is returned.
3242     *
3243     * @param string $longname
3244     * @return mixed
3245     */
3246    private function parseLongname($longname)
3247    {
3248        // http://en.wikipedia.org/wiki/Unix_file_types
3249        // http://en.wikipedia.org/wiki/Filesystem_permissions#Notation_of_traditional_Unix_permissions
3250        if (preg_match('#^[^/]([r-][w-][xstST-]){3}#', $longname)) {
3251            switch ($longname[0]) {
3252                case '-':
3253                    return NET_SFTP_TYPE_REGULAR;
3254                case 'd':
3255                    return NET_SFTP_TYPE_DIRECTORY;
3256                case 'l':
3257                    return NET_SFTP_TYPE_SYMLINK;
3258                default:
3259                    return NET_SFTP_TYPE_SPECIAL;
3260            }
3261        }
3262
3263        return false;
3264    }
3265
3266    /**
3267     * Sends SFTP Packets
3268     *
3269     * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info.
3270     *
3271     * @param int $type
3272     * @param string $data
3273     * @param int $request_id
3274     * @see self::_get_sftp_packet()
3275     * @see self::send_channel_packet()
3276     * @return void
3277     */
3278    private function send_sftp_packet($type, $data, $request_id = 1)
3279    {
3280        // in SSH2.php the timeout is cumulative per function call. eg. exec() will
3281        // timeout after 10s. but for SFTP.php it's cumulative per packet
3282        $this->curTimeout = $this->timeout;
3283        $this->is_timeout = false;
3284
3285        $packet = $this->use_request_id ?
3286            pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) :
3287            pack('NCa*', strlen($data) + 1, $type, $data);
3288
3289        $start = microtime(true);
3290        $this->send_channel_packet(self::CHANNEL, $packet);
3291        $stop = microtime(true);
3292
3293        if (defined('NET_SFTP_LOGGING')) {
3294            $packet_type = '-> ' . self::$packet_types[$type] .
3295                           ' (' . round($stop - $start, 4) . 's)';
3296            $this->append_log($packet_type, $data);
3297        }
3298    }
3299
3300    /**
3301     * Resets the SFTP channel for re-use
3302     */
3303    private function reset_sftp()
3304    {
3305        $this->use_request_id = false;
3306        $this->pwd = false;
3307        $this->requestBuffer = [];
3308        $this->partial_init = false;
3309    }
3310
3311    /**
3312     * Resets a connection for re-use
3313     */
3314    protected function reset_connection()
3315    {
3316        parent::reset_connection();
3317        $this->reset_sftp();
3318    }
3319
3320    /**
3321     * Receives SFTP Packets
3322     *
3323     * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info.
3324     *
3325     * Incidentally, the number of SSH_MSG_CHANNEL_DATA messages has no bearing on the number of SFTP packets present.
3326     * There can be one SSH_MSG_CHANNEL_DATA messages containing two SFTP packets or there can be two SSH_MSG_CHANNEL_DATA
3327     * messages containing one SFTP packet.
3328     *
3329     * @see self::_send_sftp_packet()
3330     * @return string
3331     */
3332    private function get_sftp_packet($request_id = null)
3333    {
3334        $this->channel_close = false;
3335
3336        if (isset($request_id) && isset($this->requestBuffer[$request_id])) {
3337            $this->packet_type = $this->requestBuffer[$request_id]['packet_type'];
3338            $temp = $this->requestBuffer[$request_id]['packet'];
3339            unset($this->requestBuffer[$request_id]);
3340            return $temp;
3341        }
3342
3343        // in SSH2.php the timeout is cumulative per function call. eg. exec() will
3344        // timeout after 10s. but for SFTP.php it's cumulative per packet
3345        $this->curTimeout = $this->timeout;
3346        $this->is_timeout = false;
3347
3348        $start = microtime(true);
3349
3350        // SFTP packet length
3351        while (strlen($this->packet_buffer) < 4) {
3352            $temp = $this->get_channel_packet(self::CHANNEL, true);
3353            if ($temp === true) {
3354                if ($this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) {
3355                    $this->channel_close = true;
3356                }
3357                $this->packet_type = false;
3358                $this->packet_buffer = '';
3359                return false;
3360            }
3361            $this->packet_buffer .= $temp;
3362        }
3363        if (strlen($this->packet_buffer) < 4) {
3364            throw new \RuntimeException('Packet is too small');
3365        }
3366        $length = unpack('Nlength', Strings::shift($this->packet_buffer, 4))['length'];
3367
3368        $tempLength = $length;
3369        $tempLength -= strlen($this->packet_buffer);
3370
3371        // 256 * 1024 is what SFTP_MAX_MSG_LENGTH is set to in OpenSSH's sftp-common.h
3372        if (!$this->allow_arbitrary_length_packets && !$this->use_request_id && $tempLength > 256 * 1024) {
3373            throw new \RuntimeException('Invalid Size');
3374        }
3375
3376        // SFTP packet type and data payload
3377        while ($tempLength > 0) {
3378            $temp = $this->get_channel_packet(self::CHANNEL, true);
3379            if ($temp === true) {
3380                if ($this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) {
3381                    $this->channel_close = true;
3382                }
3383                $this->packet_type = false;
3384                $this->packet_buffer = '';
3385                return false;
3386            }
3387            $this->packet_buffer .= $temp;
3388            $tempLength -= strlen($temp);
3389        }
3390
3391        $stop = microtime(true);
3392
3393        $this->packet_type = ord(Strings::shift($this->packet_buffer));
3394
3395        if ($this->use_request_id) {
3396            $packet_id = unpack('Npacket_id', Strings::shift($this->packet_buffer, 4))['packet_id']; // remove the request id
3397            $length -= 5; // account for the request id and the packet type
3398        } else {
3399            $length -= 1; // account for the packet type
3400        }
3401
3402        $packet = Strings::shift($this->packet_buffer, $length);
3403
3404        if (defined('NET_SFTP_LOGGING')) {
3405            $packet_type = '<- ' . self::$packet_types[$this->packet_type] .
3406                           ' (' . round($stop - $start, 4) . 's)';
3407            $this->append_log($packet_type, $packet);
3408        }
3409
3410        if (isset($request_id) && $this->use_request_id && $packet_id != $request_id) {
3411            $this->requestBuffer[$packet_id] = [
3412                'packet_type' => $this->packet_type,
3413                'packet' => $packet
3414            ];
3415            return $this->get_sftp_packet($request_id);
3416        }
3417
3418        return $packet;
3419    }
3420
3421    /**
3422     * Logs data packets
3423     *
3424     * Makes sure that only the last 1MB worth of packets will be logged
3425     *
3426     * @param string $message_number
3427     * @param string $message
3428     */
3429    private function append_log($message_number, $message)
3430    {
3431        $this->append_log_helper(
3432            NET_SFTP_LOGGING,
3433            $message_number,
3434            $message,
3435            $this->packet_type_log,
3436            $this->packet_log,
3437            $this->log_size,
3438            $this->realtime_log_file,
3439            $this->realtime_log_wrap,
3440            $this->realtime_log_size
3441        );
3442    }
3443
3444    /**
3445     * Returns a log of the packets that have been sent and received.
3446     *
3447     * 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')
3448     *
3449     * @return array|string|false
3450     */
3451    public function getSFTPLog()
3452    {
3453        if (!defined('NET_SFTP_LOGGING')) {
3454            return false;
3455        }
3456
3457        switch (NET_SFTP_LOGGING) {
3458            case self::LOG_COMPLEX:
3459                return $this->format_log($this->packet_log, $this->packet_type_log);
3460                break;
3461            //case self::LOG_SIMPLE:
3462            default:
3463                return $this->packet_type_log;
3464        }
3465    }
3466    /**
3467     * Returns all errors on the SFTP layer
3468     *
3469     * @return array
3470     * @removed in phpseclib 4.0.0
3471     */
3472    public function getSFTPErrors()
3473    {
3474        return $this->sftp_errors;
3475    }
3476
3477    /**
3478     * Returns the last error on the SFTP layer
3479     *
3480     * @return string
3481     * @removed in phpseclib 4.0.0
3482     */
3483    public function getLastSFTPError()
3484    {
3485        return count($this->sftp_errors) ? $this->sftp_errors[count($this->sftp_errors) - 1] : '';
3486    }
3487
3488    /**
3489     * Get supported SFTP versions
3490     *
3491     * @return array
3492     */
3493    public function getSupportedVersions()
3494    {
3495        if (!($this->bitmap & SSH2::MASK_LOGIN)) {
3496            return false;
3497        }
3498
3499        if (!$this->partial_init) {
3500            $this->partial_init_sftp_connection();
3501        }
3502
3503        $temp = ['version' => $this->defaultVersion];
3504        if (isset($this->extensions['versions'])) {
3505            $temp['extensions'] = $this->extensions['versions'];
3506        }
3507        return $temp;
3508    }
3509
3510    /**
3511     * Get supported SFTP extensions
3512     *
3513     * @return array
3514     */
3515    public function getSupportedExtensions()
3516    {
3517        if (!($this->bitmap & SSH2::MASK_LOGIN)) {
3518            return false;
3519        }
3520
3521        if (!$this->partial_init) {
3522            $this->partial_init_sftp_connection();
3523        }
3524
3525        return $this->extensions;
3526    }
3527
3528    /**
3529     * Get supported SFTP versions
3530     *
3531     * @return int|false
3532     */
3533    public function getNegotiatedVersion()
3534    {
3535        if (!$this->precheck()) {
3536            return false;
3537        }
3538
3539        return $this->version;
3540    }
3541
3542    /**
3543     * Set preferred version
3544     *
3545     * If you're preferred version isn't supported then the highest supported
3546     * version of SFTP will be utilized. Set to null or false or int(0) to
3547     * unset the preferred version
3548     *
3549     * @param int $version
3550     */
3551    public function setPreferredVersion($version)
3552    {
3553        $this->preferredVersion = $version;
3554    }
3555
3556    /**
3557     * Disconnect
3558     *
3559     * @param int $reason
3560     * @return false
3561     */
3562    protected function disconnect_helper($reason)
3563    {
3564        $this->pwd = false;
3565        return parent::disconnect_helper($reason);
3566    }
3567
3568    /**
3569     * Enable Date Preservation
3570     */
3571    public function enableDatePreservation()
3572    {
3573        $this->preserveTime = true;
3574    }
3575
3576    /**
3577     * Disable Date Preservation
3578     */
3579    public function disableDatePreservation()
3580    {
3581        $this->preserveTime = false;
3582    }
3583
3584    /**
3585     * Copy
3586     *
3587     * This method (currently) only works if the copy-data extension is available
3588     *
3589     * @param string $oldname
3590     * @param string $newname
3591     * @return bool
3592     */
3593    public function copy($oldname, $newname)
3594    {
3595        if (!$this->precheck()) {
3596            return false;
3597        }
3598
3599        $oldname = $this->realpath($oldname);
3600        $newname = $this->realpath($newname);
3601        if ($oldname === false || $newname === false) {
3602            return false;
3603        }
3604
3605        if (!isset($this->extensions['copy-data']) || $this->extensions['copy-data'] !== '1') {
3606            throw new \RuntimeException(
3607                "Extension 'copy-data' is not supported by the server. " .
3608                "Call getSupportedVersions() to see a list of supported extension"
3609            );
3610        }
3611
3612        $size = $this->filesize($oldname);
3613
3614        $packet = Strings::packSSH2('s', $oldname);
3615        $packet .= $this->version >= 5 ?
3616            pack('N3', 0, NET_SFTP_OPEN_OPEN_EXISTING, 0) :
3617            pack('N2', NET_SFTP_OPEN_READ, 0);
3618        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
3619
3620        $response = $this->get_sftp_packet();
3621        switch ($this->packet_type) {
3622            case NET_SFTP_HANDLE:
3623                $oldhandle = substr($response, 4);
3624                break;
3625            case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
3626                $this->logError($response);
3627                return false;
3628            default:
3629                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
3630                                                  . 'Got packet type: ' . $this->packet_type);
3631        }
3632
3633        if ($this->version >= 5) {
3634            $flags = NET_SFTP_OPEN_OPEN_OR_CREATE;
3635        } else {
3636            $flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE;
3637        }
3638
3639        $packet = Strings::packSSH2('s', $newname);
3640        $packet .= $this->version >= 5 ?
3641            pack('N3', 0, $flags, 0) :
3642            pack('N2', $flags, 0);
3643        $this->send_sftp_packet(NET_SFTP_OPEN, $packet);
3644
3645        $response = $this->get_sftp_packet();
3646        switch ($this->packet_type) {
3647            case NET_SFTP_HANDLE:
3648                $newhandle = substr($response, 4);
3649                break;
3650            case NET_SFTP_STATUS:
3651                $this->logError($response);
3652                return false;
3653            default:
3654                throw new \UnexpectedValueException('Expected NET_SFTP_HANDLE or NET_SFTP_STATUS. '
3655                                                  . 'Got packet type: ' . $this->packet_type);
3656        }
3657
3658        $packet = Strings::packSSH2('ssQQsQ', 'copy-data', $oldhandle, 0, $size, $newhandle, 0);
3659        $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet);
3660
3661        $response = $this->get_sftp_packet();
3662        if ($this->packet_type != NET_SFTP_STATUS) {
3663            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
3664                                              . 'Got packet type: ' . $this->packet_type);
3665        }
3666
3667        $this->close_handle($oldhandle);
3668        $this->close_handle($newhandle);
3669
3670        return true;
3671    }
3672
3673    /**
3674     * POSIX Rename
3675     *
3676     * Where rename() fails "if there already exists a file with the name specified by newpath"
3677     * (draft-ietf-secsh-filexfer-02#section-6.5), posix_rename() overwrites the existing file in an atomic fashion.
3678     * ie. "there is no observable instant in time where the name does not refer to either the old or the new file"
3679     * (draft-ietf-secsh-filexfer-13#page-39).
3680     *
3681     * @param string $oldname
3682     * @param string $newname
3683     * @return bool
3684     */
3685    public function posix_rename($oldname, $newname)
3686    {
3687        if (!$this->precheck()) {
3688            return false;
3689        }
3690
3691        $oldname = $this->realpath($oldname);
3692        $newname = $this->realpath($newname);
3693        if ($oldname === false || $newname === false) {
3694            return false;
3695        }
3696
3697        if ($this->version >= 5) {
3698            $packet = Strings::packSSH2('ssN', $oldname, $newname, 2); // 2 = SSH_FXP_RENAME_ATOMIC
3699            $this->send_sftp_packet(NET_SFTP_RENAME, $packet);
3700        } elseif (isset($this->extensions['posix-rename@openssh.com']) && $this->extensions['posix-rename@openssh.com'] === '1') {
3701            $packet = Strings::packSSH2('sss', 'posix-rename@openssh.com', $oldname, $newname);
3702            $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet);
3703        } else {
3704            throw new \RuntimeException(
3705                "Extension 'posix-rename@openssh.com' is not supported by the server. " .
3706                "Call getSupportedVersions() to see a list of supported extension"
3707            );
3708        }
3709
3710        $response = $this->get_sftp_packet();
3711        if ($this->packet_type != NET_SFTP_STATUS) {
3712            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
3713                                              . 'Got packet type: ' . $this->packet_type);
3714        }
3715
3716        // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
3717        list($status) = Strings::unpackSSH2('N', $response);
3718        if ($status != NET_SFTP_STATUS_OK) {
3719            $this->logError($response, $status);
3720            return false;
3721        }
3722
3723        // don't move the stat cache entry over since this operation could very well change the
3724        // atime and mtime attributes
3725        //$this->update_stat_cache($newname, $this->query_stat_cache($oldname));
3726        $this->remove_from_stat_cache($oldname);
3727        $this->remove_from_stat_cache($newname);
3728
3729        return true;
3730    }
3731
3732    /**
3733     * Returns general information about a file system.
3734     *
3735     * The function statvfs() returns information about a mounted filesystem.
3736     * @see https://man7.org/linux/man-pages/man3/statvfs.3.html
3737     *
3738     * @param string $path
3739     * @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}
3740     */
3741    public function statvfs($path)
3742    {
3743        if (!$this->precheck()) {
3744            return false;
3745        }
3746
3747        if (!isset($this->extensions['statvfs@openssh.com']) || $this->extensions['statvfs@openssh.com'] !== '2') {
3748            throw new \RuntimeException(
3749                "Extension 'statvfs@openssh.com' is not supported by the server. " .
3750                "Call getSupportedVersions() to see a list of supported extension"
3751            );
3752        }
3753
3754        $realpath = $this->realpath($path);
3755        if ($realpath === false) {
3756            return false;
3757        }
3758
3759        $packet = Strings::packSSH2('ss', 'statvfs@openssh.com', $realpath);
3760        $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet);
3761
3762        $response = $this->get_sftp_packet();
3763        if ($this->packet_type !== NET_SFTP_EXTENDED_REPLY) {
3764            throw new \UnexpectedValueException(
3765                'Expected SSH_FXP_EXTENDED_REPLY. '
3766                . 'Got packet type: ' . $this->packet_type
3767            );
3768        }
3769
3770        /**
3771         * These requests return a SSH_FXP_STATUS reply on failure. On success they
3772         * return the following SSH_FXP_EXTENDED_REPLY reply:
3773         *
3774         * uint32        id
3775         * uint64        f_bsize     file system block size
3776         * uint64        f_frsize     fundamental fs block size
3777         * uint64        f_blocks     number of blocks (unit f_frsize)
3778         * uint64        f_bfree      free blocks in file system
3779         * uint64        f_bavail     free blocks for non-root
3780         * uint64        f_files      total file inodes
3781         * uint64        f_ffree      free file inodes
3782         * uint64        f_favail     free file inodes for to non-root
3783         * uint64        f_fsid       file system id
3784         * uint64        f_flag       bit mask of f_flag values
3785         * uint64        f_namemax    maximum filename length
3786         */
3787        return array_combine(
3788            ['bsize', 'frsize', 'blocks', 'bfree', 'bavail', 'files', 'ffree', 'favail', 'fsid', 'flag', 'namemax'],
3789            Strings::unpackSSH2('QQQQQQQQQQQ', $response)
3790        );
3791    }
3792
3793    public function hardlink($oldpath, $newpath)
3794    {
3795        if (!$this->precheck()) {
3796            return false;
3797        }
3798
3799        if (!isset($this->extensions['hardlink@openssh.com']) || $this->extensions['hardlink@openssh.com'] !== '1') {
3800            throw new \RuntimeException(
3801                "Extension 'hardlink@openssh.com' is not supported by the server. " .
3802                "Call getSupportedVersions() to see a list of supported extension"
3803            );
3804        }
3805
3806        $oldpath = $this->realpath($oldpath);
3807        $newpath = $this->realpath($newpath);
3808        if ($oldpath === false || $newpath === false) {
3809            return false;
3810        }
3811
3812        $packet = Strings::packSSH2('sss', 'hardlink@openssh.com', $oldpath, $newpath);
3813        $this->send_sftp_packet(NET_SFTP_EXTENDED, $packet);
3814
3815        $response = $this->get_sftp_packet();
3816        if ($this->packet_type !== NET_SFTP_STATUS) {
3817            throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. '
3818                . 'Got packet type: ' . $this->packet_type);
3819        }
3820
3821        // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
3822        list($status) = Strings::unpackSSH2('N', $response);
3823        if ($status !== NET_SFTP_STATUS_OK) {
3824            $this->logError($response, $status);
3825            return false;
3826        }
3827
3828        // this operation will change the ctime and link-count attributes
3829        // which could be cached depending on sftp version
3830        $this->remove_from_stat_cache($oldpath);
3831
3832        // hardlink creation should fail if $newpath exists;
3833        // removing it from the cache anyway, just to be sure
3834        $this->remove_from_stat_cache($newpath);
3835
3836        return true;
3837    }
3838}
3839