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