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