Subversion Repositories oidplus

Rev

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