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