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