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-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
Note: See TracChangeset for help on using the changeset viewer.