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