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