Make WordPress Core


Ignore:
Timestamp:
07/05/2026 01:15:23 AM (3 months ago)
Author:
westonruter
Message:

Filesystem API: Improve type safety across the transport classes.

Change the optional constructor argument of WP_Filesystem_FTPext, WP_Filesystem_ftpsockets, and WP_Filesystem_SSH2 from an empty string default to an empty array, matching how the argument is actually consumed, and improve the associated DocBlocks.

These classes were also brought to adherence with PHPStan rule level 10:

  • Add FileListing and Options array shapes, and initialize each transport's $options to a complete default array before any early return.
  • Correct several inaccurate @return descriptions, including the group() methods that had been describing the owner.
  • Allow WP_Filesystem_SSH2::connect() to be retried after a failed connection attempt.
  • Stop WP_Filesystem_FTPext::parselisting() from leaking its intermediate date-parsing keys into the returned listing.
  • Add ext-ftp and ext-ssh2 to the suggested extensions in composer.json.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/11593.
Follow-up to r62635, r62636.

Props soean, westonruter, mukesh27.
See #65584, #64898.
Fixes #65409.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-wp-filesystem-ssh2.php

    r61699 r62637  
    3333 * @package WordPress
    3434 * @subpackage Filesystem
     35 *
     36 * @phpstan-type Options array{
     37 *     hostname: string,
     38 *     username: string,
     39 *     password: string|null,
     40 *     port: non-negative-int,
     41 *     public_key?: non-empty-string,
     42 *     private_key?: non-empty-string,
     43 *     hostkey?: array{ hostkey: non-empty-string },
     44 * }
     45 * @phpstan-import-type FileListing from WP_Filesystem_Base
    3546 */
    3647class WP_Filesystem_SSH2 extends WP_Filesystem_Base {
    … …  
    3849        /**
    3950         * @since 2.7.0
    40          * @var resource
     51         * @var resource|false
    4152         */
    4253        public $link = false;
    … …  
    4455        /**
    4556         * @since 2.7.0
    46          * @var resource
     57         * @var resource|false
    4758         */
    4859        public $sftp_link;
    … …  
    5566
    5667        /**
     68         * @since 7.1.0
     69         * @var array
     70         * @phpstan-var Options
     71         */
     72        public $options;
     73
     74        /**
    5775         * Constructor.
    5876         *
    5977         * @since 2.7.0
    6078         *
    61          * @param array $opt
    62          */
    63         public function __construct( $opt = '' ) {
    64                 $this->method = 'ssh2';
    65                 $this->errors = new WP_Error();
     79         * @param array $opt {
     80         *     Array of connection options.
     81         *
     82         *     @type string $hostname    Required. SSH server hostname.
     83         *     @type string $username    Required. SSH username.
     84         *     @type int    $port        Optional. SSH server port. Default 22.
     85         *     @type string $password    Optional. SSH password. May be empty when using keys.
     86         *     @type string $public_key  Optional. Path to public key file for publickey authentication.
     87         *     @type string $private_key Optional. Path to private key file for publickey authentication.
     88         * }
     89         * @phpstan-param array{
     90         *     hostname: non-empty-string,
     91         *     username: non-empty-string,
     92         *     port?: non-negative-int,
     93         *     password?: string,
     94         *     public_key?: non-empty-string,
     95         *     private_key?: non-empty-string,
     96         * }|null $opt
     97         */
     98        public function __construct( $opt = null ) {
     99                $this->method  = 'ssh2';
     100                $this->errors  = new WP_Error();
     101                $this->options = array(
     102                        'port'     => 22,
     103                        'hostname' => '',
     104                        'username' => '',
     105                        'password' => null,
     106                );
    66107
    67108                // Check if possible to use ssh2 functions.
    … …  
    71112                }
    72113
     114                if ( ! is_array( $opt ) ) {
     115                        $opt = array();
     116                }
     117
    73118                // Set defaults:
    74                 if ( empty( $opt['port'] ) ) {
    75                         $this->options['port'] = 22;
    76                 } else {
     119                if ( ! empty( $opt['port'] ) ) {
    77120                        $this->options['port'] = $opt['port'];
    78121                }
    … …  
    92135
    93136                        $this->keys = true;
    94                 } elseif ( empty( $opt['username'] ) ) {
     137                }
     138
     139                // A username is always required, whether authenticating with a password or with keys.
     140                if ( empty( $opt['username'] ) ) {
    95141                        $this->errors->add( 'empty_username', __( 'SSH2 username is required' ) );
    96                 }
    97 
    98                 if ( ! empty( $opt['username'] ) ) {
     142                } else {
    99143                        $this->options['username'] = $opt['username'];
    100144                }
    101145
    102                 if ( empty( $opt['password'] ) ) {
     146                if ( ! empty( $opt['password'] ) ) {
     147                        $this->options['password'] = $opt['password'];
     148                } elseif ( ! $this->keys ) {
    103149                        // Password can be blank if we are using keys.
    104                         if ( ! $this->keys ) {
    105                                 $this->errors->add( 'empty_password', __( 'SSH2 password is required' ) );
    106                         } else {
    107                                 $this->options['password'] = null;
    108                         }
    109                 } else {
    110                         $this->options['password'] = $opt['password'];
     150                        $this->errors->add( 'empty_password', __( 'SSH2 password is required' ) );
    111151                }
    112152        }
    … …  
    120160         */
    121161        public function connect() {
    122                 if ( ! $this->keys ) {
     162                /*
     163                 * Bail if the constructor recorded a configuration error. Connection and
     164                 * authentication errors are excluded so that a failed connection attempt
     165                 * can be retried on the same instance.
     166                 */
     167                if ( $this->errors->has_errors() && ! array_intersect( array( 'connect', 'auth' ), $this->errors->get_error_codes() ) ) {
     168                        return false;
     169                }
     170
     171                if ( ! isset( $this->options['hostkey'] ) ) {
    123172                        $this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'] );
    124173                } else {
    … …  
    140189
    141190                if ( ! $this->keys ) {
    142                         if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ) ) {
     191                        if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ?? '' ) ) {
    143192                                $this->errors->add(
    144193                                        'auth',
    … …  
    153202                        }
    154203                } else {
    155                         if ( ! @ssh2_auth_pubkey_file( $this->link, $this->options['username'], $this->options['public_key'], $this->options['private_key'], $this->options['password'] ) ) {
     204                        if ( ! @ssh2_auth_pubkey_file( $this->link, $this->options['username'], $this->options['public_key'] ?? '', $this->options['private_key'] ?? '', $this->options['password'] ?? '' ) ) {
    156205                                $this->errors->add(
    157206                                        'auth',
    … …  
    213262         * @return bool|string True on success, false on failure. String if the command was executed, `$returnbool`
    214263         *                     is false (default), and data from the resulting stream was retrieved.
     264         *
     265         * @phpstan-return ( $returnbool is true ? bool : string )
    215266         */
    216267        public function run_command( $command, $returnbool = false ) {
    … …  
    265316         *
    266317         * @param string $file Path to the file.
    267          * @return array|false File contents in an array on success, false on failure.
     318         * @return string[]|false File contents in an array on success, false on failure.
    268319         */
    269320        public function get_contents_array( $file ) {
    … …  
    302353         */
    303354        public function cwd() {
     355                if ( ! $this->sftp_link ) {
     356                        return false;
     357                }
     358
    304359                $cwd = ssh2_sftp_realpath( $this->sftp_link, '.' );
    305360
    306                 if ( $cwd ) {
    307                         $cwd = trailingslashit( trim( $cwd ) );
    308                 }
    309 
    310                 return $cwd;
     361                if ( ! is_string( $cwd ) ) {
     362                        return false;
     363                }
     364
     365                return trailingslashit( trim( $cwd ) );
    311366        }
    312367
    … …  
    340395
    341396                if ( ! $recursive || ! $this->is_dir( $file ) ) {
    342                         return $this->run_command( sprintf( 'chgrp %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
    343                 }
    344 
    345                 return $this->run_command( sprintf( 'chgrp -R %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true );
     397                        return $this->run_command( sprintf( 'chgrp %s %s', escapeshellarg( (string) $group ), escapeshellarg( $file ) ), true );
     398                }
     399
     400                return $this->run_command( sprintf( 'chgrp -R %s %s', escapeshellarg( (string) $group ), escapeshellarg( $file ) ), true );
    346401        }
    347402
    … …  
    397452
    398453                if ( ! $recursive || ! $this->is_dir( $file ) ) {
    399                         return $this->run_command( sprintf( 'chown %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
    400                 }
    401 
    402                 return $this->run_command( sprintf( 'chown -R %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true );
     454                        return $this->run_command( sprintf( 'chown %s %s', escapeshellarg( (string) $owner ), escapeshellarg( $file ) ), true );
     455                }
     456
     457                return $this->run_command( sprintf( 'chown -R %s %s', escapeshellarg( (string) $owner ), escapeshellarg( $file ) ), true );
    403458        }
    404459
    … …  
    409464         *
    410465         * @param string $file Path to the file.
    411          * @return string|false Username of the owner on success, false on failure.
     466         * @return string|int<1, max>|false Username of the owner on success, or UID of file owner if not available; false on failure.
    412467         */
    413468        public function owner( $file ) {
    … …  
    437492         *
    438493         * @param string $file Path to the file.
    439          * @return string Mode of the file (the last 3 digits).
     494         * @return string Mode of the file (the last 3 digits). Empty string on failure.
    440495         */
    441496        public function getchmod( $file ) {
    442                 return substr( decoct( @fileperms( $this->sftp_path( $file ) ) ), -3 );
     497                $file_perms = @fileperms( $this->sftp_path( $file ) );
     498                if ( ! is_int( $file_perms ) ) {
     499                        return '';
     500                }
     501                return substr( decoct( $file_perms ), -3 );
    443502        }
    444503
    … …  
    449508         *
    450509         * @param string $file Path to the file.
    451          * @return string|false The group on success, false on failure.
     510         * @return string|int<1, max>|false Group name on success, or GID of the file's group if not available; false on failure.
    452511         */
    453512        public function group( $file ) {
    … …  
    527586                }
    528587
     588                if ( ! $this->sftp_link ) {
     589                        return false;
     590                }
    529591                return ssh2_sftp_rename( $this->sftp_link, $source, $destination );
    530592        }
    … …  
    543605         */
    544606        public function delete( $file, $recursive = false, $type = false ) {
     607                if ( ! $this->sftp_link ) {
     608                        return false;
     609                }
    545610                if ( 'f' === $type || $this->is_file( $file ) ) {
    546611                        return ssh2_sftp_unlink( $this->sftp_link, $file );
    … …  
    693758         */
    694759        public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) {
     760                if ( ! $this->sftp_link ) {
     761                        return false;
     762                }
    695763                $path = untrailingslashit( $path );
    696764
    … …  
    769837         *     }
    770838         * }
     839         * @phpstan-return array<string, FileListing>|false
    771840         */
    772841        public function dirlist( $path, $include_hidden = true, $recursive = false ) {
    … …  
    814883                        $struc['size']        = $this->size( $path . $entry );
    815884                        $struc['lastmodunix'] = $this->mtime( $path . $entry );
    816                         $struc['lastmod']     = gmdate( 'M j', $struc['lastmodunix'] );
    817                         $struc['time']        = gmdate( 'h:i:s', $struc['lastmodunix'] );
     885                        $struc['lastmod']     = is_int( $struc['lastmodunix'] ) ? gmdate( 'M j', $struc['lastmodunix'] ) : false;
     886                        $struc['time']        = is_int( $struc['lastmodunix'] ) ? gmdate( 'h:i:s', $struc['lastmodunix'] ) : false;
    818887                        $struc['type']        = $this->is_dir( $path . $entry ) ? 'd' : 'f';
    819888
Note: See TracChangeset for help on using the changeset viewer.