Subversion Repositories oidplus

Rev

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