Subversion Repositories oidplus

Rev

Rev 842 | Go to most recent revision | Details | Last modification | View Log | RSS feed

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