Make WordPress Core

Changeset 62637


Ignore:
Timestamp:
07/05/2026 01:15:23 AM (2 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.

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/composer.json

    r62618 r62637  
    1818        "suggest": {
    1919                "ext-dom": "*",
    20                 "ext-mysqli": "*"
     20                "ext-ftp": "*",
     21                "ext-mysqli": "*",
     22                "ext-ssh2": "*"
    2123        },
    2224        "require-dev": {
  • trunk/src/wp-admin/includes/class-wp-filesystem-base.php

    r57644 r62637  
    1111 *
    1212 * @since 2.5.0
     13 *
     14 * @phpstan-type FileListing array{
     15 *     name: string,
     16 *     perms?: string,
     17 *     permsn?: string,
     18 *     number?: int|string|false,
     19 *     owner?: string|int<1, max>|false,
     20 *     group?: string|int<1, max>|false,
     21 *     size: int|string|false,
     22 *     lastmodunix?: int|string|false,
     23 *     lastmod?: string|false,
     24 *     time: int|string|false,
     25 *     type: 'd'|'f'|'l',
     26 *     islink?: bool,
     27 *     isdir?: bool,
     28 *     files?: mixed[]|false, // The mixed[] is actually FileListing[] but PHPStan does not support recursive or self-referencing array shapes.
     29 * }
    1330 */
    1431#[AllowDynamicProperties]
     
    2744         *
    2845         * @since 2.7.0
    29          * @var array
     46         * @var array<string, string>
    3047         */
    3148        public $cache = array();
     
    4562
    4663        /**
     64         * @var array<string, mixed>
    4765         */
    4866        public $options = array();
     
    5371         * @since 2.7.0
    5472         *
    55          * @return string The location of the remote path.
     73         * @return string|false The location of the remote path, or false on failure.
    5674         */
    5775        public function abspath() {
     
    7492         * @since 2.7.0
    7593         *
    76          * @return string The location of the remote path.
     94         * @return string|false The location of the remote path, or false on failure.
    7795         */
    7896        public function wp_content_dir() {
     
    85103         * @since 2.7.0
    86104         *
    87          * @return string The location of the remote path.
     105         * @return string|false The location of the remote path, or false on failure.
    88106         */
    89107        public function wp_plugins_dir() {
     
    98116         * @param string|false $theme Optional. The theme stylesheet or template for the directory.
    99117         *                            Default false.
    100          * @return string The location of the remote path.
     118         * @return string|false The location of the remote path, or false on failure.
    101119         */
    102120        public function wp_themes_dir( $theme = false ) {
    103                 $theme_root = get_theme_root( $theme );
     121                $theme_root = get_theme_root( is_string( $theme ) ? $theme : '' );
    104122
    105123                // Account for relative theme roots.
     
    116134         * @since 3.2.0
    117135         *
    118          * @return string The location of the remote path.
     136         * @return string|false The location of the remote path, or false on failure.
    119137         */
    120138        public function wp_lang_dir() {
     
    135153         * @param string $base    Optional. The folder to start searching from. Default '.'.
    136154         * @param bool   $verbose Optional. True to display debug information. Default false.
    137          * @return string The location of the remote path.
     155         * @return string|false The location of the remote path, or false on failure.
    138156         */
    139157        public function find_base_dir( $base = '.', $verbose = false ) {
     
    156174         * @param string $base    Optional. The folder to start searching from. Default '.'.
    157175         * @param bool   $verbose Optional. True to display debug information. Default false.
    158          * @return string The location of the remote path.
     176         * @return string|false The location of the remote path, or false on failure.
    159177         */
    160178        public function get_base_dir( $base = '.', $verbose = false ) {
     
    195213
    196214                                if ( $folder === $dir ) {
    197                                         return trailingslashit( constant( $constant ) );
     215                                        /** @var string $constant_value */
     216                                        $constant_value = constant( $constant );
     217                                        return trailingslashit( $constant_value );
    198218                                }
    199219                        }
     
    206226
    207227                                if ( 0 === stripos( $folder, $dir ) ) { // $folder starts with $dir.
    208                                         $potential_folder = preg_replace( '#^' . preg_quote( $dir, '#' ) . '/#i', trailingslashit( constant( $constant ) ), $folder );
     228                                        /** @var string $constant_value */
     229                                        $constant_value   = constant( $constant );
     230                                        $potential_folder = (string) preg_replace( '#^' . preg_quote( $dir, '#' ) . '/#i', trailingslashit( $constant_value ), $folder );
    209231                                        $potential_folder = trailingslashit( $potential_folder );
    210232
     
    222244                }
    223245
    224                 $folder = preg_replace( '|^([a-z]{1}):|i', '', $folder ); // Strip out Windows drive letter if it's there.
     246                $folder = (string) preg_replace( '|^([a-z]{1}):|i', '', $folder ); // Strip out Windows drive letter if it's there.
    225247                $folder = str_replace( '\\', '/', $folder ); // Windows path sanitization.
    226248
     
    259281        public function search_for_folder( $folder, $base = '.', $loop = false ) {
    260282                if ( empty( $base ) || '.' === $base ) {
    261                         $base = trailingslashit( $this->cwd() );
     283                        $cwd  = $this->cwd();
     284                        $base = is_string( $cwd ) ? trailingslashit( $cwd ) : '/';
    262285                }
    263286
     
    421444                $realmode = '';
    422445                $legal    = array( '', 'w', 'r', 'x', '-' );
    423                 $attarray = preg_split( '//', $mode );
     446                $attarray = (array) preg_split( '//', $mode );
    424447
    425448                for ( $i = 0, $c = count( $attarray ); $i < $c; $i++ ) {
     
    441464
    442465                $newmode  = $mode[0];
    443                 $newmode .= $mode[1] + $mode[2] + $mode[3];
    444                 $newmode .= $mode[4] + $mode[5] + $mode[6];
    445                 $newmode .= $mode[7] + $mode[8] + $mode[9];
     466                $newmode .= (int) $mode[1] + (int) $mode[2] + (int) $mode[3];
     467                $newmode .= (int) $mode[4] + (int) $mode[5] + (int) $mode[6];
     468                $newmode .= (int) $mode[7] + (int) $mode[8] + (int) $mode[9];
    446469
    447470                return $newmode;
     
    509532         *
    510533         * @param string $file Path to the file.
    511          * @return array|false File contents in an array on success, false on failure.
     534         * @return string[]|false File contents in an array on success, false on failure.
    512535         */
    513536        public function get_contents_array( $file ) {
     
    852875         *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
    853876         *                                             false if not available.
    854          *         @type string|false     $time        Last modified time, or false if not available.
     877         *         @type int|string|false $time        Last modified time. A Unix timestamp on FTP transports, or false if not available.
    855878         *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
    856879         *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
     
    858881         *     }
    859882         * }
     883         * @phpstan-return array<string, FileListing>|false
    860884         */
    861885        public function dirlist( $path, $include_hidden = true, $recursive = false ) {
  • trunk/src/wp-admin/includes/class-wp-filesystem-direct.php

    r62636 r62637  
    1313 *
    1414 * @see WP_Filesystem_Base
     15 * @phpstan-import-type FileListing from WP_Filesystem_Base
    1516 */
    1617class WP_Filesystem_Direct extends WP_Filesystem_Base {
     
    2425         */
    2526        public function __construct( $arg ) {
     27                // The $arg parameter is required for signature parity with the other transports, but is unused here.
     28                unset( $arg );
    2629                $this->method = 'direct';
    2730                $this->errors = new WP_Error();
     
    4649         *
    4750         * @param string $file Path to the file.
    48          * @return array|false File contents in an array on success, false on failure.
     51         * @return string[]|false File contents in an array on success, false on failure.
    4952         */
    5053        public function get_contents_array( $file ) {
     
    139142                $file     = trailingslashit( $file );
    140143                $filelist = $this->dirlist( $file );
     144                if ( false === $filelist ) {
     145                        return false;
     146                }
    141147
    142148                foreach ( $filelist as $file_listing ) {
     
    227233                // Is a directory, and we want recursive.
    228234                $filelist = $this->dirlist( $file );
     235                if ( false === $filelist ) {
     236                        return false;
     237                }
    229238
    230239                foreach ( $filelist as $file_listing ) {
     
    241250         *
    242251         * @param string $file Path to the file.
    243          * @return string|false Username of the owner on success, false on failure.
     252         * @return string|int<1, max>|false Username of the owner on success, or UID of file owner if not available; false on failure.
    244253         */
    245254        public function owner( $file ) {
     
    286295         *
    287296         * @param string $file Path to the file.
    288          * @return string|false The group on success, false on failure.
     297         * @return string|int<1, max>|false Group name on success, or GID of the file's group if not available; false on failure.
    289298         */
    290299        public function group( $file ) {
     
    640649         *     }
    641650         * }
     651         * @phpstan-return array<string, FileListing>|false
    642652         */
    643653        public function dirlist( $path, $include_hidden = true, $recursive = false ) {
     
    685695                        $struc['size']        = $this->size( $path . $entry );
    686696                        $struc['lastmodunix'] = $this->mtime( $path . $entry );
    687                         $struc['lastmod']     = gmdate( 'M j', $struc['lastmodunix'] );
    688                         $struc['time']        = gmdate( 'h:i:s', $struc['lastmodunix'] );
     697                        $struc['lastmod']     = is_int( $struc['lastmodunix'] ) ? gmdate( 'M j', $struc['lastmodunix'] ) : false;
     698                        $struc['time']        = is_int( $struc['lastmodunix'] ) ? gmdate( 'h:i:s', $struc['lastmodunix'] ) : false;
    689699                        $struc['type']        = $this->is_dir( $path . $entry ) ? 'd' : 'f';
    690700
  • trunk/src/wp-admin/includes/class-wp-filesystem-ftpext.php

    r61311 r62637  
    1313 *
    1414 * @see WP_Filesystem_Base
     15 * @phpstan-type Options array{
     16 *     hostname: string,
     17 *     username: string,
     18 *     password: string,
     19 *     port: non-negative-int,
     20 *     ssl: bool,
     21 * }
     22 * @phpstan-import-type FileListing from WP_Filesystem_Base
    1523 */
    1624class WP_Filesystem_FTPext extends WP_Filesystem_Base {
     
    2331
    2432        /**
     33         * @since 7.1.0
     34         * @var array
     35         * @phpstan-var Options
     36         */
     37        public $options;
     38
     39        /**
    2540         * Constructor.
    2641         *
    2742         * @since 2.5.0
    2843         *
    29          * @param array $opt
    30          */
    31         public function __construct( $opt = '' ) {
    32                 $this->method = 'ftpext';
    33                 $this->errors = new WP_Error();
     44         * @param array $opt {
     45         *     Array of connection options.
     46         *
     47         *     @type string $hostname        Required. FTP server hostname.
     48         *     @type string $username        Required. FTP username.
     49         *     @type string $password        Required. FTP password.
     50         *     @type int    $port            Optional. FTP server port. Default 21.
     51         *     @type string $connection_type Optional. Connection type. Use 'ftps' to enable SSL.
     52         * }
     53         * @phpstan-param array{
     54         *     hostname: non-empty-string,
     55         *     username: non-empty-string,
     56         *     password: string,
     57         *     port?: non-negative-int,
     58         *     connection_type?: 'ftps',
     59         * }|null $opt
     60         */
     61        public function __construct( $opt = null ) {
     62                $this->method  = 'ftpext';
     63                $this->errors  = new WP_Error();
     64                $this->options = array(
     65                        'port'     => 21,
     66                        'hostname' => '',
     67                        'username' => '',
     68                        'password' => '',
     69                        'ssl'      => false,
     70                );
    3471
    3572                // Check if possible to use ftp functions.
     
    4481                }
    4582
    46                 if ( empty( $opt['port'] ) ) {
    47                         $this->options['port'] = 21;
    48                 } else {
     83                if ( ! is_array( $opt ) ) {
     84                        $opt = array();
     85                }
     86
     87                if ( ! empty( $opt['port'] ) ) {
    4988                        $this->options['port'] = $opt['port'];
    5089                }
     
    69108                }
    70109
    71                 $this->options['ssl'] = false;
    72 
    73110                if ( isset( $opt['connection_type'] ) && 'ftps' === $opt['connection_type'] ) {
    74111                        $this->options['ssl'] = true;
     
    84121         */
    85122        public function connect() {
     123                /*
     124                 * Bail if the constructor recorded a configuration error. Connection and
     125                 * authentication errors are excluded so that a failed connection attempt
     126                 * can be retried on the same instance.
     127                 */
     128                if ( $this->errors->has_errors() && ! array_intersect( array( 'connect', 'auth' ), $this->errors->get_error_codes() ) ) {
     129                        return false;
     130                }
     131
    86132                if ( isset( $this->options['ssl'] ) && $this->options['ssl'] && function_exists( 'ftp_ssl_connect' ) ) {
    87133                        $this->link = @ftp_ssl_connect( $this->options['hostname'], $this->options['port'], FS_CONNECT_TIMEOUT );
     
    136182         */
    137183        public function get_contents( $file ) {
     184                if ( ! $this->link ) {
     185                        return false;
     186                }
     187
    138188                $tempfile   = wp_tempnam( $file );
    139189                $temphandle = fopen( $tempfile, 'w+' );
     
    169219         *
    170220         * @param string $file Path to the file.
    171          * @return array|false File contents in an array on success, false on failure.
     221         * @return string[]|false File contents in an array on success, false on failure.
    172222         */
    173223        public function get_contents_array( $file ) {
    174                 return explode( "\n", $this->get_contents( $file ) );
     224                $contents = $this->get_contents( $file );
     225                if ( is_string( $contents ) ) {
     226                        return explode( "\n", $contents );
     227                }
     228                return false;
    175229        }
    176230
     
    295349         *
    296350         * @param string $file Path to the file.
    297          * @return string|false Username of the owner on success, false on failure.
     351         * @return string|int<1, max>|false Username of the owner on success, false on failure.
    298352         */
    299353        public function owner( $file ) {
     
    323377         *
    324378         * @param string $file Path to the file.
    325          * @return string|false The group on success, false on failure.
     379         * @return string|int<1, max>|false The group on success, false on failure.
    326380         */
    327381        public function group( $file ) {
     
    466520         */
    467521        public function is_dir( $path ) {
    468                 $cwd    = $this->cwd();
     522                $cwd = $this->cwd();
     523                if ( false === $cwd ) {
     524                        return false;
     525                }
     526
    469527                $result = @ftp_chdir( $this->link, trailingslashit( $path ) );
     528
     529                if ( ! $this->link ) {
     530                        return false;
     531                }
    470532
    471533                if ( $result && $path === $this->cwd() || $this->cwd() !== $cwd ) {
     
    623685         *                                     False if unable to list directory contents.
    624686         * }
     687         * @phpstan-return FileListing|''
    625688         */
    626689        public function parselisting( $line ) {
     
    648711                        }
    649712
    650                         $b['size']   = $lucifer[7];
    651                         $b['month']  = $lucifer[1];
    652                         $b['day']    = $lucifer[2];
    653                         $b['year']   = $lucifer[3];
    654                         $b['hour']   = $lucifer[4];
    655                         $b['minute'] = $lucifer[5];
    656                         $b['time']   = mktime( $lucifer[4] + ( strcasecmp( $lucifer[6], 'PM' ) === 0 ? 12 : 0 ), $lucifer[5], 0, $lucifer[1], $lucifer[2], $lucifer[3] );
    657                         $b['am/pm']  = $lucifer[6];
    658                         $b['name']   = $lucifer[8];
     713                        $b['size'] = $lucifer[7];
     714                        $b['time'] = mktime( (int) $lucifer[4] + ( strcasecmp( $lucifer[6], 'PM' ) === 0 ? 12 : 0 ), (int) $lucifer[5], 0, (int) $lucifer[1], (int) $lucifer[2], (int) $lucifer[3] );
     715                        $b['name'] = $lucifer[8];
    659716                } elseif ( ! $is_windows ) {
    660717                        $lucifer = preg_split( '/[ ]/', $line, 9, PREG_SPLIT_NO_EMPTY );
     
    687744
    688745                                if ( 8 === $lcount ) {
    689                                         sscanf( $lucifer[5], '%d-%d-%d', $b['year'], $b['month'], $b['day'] );
    690                                         sscanf( $lucifer[6], '%d:%d', $b['hour'], $b['minute'] );
    691 
    692                                         $b['time'] = mktime( $b['hour'], $b['minute'], 0, $b['month'], $b['day'], $b['year'] );
     746                                        sscanf( $lucifer[5], '%d-%d-%d', $year, $month, $day );
     747                                        sscanf( $lucifer[6], '%d:%d', $hour, $minute );
     748
     749                                        $b['time'] = mktime( (int) $hour, (int) $minute, 0, (int) $month, (int) $day, (int) $year );
    693750                                        $b['name'] = $lucifer[7];
    694751                                } else {
    695                                         $b['month'] = $lucifer[5];
    696                                         $b['day']   = $lucifer[6];
     752                                        $month = $lucifer[5];
     753                                        $day   = $lucifer[6];
    697754
    698755                                        if ( preg_match( '/([0-9]{2}):([0-9]{2})/', $lucifer[7], $l2 ) ) {
    699                                                 $b['year']   = gmdate( 'Y' );
    700                                                 $b['hour']   = $l2[1];
    701                                                 $b['minute'] = $l2[2];
     756                                                $year   = gmdate( 'Y' );
     757                                                $hour   = $l2[1];
     758                                                $minute = $l2[2];
    702759                                        } else {
    703                                                 $b['year']   = $lucifer[7];
    704                                                 $b['hour']   = 0;
    705                                                 $b['minute'] = 0;
     760                                                $year   = $lucifer[7];
     761                                                $hour   = 0;
     762                                                $minute = 0;
    706763                                        }
    707764
    708                                         $b['time'] = strtotime( sprintf( '%d %s %d %02d:%02d', $b['day'], $b['month'], $b['year'], $b['hour'], $b['minute'] ) );
     765                                        $b['time'] = strtotime( sprintf( '%d %s %d %02d:%02d', $day, $month, $year, $hour, $minute ) );
    709766                                        $b['name'] = $lucifer[8];
    710767                                }
     
    714771                // Replace symlinks formatted as "source -> target" with just the source name.
    715772                if ( isset( $b['islink'] ) && $b['islink'] ) {
    716                         $b['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $b['name'] );
    717                 }
    718 
    719                 return $b;
     773                        $b['name'] = (string) preg_replace( '/(\s*->\s*.*)$/', '', $b['name'] );
     774                }
     775
     776                return $b ?? '';
    720777        }
    721778
     
    748805         *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
    749806         *                                             false if not available.
    750          *         @type string|false     $time        Last modified time, or false if not available.
     807         *         @type int|string|false $time        Last modified time as a Unix timestamp, or false if not available.
    751808         *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
    752809         *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
     
    754811         *     }
    755812         * }
     813         * @phpstan-return array<string, FileListing>|false
    756814         */
    757815        public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) {
     816                if ( ! $this->link ) {
     817                        return false;
     818                }
     819
    758820                if ( $this->is_file( $path ) ) {
    759821                        $limit_file = basename( $path );
     
    764826
    765827                $pwd = ftp_pwd( $this->link );
     828                if ( ! is_string( $pwd ) ) {
     829                        return false;
     830                }
    766831
    767832                if ( ! @ftp_chdir( $this->link, $path ) ) { // Can't change to folder = folder doesn't exist.
     
    769834                }
    770835
     836                /** @var string[]|false $list */
    771837                $list = ftp_rawlist( $this->link, '-a', false );
    772838
  • trunk/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php

    r61311 r62637  
    1313 *
    1414 * @see WP_Filesystem_Base
     15 * @phpstan-type Options array{
     16 *     hostname: string,
     17 *     username: string,
     18 *     password: string,
     19 *     port: non-negative-int,
     20 * }
     21 * @phpstan-import-type FileListing from WP_Filesystem_Base
    1522 */
    1623class WP_Filesystem_ftpsockets extends WP_Filesystem_Base {
     
    2330
    2431        /**
     32         * @since 7.1.0
     33         * @var array
     34         * @phpstan-var Options
     35         */
     36        public $options;
     37
     38        /**
    2539         * Constructor.
    2640         *
    2741         * @since 2.5.0
    2842         *
    29          * @param array $opt
    30          */
    31         public function __construct( $opt = '' ) {
    32                 $this->method = 'ftpsockets';
    33                 $this->errors = new WP_Error();
     43         * @param array $opt {
     44         *     Array of connection options.
     45         *
     46         *     @type string $hostname Required. FTP server hostname.
     47         *     @type string $username Required. FTP username.
     48         *     @type string $password Required. FTP password.
     49         *     @type int $port Optional. FTP server port. Default 21.
     50         * }
     51         * @phpstan-param array{
     52         *     hostname: non-empty-string,
     53         *     username: non-empty-string,
     54         *     password: string,
     55         *     port?: non-negative-int,
     56         * }|null $opt
     57         */
     58        public function __construct( $opt = null ) {
     59                $this->method  = 'ftpsockets';
     60                $this->errors  = new WP_Error();
     61                $this->options = array(
     62                        'port'     => 21,
     63                        'hostname' => '',
     64                        'username' => '',
     65                        'password' => '',
     66                );
    3467
    3568                // Check if possible to use ftp functions.
     
    4073                $this->ftp = new ftp();
    4174
    42                 if ( empty( $opt['port'] ) ) {
    43                         $this->options['port'] = 21;
    44                 } else {
     75                if ( ! is_array( $opt ) ) {
     76                        $opt = array();
     77                }
     78
     79                if ( ! empty( $opt['port'] ) ) {
    4580                        $this->options['port'] = (int) $opt['port'];
    4681                }
     
    74109         */
    75110        public function connect() {
     111                /*
     112                 * Bail if the constructor recorded a configuration error. Connection and
     113                 * authentication errors are excluded so that a failed connection attempt
     114                 * can be retried on the same instance.
     115                 */
     116                if ( $this->errors->has_errors() && ! array_intersect( array( 'connect', 'auth' ), $this->errors->get_error_codes() ) ) {
     117                        return false;
     118                }
     119
    76120                if ( ! $this->ftp ) {
    77121                        return false;
     
    180224         *
    181225         * @param string $file Path to the file.
    182          * @return array|false File contents in an array on success, false on failure.
     226         * @return string[]|false File contents in an array on success, false on failure.
    183227         */
    184228        public function get_contents_array( $file ) {
    185                 return explode( "\n", $this->get_contents( $file ) );
     229                $contents = $this->get_contents( $file );
     230                if ( is_string( $contents ) ) {
     231                        return explode( "\n", $contents );
     232                }
     233                return false;
    186234        }
    187235
     
    222270                fseek( $temphandle, 0 ); // Skip back to the start of the file being written to.
    223271
    224                 $ret = $this->ftp->fput( $file, $temphandle );
     272                $ret = (bool) $this->ftp->fput( $file, $temphandle );
    225273
    226274                reset_mbstring_encoding();
     
    243291        public function cwd() {
    244292                $cwd = $this->ftp->pwd();
     293                if ( ! is_string( $cwd ) ) {
     294                        return false;
     295                }
    245296
    246297                if ( $cwd ) {
     
    260311         */
    261312        public function chdir( $dir ) {
    262                 return $this->ftp->chdir( $dir );
     313                return (bool) $this->ftp->chdir( $dir );
    263314        }
    264315
     
    296347
    297348                // chmod the file or directory.
    298                 return $this->ftp->chmod( $file, $mode );
     349                return (bool) $this->ftp->chmod( $file, $mode );
    299350        }
    300351
     
    305356         *
    306357         * @param string $file Path to the file.
    307          * @return string|false Username of the owner on success, false on failure.
     358         * @return string|int<1, max>|false Username of the owner on success, false on failure.
    308359         */
    309360        public function owner( $file ) {
     
    333384         *
    334385         * @param string $file Path to the file.
    335          * @return string|false The group on success, false on failure.
     386         * @return string|int<1, max>|false The group on success, false on failure.
    336387         */
    337388        public function group( $file ) {
     
    387438         */
    388439        public function move( $source, $destination, $overwrite = false ) {
    389                 return $this->ftp->rename( $source, $destination );
     440                return (bool) $this->ftp->rename( $source, $destination );
    390441        }
    391442
     
    408459
    409460                if ( 'f' === $type || $this->is_file( $file ) ) {
    410                         return $this->ftp->delete( $file );
     461                        return (bool) $this->ftp->delete( $file );
    411462                }
    412463
    413464                if ( ! $recursive ) {
    414                         return $this->ftp->rmdir( $file );
    415                 }
    416 
    417                 return $this->ftp->mdel( $file );
     465                        return (bool) $this->ftp->rmdir( $file );
     466                }
     467
     468                return (bool) $this->ftp->mdel( $file );
    418469        }
    419470
     
    478529        public function is_dir( $path ) {
    479530                $cwd = $this->cwd();
     531                if ( ! $cwd ) {
     532                        return false;
     533                }
    480534
    481535                if ( $this->chdir( $path ) ) {
     
    532586         */
    533587        public function mtime( $file ) {
    534                 return $this->ftp->mdtm( $file );
     588                $modified_time = $this->ftp->mdtm( $file );
     589                if ( false === $modified_time ) {
     590                        return false;
     591                }
     592                return (int) $modified_time;
    535593        }
    536594
     
    544602         */
    545603        public function size( $file ) {
    546                 return $this->ftp->filesize( $file );
     604                $size = $this->ftp->filesize( $file );
     605                if ( false === $size ) {
     606                        return false;
     607                }
     608                return (int) $size;
    547609        }
    548610
     
    641703         *         @type string|false     $lastmod     Last modified month (3 letters) and day (without leading 0), or
    642704         *                                             false if not available.
    643          *         @type string|false     $time        Last modified time, or false if not available.
     705         *         @type int|string|false $time        Last modified time as a Unix timestamp, or false if not available.
    644706         *         @type string           $type        Type of resource. 'f' for file, 'd' for directory, 'l' for link.
    645707         *         @type array|false      $files       If a directory and `$recursive` is true, contains another array of
     
    647709         *     }
    648710         * }
     711         * @phpstan-return array<string, FileListing>|false
    649712         */
    650713        public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) {
     
    658721                mbstring_binary_safe_encoding();
    659722
     723                /** @var array<string, FileListing>|false $list */
    660724                $list = $this->ftp->dirlist( $path );
    661725
    662                 if ( empty( $list ) && ! $this->exists( $path ) ) {
     726                if ( ! is_array( $list ) || ( empty( $list ) && ! $this->exists( $path ) ) ) {
    663727
    664728                        reset_mbstring_encoding();
     
    693757
    694758                        // Replace symlinks formatted as "source -> target" with just the source name.
    695                         if ( $struc['islink'] ) {
    696                                 $struc['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $struc['name'] );
     759                        if ( $struc['islink'] ?? false ) {
     760                                $struc['name'] = (string) preg_replace( '/(\s*->\s*.*)$/', '', $struc['name'] );
    697761                        }
    698762
    699763                        // Add the octal representation of the file permissions.
    700                         $struc['permsn'] = $this->getnumchmodfromh( $struc['perms'] );
     764                        if ( isset( $struc['perms'] ) ) {
     765                                $struc['permsn'] = $this->getnumchmodfromh( $struc['perms'] );
     766                        }
    701767
    702768                        $ret[ $struc['name'] ] = $struc;
  • 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.