Subversion Repositories oidplus

Rev

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