[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/vendor/phpseclib/phpseclib/phpseclib/Net/ -> SFTP.php (source)

   1  <?php
   2  
   3  /**
   4   * Pure-PHP implementation of SFTP.
   5   *
   6   * PHP version 5
   7   *
   8   * Supports SFTPv2/3/4/5/6. Defaults to v3.
   9   *
  10   * The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}.
  11   *
  12   * Here's a short example of how to use this library:
  13   * <code>
  14   * <?php
  15   *    include 'vendor/autoload.php';
  16   *
  17   *    $sftp = new \phpseclib3\Net\SFTP('www.domain.tld');
  18   *    if (!$sftp->login('username', 'password')) {
  19   *        exit('Login Failed');
  20   *    }
  21   *
  22   *    echo $sftp->pwd() . "\r\n";
  23   *    $sftp->put('filename.ext', 'hello, world!');
  24   *    print_r($sftp->nlist());
  25   * ?>
  26   * </code>
  27   *
  28   * @author    Jim Wigginton <terrafrost@php.net>
  29   * @copyright 2009 Jim Wigginton
  30   * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
  31   * @link      http://phpseclib.sourceforge.net
  32   */
  33  
  34  namespace phpseclib3\Net;
  35  
  36  use phpseclib3\Common\Functions\Strings;
  37  use phpseclib3\Exception\FileNotFoundException;
  38  
  39  /**
  40   * Pure-PHP implementations of SFTP.
  41   *
  42   * @author  Jim Wigginton <terrafrost@php.net>
  43   */
  44  class 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->openChannel(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              $size = $this->stat($remote_file)['size'];
2133              $offset = $size !== false ? $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
3450       *
3451       * @return array
3452       */
3453      public function getSFTPErrors()
3454      {
3455          return $this->sftp_errors;
3456      }
3457  
3458      /**
3459       * Returns the last error
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  }