diff --git a/composer.json b/composer.json index 6500e7ccbf8af..86d9efbe249e6 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,9 @@ }, "suggest": { "ext-dom": "*", - "ext-mysqli": "*" + "ext-ftp": "*", + "ext-mysqli": "*", + "ext-ssh2": "*" }, "require-dev": { "composer/ca-bundle": "1.5.12", diff --git a/src/wp-admin/includes/class-wp-filesystem-base.php b/src/wp-admin/includes/class-wp-filesystem-base.php index 125c2d3b9a8b0..4972c8421cf11 100644 --- a/src/wp-admin/includes/class-wp-filesystem-base.php +++ b/src/wp-admin/includes/class-wp-filesystem-base.php @@ -10,6 +10,23 @@ * Base WordPress Filesystem class which Filesystem implementations extend. * * @since 2.5.0 + * + * @phpstan-type FileListing array{ + * name: string, + * perms?: string, + * permsn?: string, + * number?: int|string|false, + * owner?: string|int<1, max>|false, + * group?: string|int<1, max>|false, + * size: int|string|false, + * lastmodunix?: int|string|false, + * lastmod?: string|false, + * time: int|string|false, + * type: 'd'|'f'|'l', + * islink?: bool, + * isdir?: bool, + * files?: mixed[]|false, // The mixed[] is actually FileListing[] but PHPStan does not support recursive or self-referencing array shapes. + * } */ #[AllowDynamicProperties] class WP_Filesystem_Base { @@ -26,7 +43,7 @@ class WP_Filesystem_Base { * Cached list of local filepaths to mapped remote filepaths. * * @since 2.7.0 - * @var array + * @var array */ public $cache = array(); @@ -44,6 +61,7 @@ class WP_Filesystem_Base { public $errors = null; /** + * @var array */ public $options = array(); @@ -52,7 +70,7 @@ class WP_Filesystem_Base { * * @since 2.7.0 * - * @return string The location of the remote path. + * @return string|false The location of the remote path, or false on failure. */ public function abspath() { $folder = $this->find_folder( ABSPATH ); @@ -73,7 +91,7 @@ public function abspath() { * * @since 2.7.0 * - * @return string The location of the remote path. + * @return string|false The location of the remote path, or false on failure. */ public function wp_content_dir() { return $this->find_folder( WP_CONTENT_DIR ); @@ -84,7 +102,7 @@ public function wp_content_dir() { * * @since 2.7.0 * - * @return string The location of the remote path. + * @return string|false The location of the remote path, or false on failure. */ public function wp_plugins_dir() { return $this->find_folder( WP_PLUGIN_DIR ); @@ -97,10 +115,10 @@ public function wp_plugins_dir() { * * @param string|false $theme Optional. The theme stylesheet or template for the directory. * Default false. - * @return string The location of the remote path. + * @return string|false The location of the remote path, or false on failure. */ public function wp_themes_dir( $theme = false ) { - $theme_root = get_theme_root( $theme ); + $theme_root = get_theme_root( is_string( $theme ) ? $theme : '' ); // Account for relative theme roots. if ( '/themes' === $theme_root || ! is_dir( $theme_root ) ) { @@ -115,7 +133,7 @@ public function wp_themes_dir( $theme = false ) { * * @since 3.2.0 * - * @return string The location of the remote path. + * @return string|false The location of the remote path, or false on failure. */ public function wp_lang_dir() { return $this->find_folder( WP_LANG_DIR ); @@ -134,7 +152,7 @@ public function wp_lang_dir() { * * @param string $base Optional. The folder to start searching from. Default '.'. * @param bool $verbose Optional. True to display debug information. Default false. - * @return string The location of the remote path. + * @return string|false The location of the remote path, or false on failure. */ public function find_base_dir( $base = '.', $verbose = false ) { _deprecated_function( __FUNCTION__, '2.7.0', 'WP_Filesystem_Base::abspath() or WP_Filesystem_Base::wp_*_dir()' ); @@ -155,7 +173,7 @@ public function find_base_dir( $base = '.', $verbose = false ) { * * @param string $base Optional. The folder to start searching from. Default '.'. * @param bool $verbose Optional. True to display debug information. Default false. - * @return string The location of the remote path. + * @return string|false The location of the remote path, or false on failure. */ public function get_base_dir( $base = '.', $verbose = false ) { _deprecated_function( __FUNCTION__, '2.7.0', 'WP_Filesystem_Base::abspath() or WP_Filesystem_Base::wp_*_dir()' ); @@ -194,7 +212,9 @@ public function find_folder( $folder ) { } if ( $folder === $dir ) { - return trailingslashit( constant( $constant ) ); + /** @var string $constant_value */ + $constant_value = constant( $constant ); + return trailingslashit( $constant_value ); } } @@ -205,7 +225,9 @@ public function find_folder( $folder ) { } if ( 0 === stripos( $folder, $dir ) ) { // $folder starts with $dir. - $potential_folder = preg_replace( '#^' . preg_quote( $dir, '#' ) . '/#i', trailingslashit( constant( $constant ) ), $folder ); + /** @var string $constant_value */ + $constant_value = constant( $constant ); + $potential_folder = (string) preg_replace( '#^' . preg_quote( $dir, '#' ) . '/#i', trailingslashit( $constant_value ), $folder ); $potential_folder = trailingslashit( $potential_folder ); if ( $this->is_dir( $potential_folder ) ) { @@ -221,7 +243,7 @@ public function find_folder( $folder ) { return trailingslashit( $folder ); } - $folder = preg_replace( '|^([a-z]{1}):|i', '', $folder ); // Strip out Windows drive letter if it's there. + $folder = (string) preg_replace( '|^([a-z]{1}):|i', '', $folder ); // Strip out Windows drive letter if it's there. $folder = str_replace( '\\', '/', $folder ); // Windows path sanitization. if ( isset( $this->cache[ $folder ] ) ) { @@ -258,7 +280,8 @@ public function find_folder( $folder ) { */ public function search_for_folder( $folder, $base = '.', $loop = false ) { if ( empty( $base ) || '.' === $base ) { - $base = trailingslashit( $this->cwd() ); + $cwd = $this->cwd(); + $base = is_string( $cwd ) ? trailingslashit( $cwd ) : '/'; } $folder = untrailingslashit( $folder ); @@ -420,7 +443,7 @@ public function getchmod( $file ) { public function getnumchmodfromh( $mode ) { $realmode = ''; $legal = array( '', 'w', 'r', 'x', '-' ); - $attarray = preg_split( '//', $mode ); + $attarray = (array) preg_split( '//', $mode ); for ( $i = 0, $c = count( $attarray ); $i < $c; $i++ ) { $key = array_search( $attarray[ $i ], $legal, true ); @@ -440,9 +463,9 @@ public function getnumchmodfromh( $mode ) { $mode = strtr( $mode, $trans ); $newmode = $mode[0]; - $newmode .= $mode[1] + $mode[2] + $mode[3]; - $newmode .= $mode[4] + $mode[5] + $mode[6]; - $newmode .= $mode[7] + $mode[8] + $mode[9]; + $newmode .= (int) $mode[1] + (int) $mode[2] + (int) $mode[3]; + $newmode .= (int) $mode[4] + (int) $mode[5] + (int) $mode[6]; + $newmode .= (int) $mode[7] + (int) $mode[8] + (int) $mode[9]; return $newmode; } @@ -508,7 +531,7 @@ public function get_contents( $file ) { * @abstract * * @param string $file Path to the file. - * @return array|false File contents in an array on success, false on failure. + * @return string[]|false File contents in an array on success, false on failure. */ public function get_contents_array( $file ) { return false; @@ -851,12 +874,13 @@ public function rmdir( $path, $recursive = false ) { * False if not available. * @type string|false $lastmod Last modified month (3 letters) and day (without leading 0), or * false if not available. - * @type string|false $time Last modified time, or false if not available. + * @type int|string|false $time Last modified time. A Unix timestamp on FTP transports, or false if not available. * @type string $type Type of resource. 'f' for file, 'd' for directory, 'l' for link. * @type array|false $files If a directory and `$recursive` is true, contains another array of * files. False if unable to list directory contents. * } * } + * @phpstan-return array|false */ public function dirlist( $path, $include_hidden = true, $recursive = false ) { return false; diff --git a/src/wp-admin/includes/class-wp-filesystem-direct.php b/src/wp-admin/includes/class-wp-filesystem-direct.php index a4b197c15229f..7ce13d8644a5b 100644 --- a/src/wp-admin/includes/class-wp-filesystem-direct.php +++ b/src/wp-admin/includes/class-wp-filesystem-direct.php @@ -12,6 +12,7 @@ * @since 2.5.0 * * @see WP_Filesystem_Base + * @phpstan-import-type FileListing from WP_Filesystem_Base */ class WP_Filesystem_Direct extends WP_Filesystem_Base { @@ -23,6 +24,7 @@ class WP_Filesystem_Direct extends WP_Filesystem_Base { * @param mixed $arg Not used. */ public function __construct( $arg ) { + unset( $arg ); $this->method = 'direct'; $this->errors = new WP_Error(); } @@ -45,7 +47,7 @@ public function get_contents( $file ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return array|false File contents in an array on success, false on failure. + * @return string[]|false File contents in an array on success, false on failure. */ public function get_contents_array( $file ) { return @file( $file ); @@ -138,9 +140,12 @@ public function chgrp( $file, $group, $recursive = false ) { // Is a directory, and we want recursive. $file = trailingslashit( $file ); $filelist = $this->dirlist( $file ); + if ( false === $filelist ) { + return false; + } - foreach ( $filelist as $filename ) { - $this->chgrp( $file . $filename, $group, $recursive ); + foreach ( $filelist as $file_listing ) { + $this->chgrp( $file . $file_listing['name'], $group, $recursive ); } return true; @@ -226,9 +231,12 @@ public function chown( $file, $owner, $recursive = false ) { // Is a directory, and we want recursive. $filelist = $this->dirlist( $file ); + if ( false === $filelist ) { + return false; + } - foreach ( $filelist as $filename ) { - $this->chown( $file . '/' . $filename, $owner, $recursive ); + foreach ( $filelist as $file_listing ) { + $this->chown( $file . '/' . $file_listing['name'], $owner, $recursive ); } return true; @@ -240,7 +248,7 @@ public function chown( $file, $owner, $recursive = false ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return string|false Username of the owner on success, false on failure. + * @return string|int<1, max>|false Username of the owner on success, or UID of file owner if not available; false on failure. */ public function owner( $file ) { $owneruid = @fileowner( $file ); @@ -285,7 +293,7 @@ public function getchmod( $file ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return string|false The group on success, false on failure. + * @return string|int<1, max>|false Username of the owner on success, or group ID of file owner if not available; false on failure. */ public function group( $file ) { $gid = @filegroup( $file ); @@ -639,6 +647,7 @@ public function rmdir( $path, $recursive = false ) { * files. False if unable to list directory contents. * } * } + * @phpstan-return array|false */ public function dirlist( $path, $include_hidden = true, $recursive = false ) { if ( $this->is_file( $path ) ) { @@ -684,8 +693,8 @@ public function dirlist( $path, $include_hidden = true, $recursive = false ) { $struc['group'] = $this->group( $path . $entry ); $struc['size'] = $this->size( $path . $entry ); $struc['lastmodunix'] = $this->mtime( $path . $entry ); - $struc['lastmod'] = gmdate( 'M j', $struc['lastmodunix'] ); - $struc['time'] = gmdate( 'h:i:s', $struc['lastmodunix'] ); + $struc['lastmod'] = is_int( $struc['lastmodunix'] ) ? gmdate( 'M j', $struc['lastmodunix'] ) : false; + $struc['time'] = is_int( $struc['lastmodunix'] ) ? gmdate( 'h:i:s', $struc['lastmodunix'] ) : false; $struc['type'] = $this->is_dir( $path . $entry ) ? 'd' : 'f'; if ( 'd' === $struc['type'] ) { diff --git a/src/wp-admin/includes/class-wp-filesystem-ftpext.php b/src/wp-admin/includes/class-wp-filesystem-ftpext.php index 0ab4bc17c32a2..d98f1903d86e8 100644 --- a/src/wp-admin/includes/class-wp-filesystem-ftpext.php +++ b/src/wp-admin/includes/class-wp-filesystem-ftpext.php @@ -12,6 +12,14 @@ * @since 2.5.0 * * @see WP_Filesystem_Base + * @phpstan-type Options array{ + * hostname: non-empty-string, + * username: non-empty-string, + * password: string, + * port: non-negative-int, + * ssl: bool, + * } + * @phpstan-import-type FileListing from WP_Filesystem_Base */ class WP_Filesystem_FTPext extends WP_Filesystem_Base { @@ -21,14 +29,36 @@ class WP_Filesystem_FTPext extends WP_Filesystem_Base { */ public $link; + /** + * @since 7.1.0 + * @var array + * @phpstan-var Options + */ + public $options; + /** * Constructor. * * @since 2.5.0 * - * @param array $opt + * @param array $opt { + * Array of connection options. + * + * @type string $hostname Required. FTP server hostname. + * @type string $username Required. FTP username. + * @type string $password Required. FTP password. + * @type int $port Optional. FTP server port. Default 21. + * @type string $connection_type Optional. Connection type. Use 'ftps' to enable SSL. + * } + * @phpstan-param array{ + * hostname: non-empty-string, + * username: non-empty-string, + * password: string, + * port?: non-negative-int, + * connection_type?: 'ftps', + * }|null $opt */ - public function __construct( $opt = '' ) { + public function __construct( $opt = null ) { $this->method = 'ftpext'; $this->errors = new WP_Error(); @@ -43,35 +73,45 @@ public function __construct( $opt = '' ) { define( 'FS_TIMEOUT', 4 * MINUTE_IN_SECONDS ); } + $options = array(); + if ( ! is_array( $opt ) ) { + $opt = array(); + } + if ( empty( $opt['port'] ) ) { - $this->options['port'] = 21; + $options['port'] = 21; } else { - $this->options['port'] = $opt['port']; + $options['port'] = $opt['port']; } if ( empty( $opt['hostname'] ) ) { $this->errors->add( 'empty_hostname', __( 'FTP hostname is required' ) ); } else { - $this->options['hostname'] = $opt['hostname']; + $options['hostname'] = $opt['hostname']; } // Check if the options provided are OK. if ( empty( $opt['username'] ) ) { $this->errors->add( 'empty_username', __( 'FTP username is required' ) ); } else { - $this->options['username'] = $opt['username']; + $options['username'] = $opt['username']; } if ( empty( $opt['password'] ) ) { $this->errors->add( 'empty_password', __( 'FTP password is required' ) ); } else { - $this->options['password'] = $opt['password']; + $options['password'] = $opt['password']; } - $this->options['ssl'] = false; + $options['ssl'] = false; if ( isset( $opt['connection_type'] ) && 'ftps' === $opt['connection_type'] ) { - $this->options['ssl'] = true; + $options['ssl'] = true; + } + + if ( ! $this->errors->has_errors() ) { + /** @var Options $options */ + $this->options = $options; } } @@ -83,6 +123,10 @@ public function __construct( $opt = '' ) { * @return bool True on success, false on failure. */ public function connect() { + if ( $this->errors->has_errors() ) { + return false; + } + if ( isset( $this->options['ssl'] ) && $this->options['ssl'] && function_exists( 'ftp_ssl_connect' ) ) { $this->link = @ftp_ssl_connect( $this->options['hostname'], $this->options['port'], FS_CONNECT_TIMEOUT ); } else { @@ -135,6 +179,10 @@ public function connect() { * or if the file couldn't be retrieved. */ public function get_contents( $file ) { + if ( ! $this->link ) { + return false; + } + $tempfile = wp_tempnam( $file ); $temphandle = fopen( $tempfile, 'w+' ); @@ -168,10 +216,14 @@ public function get_contents( $file ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return array|false File contents in an array on success, false on failure. + * @return string[]|false File contents in an array on success, false on failure. */ public function get_contents_array( $file ) { - return explode( "\n", $this->get_contents( $file ) ); + $contents = $this->get_contents( $file ); + if ( is_string( $contents ) ) { + return explode( "\n", $contents ); + } + return false; } /** @@ -294,7 +346,7 @@ public function chmod( $file, $mode = false, $recursive = false ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return string|false Username of the owner on success, false on failure. + * @return string|int<1, max>|false Username of the owner on success, false on failure. */ public function owner( $file ) { $dir = $this->dirlist( $file ); @@ -322,7 +374,7 @@ public function getchmod( $file ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return string|false The group on success, false on failure. + * @return string|int<1, max>|false The group on success, false on failure. */ public function group( $file ) { $dir = $this->dirlist( $file ); @@ -465,9 +517,17 @@ public function is_file( $file ) { * @return bool Whether $path is a directory. */ public function is_dir( $path ) { - $cwd = $this->cwd(); + $cwd = $this->cwd(); + if ( false === $cwd ) { + return false; + } + $result = @ftp_chdir( $this->link, trailingslashit( $path ) ); + if ( ! $this->link ) { + return false; + } + if ( $result && $path === $this->cwd() || $this->cwd() !== $cwd ) { @ftp_chdir( $this->link, $cwd ); return true; @@ -622,6 +682,7 @@ public function rmdir( $path, $recursive = false ) { * @type array|false $files If a directory and `$recursive` is true, contains another array of files. * False if unable to list directory contents. * } + * @phpstan-return FileListing|'' */ public function parselisting( $line ) { static $is_windows = null; @@ -653,7 +714,7 @@ public function parselisting( $line ) { $b['year'] = $lucifer[3]; $b['hour'] = $lucifer[4]; $b['minute'] = $lucifer[5]; - $b['time'] = mktime( $lucifer[4] + ( strcasecmp( $lucifer[6], 'PM' ) === 0 ? 12 : 0 ), $lucifer[5], 0, $lucifer[1], $lucifer[2], $lucifer[3] ); + $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] ); $b['am/pm'] = $lucifer[6]; $b['name'] = $lucifer[8]; } elseif ( ! $is_windows ) { @@ -689,7 +750,7 @@ public function parselisting( $line ) { sscanf( $lucifer[5], '%d-%d-%d', $b['year'], $b['month'], $b['day'] ); sscanf( $lucifer[6], '%d:%d', $b['hour'], $b['minute'] ); - $b['time'] = mktime( $b['hour'], $b['minute'], 0, $b['month'], $b['day'], $b['year'] ); + $b['time'] = mktime( (int) $b['hour'], (int) $b['minute'], 0, (int) $b['month'], (int) $b['day'], (int) $b['year'] ); $b['name'] = $lucifer[7]; } else { $b['month'] = $lucifer[5]; @@ -713,10 +774,10 @@ public function parselisting( $line ) { // Replace symlinks formatted as "source -> target" with just the source name. if ( isset( $b['islink'] ) && $b['islink'] ) { - $b['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $b['name'] ); + $b['name'] = (string) preg_replace( '/(\s*->\s*.*)$/', '', $b['name'] ); } - return $b; + return $b ?? ''; } /** @@ -747,14 +808,19 @@ public function parselisting( $line ) { * False if not available. * @type string|false $lastmod Last modified month (3 letters) and day (without leading 0), or * false if not available. - * @type string|false $time Last modified time, or false if not available. + * @type int|string|false $time Last modified time as a Unix timestamp, or false if not available. * @type string $type Type of resource. 'f' for file, 'd' for directory, 'l' for link. * @type array|false $files If a directory and `$recursive` is true, contains another array of * files. False if unable to list directory contents. * } * } + * @phpstan-return array|false */ public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) { + if ( ! $this->link ) { + return false; + } + if ( $this->is_file( $path ) ) { $limit_file = basename( $path ); $path = dirname( $path ) . '/'; @@ -763,11 +829,15 @@ public function dirlist( $path = '.', $include_hidden = true, $recursive = false } $pwd = ftp_pwd( $this->link ); + if ( ! is_string( $pwd ) ) { + return false; + } if ( ! @ftp_chdir( $this->link, $path ) ) { // Can't change to folder = folder doesn't exist. return false; } + /** @var string[]|false $list */ $list = ftp_rawlist( $this->link, '-a', false ); @ftp_chdir( $this->link, $pwd ); diff --git a/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php b/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php index cc665ad9bf7b4..260c3fade15d5 100644 --- a/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php +++ b/src/wp-admin/includes/class-wp-filesystem-ftpsockets.php @@ -12,6 +12,13 @@ * @since 2.5.0 * * @see WP_Filesystem_Base + * @phpstan-type Options array{ + * hostname: non-empty-string, + * username: non-empty-string, + * password: string, + * port: non-negative-int, + * } + * @phpstan-import-type FileListing from WP_Filesystem_Base */ class WP_Filesystem_ftpsockets extends WP_Filesystem_Base { @@ -21,14 +28,34 @@ class WP_Filesystem_ftpsockets extends WP_Filesystem_Base { */ public $ftp; + /** + * @since 7.1.0 + * @var array + * @phpstan-var Options + */ + public $options; + /** * Constructor. * * @since 2.5.0 * - * @param array $opt + * @param array $opt { + * Array of connection options. + * + * @type string $hostname Required. FTP server hostname. + * @type string $username Required. FTP username. + * @type string $password Required. FTP password. + * @type int $port Optional. FTP server port. Default 21. + * } + * @phpstan-param array{ + * hostname: non-empty-string, + * username: non-empty-string, + * password: string, + * port?: non-negative-int, + * }|null $opt */ - public function __construct( $opt = '' ) { + public function __construct( $opt = null ) { $this->method = 'ftpsockets'; $this->errors = new WP_Error(); @@ -39,29 +66,39 @@ public function __construct( $opt = '' ) { $this->ftp = new ftp(); + $options = array(); + if ( ! is_array( $opt ) ) { + $opt = array(); + } + if ( empty( $opt['port'] ) ) { - $this->options['port'] = 21; + $options['port'] = 21; } else { - $this->options['port'] = (int) $opt['port']; + $options['port'] = (int) $opt['port']; } if ( empty( $opt['hostname'] ) ) { $this->errors->add( 'empty_hostname', __( 'FTP hostname is required' ) ); } else { - $this->options['hostname'] = $opt['hostname']; + $options['hostname'] = $opt['hostname']; } // Check if the options provided are OK. if ( empty( $opt['username'] ) ) { $this->errors->add( 'empty_username', __( 'FTP username is required' ) ); } else { - $this->options['username'] = $opt['username']; + $options['username'] = $opt['username']; } if ( empty( $opt['password'] ) ) { $this->errors->add( 'empty_password', __( 'FTP password is required' ) ); } else { - $this->options['password'] = $opt['password']; + $options['password'] = $opt['password']; + } + + if ( ! $this->errors->has_errors() ) { + /** @var Options $options */ + $this->options = $options; } } @@ -73,6 +110,10 @@ public function __construct( $opt = '' ) { * @return bool True on success, false on failure. */ public function connect() { + if ( $this->errors->has_errors() ) { + return false; + } + if ( ! $this->ftp ) { return false; } @@ -179,10 +220,14 @@ public function get_contents( $file ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return array|false File contents in an array on success, false on failure. + * @return string[]|false File contents in an array on success, false on failure. */ public function get_contents_array( $file ) { - return explode( "\n", $this->get_contents( $file ) ); + $contents = $this->get_contents( $file ); + if ( is_string( $contents ) ) { + return explode( "\n", $contents ); + } + return false; } /** @@ -221,7 +266,7 @@ public function put_contents( $file, $contents, $mode = false ) { fseek( $temphandle, 0 ); // Skip back to the start of the file being written to. - $ret = $this->ftp->fput( $file, $temphandle ); + $ret = (bool) $this->ftp->fput( $file, $temphandle ); reset_mbstring_encoding(); @@ -242,6 +287,9 @@ public function put_contents( $file, $contents, $mode = false ) { */ public function cwd() { $cwd = $this->ftp->pwd(); + if ( ! is_string( $cwd ) ) { + return false; + } if ( $cwd ) { $cwd = trailingslashit( $cwd ); @@ -259,7 +307,7 @@ public function cwd() { * @return bool True on success, false on failure. */ public function chdir( $dir ) { - return $this->ftp->chdir( $dir ); + return (bool) $this->ftp->chdir( $dir ); } /** @@ -295,7 +343,7 @@ public function chmod( $file, $mode = false, $recursive = false ) { } // chmod the file or directory. - return $this->ftp->chmod( $file, $mode ); + return (bool) $this->ftp->chmod( $file, $mode ); } /** @@ -304,7 +352,7 @@ public function chmod( $file, $mode = false, $recursive = false ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return string|false Username of the owner on success, false on failure. + * @return string|int<1, max>|false Username of the owner on success, false on failure. */ public function owner( $file ) { $dir = $this->dirlist( $file ); @@ -332,7 +380,7 @@ public function getchmod( $file ) { * @since 2.5.0 * * @param string $file Path to the file. - * @return string|false The group on success, false on failure. + * @return string|int<1, max>|false The group on success, false on failure. */ public function group( $file ) { $dir = $this->dirlist( $file ); @@ -386,7 +434,7 @@ public function copy( $source, $destination, $overwrite = false, $mode = false ) * @return bool True on success, false on failure. */ public function move( $source, $destination, $overwrite = false ) { - return $this->ftp->rename( $source, $destination ); + return (bool) $this->ftp->rename( $source, $destination ); } /** @@ -407,14 +455,14 @@ public function delete( $file, $recursive = false, $type = false ) { } if ( 'f' === $type || $this->is_file( $file ) ) { - return $this->ftp->delete( $file ); + return (bool) $this->ftp->delete( $file ); } if ( ! $recursive ) { - return $this->ftp->rmdir( $file ); + return (bool) $this->ftp->rmdir( $file ); } - return $this->ftp->mdel( $file ); + return (bool) $this->ftp->mdel( $file ); } /** @@ -477,6 +525,9 @@ public function is_file( $file ) { */ public function is_dir( $path ) { $cwd = $this->cwd(); + if ( ! $cwd ) { + return false; + } if ( $this->chdir( $path ) ) { $this->chdir( $cwd ); @@ -531,7 +582,11 @@ public function atime( $file ) { * @return int|false Unix timestamp representing modification time, false on failure. */ public function mtime( $file ) { - return $this->ftp->mdtm( $file ); + $modified_time = $this->ftp->mdtm( $file ); + if ( false === $modified_time ) { + return false; + } + return (int) $modified_time; } /** @@ -543,7 +598,11 @@ public function mtime( $file ) { * @return int|false Size of the file in bytes on success, false on failure. */ public function size( $file ) { - return $this->ftp->filesize( $file ); + $size = $this->ftp->filesize( $file ); + if ( false === $size ) { + return false; + } + return (int) $size; } /** @@ -640,12 +699,13 @@ public function rmdir( $path, $recursive = false ) { * False if not available. * @type string|false $lastmod Last modified month (3 letters) and day (without leading 0), or * false if not available. - * @type string|false $time Last modified time, or false if not available. + * @type int|string|false $time Last modified time as a Unix timestamp, or false if not available. * @type string $type Type of resource. 'f' for file, 'd' for directory, 'l' for link. * @type array|false $files If a directory and `$recursive` is true, contains another array of * files. False if unable to list directory contents. * } * } + * @phpstan-return array|false */ public function dirlist( $path = '.', $include_hidden = true, $recursive = false ) { if ( $this->is_file( $path ) ) { @@ -657,9 +717,10 @@ public function dirlist( $path = '.', $include_hidden = true, $recursive = false mbstring_binary_safe_encoding(); + /** @var array|false $list */ $list = $this->ftp->dirlist( $path ); - if ( empty( $list ) && ! $this->exists( $path ) ) { + if ( ! is_array( $list ) || ( empty( $list ) && ! $this->exists( $path ) ) ) { reset_mbstring_encoding(); @@ -692,12 +753,14 @@ public function dirlist( $path = '.', $include_hidden = true, $recursive = false } // Replace symlinks formatted as "source -> target" with just the source name. - if ( $struc['islink'] ) { - $struc['name'] = preg_replace( '/(\s*->\s*.*)$/', '', $struc['name'] ); + if ( $struc['islink'] ?? false ) { + $struc['name'] = (string) preg_replace( '/(\s*->\s*.*)$/', '', $struc['name'] ); } // Add the octal representation of the file permissions. - $struc['permsn'] = $this->getnumchmodfromh( $struc['perms'] ); + if ( isset( $struc['perms'] ) ) { + $struc['permsn'] = $this->getnumchmodfromh( $struc['perms'] ); + } $ret[ $struc['name'] ] = $struc; } diff --git a/src/wp-admin/includes/class-wp-filesystem-ssh2.php b/src/wp-admin/includes/class-wp-filesystem-ssh2.php index 9146045025942..c01ab6f465d5b 100644 --- a/src/wp-admin/includes/class-wp-filesystem-ssh2.php +++ b/src/wp-admin/includes/class-wp-filesystem-ssh2.php @@ -32,18 +32,29 @@ * * @package WordPress * @subpackage Filesystem + * + * @phpstan-type Options array{ + * hostname: non-empty-string, + * username: non-empty-string, + * password: string|null, + * port: non-negative-int, + * public_key?: non-empty-string, + * private_key?: non-empty-string, + * hostkey?: array{ hostkey: non-empty-string }, + * } + * @phpstan-import-type FileListing from WP_Filesystem_Base */ class WP_Filesystem_SSH2 extends WP_Filesystem_Base { /** * @since 2.7.0 - * @var resource + * @var resource|false */ public $link = false; /** * @since 2.7.0 - * @var resource + * @var resource|false */ public $sftp_link; @@ -53,14 +64,38 @@ class WP_Filesystem_SSH2 extends WP_Filesystem_Base { */ public $keys = false; + /** + * @since 7.1.0 + * @var array + * @phpstan-var Options + */ + public $options; + /** * Constructor. * * @since 2.7.0 * - * @param array $opt - */ - public function __construct( $opt = '' ) { + * @param array $opt { + * Array of connection options. + * + * @type string $hostname Required. SSH server hostname. + * @type string $username Required. SSH username. + * @type int $port Optional. SSH server port. Default 22. + * @type string $password Optional. SSH password. May be empty when using keys. + * @type string $public_key Optional. Path to public key file for publickey authentication. + * @type string $private_key Optional. Path to private key file for publickey authentication. + * } + * @phpstan-param array{ + * hostname: non-empty-string, + * username: non-empty-string, + * port?: non-negative-int, + * password?: string, + * public_key?: non-empty-string, + * private_key?: non-empty-string, + * }|null $opt + */ + public function __construct( $opt = null ) { $this->method = 'ssh2'; $this->errors = new WP_Error(); @@ -70,33 +105,39 @@ public function __construct( $opt = '' ) { return; } + $options = array(); + if ( ! is_array( $opt ) ) { + $opt = array(); + } + // Set defaults: if ( empty( $opt['port'] ) ) { - $this->options['port'] = 22; + $options['port'] = 22; } else { - $this->options['port'] = $opt['port']; + $options['port'] = $opt['port']; } if ( empty( $opt['hostname'] ) ) { $this->errors->add( 'empty_hostname', __( 'SSH2 hostname is required' ) ); } else { - $this->options['hostname'] = $opt['hostname']; + $options['hostname'] = $opt['hostname']; } // Check if the options provided are OK. if ( ! empty( $opt['public_key'] ) && ! empty( $opt['private_key'] ) ) { - $this->options['public_key'] = $opt['public_key']; - $this->options['private_key'] = $opt['private_key']; + $options['public_key'] = $opt['public_key']; + $options['private_key'] = $opt['private_key']; - $this->options['hostkey'] = array( 'hostkey' => 'ssh-rsa,ssh-ed25519' ); + $options['hostkey'] = array( 'hostkey' => 'ssh-rsa,ssh-ed25519' ); $this->keys = true; - } elseif ( empty( $opt['username'] ) ) { - $this->errors->add( 'empty_username', __( 'SSH2 username is required' ) ); } - if ( ! empty( $opt['username'] ) ) { - $this->options['username'] = $opt['username']; + // A username is always required, whether authenticating with a password or with keys. + if ( empty( $opt['username'] ) ) { + $this->errors->add( 'empty_username', __( 'SSH2 username is required' ) ); + } else { + $options['username'] = $opt['username']; } if ( empty( $opt['password'] ) ) { @@ -104,10 +145,15 @@ public function __construct( $opt = '' ) { if ( ! $this->keys ) { $this->errors->add( 'empty_password', __( 'SSH2 password is required' ) ); } else { - $this->options['password'] = null; + $options['password'] = null; } } else { - $this->options['password'] = $opt['password']; + $options['password'] = $opt['password']; + } + + if ( ! $this->errors->has_errors() ) { + /** @var Options $options */ + $this->options = $options; } } @@ -119,7 +165,11 @@ public function __construct( $opt = '' ) { * @return bool True on success, false on failure. */ public function connect() { - if ( ! $this->keys ) { + if ( $this->errors->has_errors() ) { + return false; + } + + if ( ! isset( $this->options['hostkey'] ) ) { $this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'] ); } else { $this->link = @ssh2_connect( $this->options['hostname'], $this->options['port'], $this->options['hostkey'] ); @@ -139,7 +189,7 @@ public function connect() { } if ( ! $this->keys ) { - if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ) ) { + if ( ! @ssh2_auth_password( $this->link, $this->options['username'], $this->options['password'] ?? '' ) ) { $this->errors->add( 'auth', sprintf( @@ -152,7 +202,7 @@ public function connect() { return false; } } else { - if ( ! @ssh2_auth_pubkey_file( $this->link, $this->options['username'], $this->options['public_key'], $this->options['private_key'], $this->options['password'] ) ) { + if ( ! @ssh2_auth_pubkey_file( $this->link, $this->options['username'], $this->options['public_key'] ?? '', $this->options['private_key'] ?? '', $this->options['password'] ?? '' ) ) { $this->errors->add( 'auth', sprintf( @@ -212,6 +262,8 @@ public function sftp_path( $path ) { * @param bool $returnbool * @return bool|string True on success, false on failure. String if the command was executed, `$returnbool` * is false (default), and data from the resulting stream was retrieved. + * + * @phpstan-return ( $returnbool is true ? bool : string ) */ public function run_command( $command, $returnbool = false ) { if ( ! $this->link ) { @@ -264,7 +316,7 @@ public function get_contents( $file ) { * @since 2.7.0 * * @param string $file Path to the file. - * @return array|false File contents in an array on success, false on failure. + * @return string[]|false File contents in an array on success, false on failure. */ public function get_contents_array( $file ) { return file( $this->sftp_path( $file ) ); @@ -298,9 +350,14 @@ public function put_contents( $file, $contents, $mode = false ) { * * @since 2.7.0 * - * @return string|false The current working directory on success, false on failure. + * @return string The current working directory. */ public function cwd() { + if ( ! $this->sftp_link ) { + return ''; + } + + /** @var string $cwd */ $cwd = ssh2_sftp_realpath( $this->sftp_link, '.' ); if ( $cwd ) { @@ -339,10 +396,10 @@ public function chgrp( $file, $group, $recursive = false ) { } if ( ! $recursive || ! $this->is_dir( $file ) ) { - return $this->run_command( sprintf( 'chgrp %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true ); + return $this->run_command( sprintf( 'chgrp %s %s', escapeshellarg( (string) $group ), escapeshellarg( $file ) ), true ); } - return $this->run_command( sprintf( 'chgrp -R %s %s', escapeshellarg( $group ), escapeshellarg( $file ) ), true ); + return $this->run_command( sprintf( 'chgrp -R %s %s', escapeshellarg( (string) $group ), escapeshellarg( $file ) ), true ); } /** @@ -396,10 +453,10 @@ public function chown( $file, $owner, $recursive = false ) { } if ( ! $recursive || ! $this->is_dir( $file ) ) { - return $this->run_command( sprintf( 'chown %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true ); + return $this->run_command( sprintf( 'chown %s %s', escapeshellarg( (string) $owner ), escapeshellarg( $file ) ), true ); } - return $this->run_command( sprintf( 'chown -R %s %s', escapeshellarg( $owner ), escapeshellarg( $file ) ), true ); + return $this->run_command( sprintf( 'chown -R %s %s', escapeshellarg( (string) $owner ), escapeshellarg( $file ) ), true ); } /** @@ -408,7 +465,7 @@ public function chown( $file, $owner, $recursive = false ) { * @since 2.7.0 * * @param string $file Path to the file. - * @return string|false Username of the owner on success, false on failure. + * @return string|int<1, max>|false Username of the owner on success, or UID of file owner if not available; false on failure. */ public function owner( $file ) { $owneruid = @fileowner( $this->sftp_path( $file ) ); @@ -436,10 +493,14 @@ public function owner( $file ) { * @since 2.7.0 * * @param string $file Path to the file. - * @return string Mode of the file (the last 3 digits). + * @return string Mode of the file (the last 3 digits). Empty string on failure. */ public function getchmod( $file ) { - return substr( decoct( @fileperms( $this->sftp_path( $file ) ) ), -3 ); + $file_perms = @fileperms( $this->sftp_path( $file ) ); + if ( ! is_int( $file_perms ) ) { + return ''; + } + return substr( decoct( $file_perms ), -3 ); } /** @@ -448,7 +509,7 @@ public function getchmod( $file ) { * @since 2.7.0 * * @param string $file Path to the file. - * @return string|false The group on success, false on failure. + * @return string|int<1, max>|false Username of the owner on success, or group ID of file owner if not available; false on failure. */ public function group( $file ) { $gid = @filegroup( $this->sftp_path( $file ) ); @@ -526,6 +587,9 @@ public function move( $source, $destination, $overwrite = false ) { } } + if ( ! $this->sftp_link ) { + return false; + } return ssh2_sftp_rename( $this->sftp_link, $source, $destination ); } @@ -542,6 +606,9 @@ public function move( $source, $destination, $overwrite = false ) { * @return bool True on success, false on failure. */ public function delete( $file, $recursive = false, $type = false ) { + if ( ! $this->sftp_link ) { + return false; + } if ( 'f' === $type || $this->is_file( $file ) ) { return ssh2_sftp_unlink( $this->sftp_link, $file ); } @@ -692,6 +759,9 @@ public function touch( $file, $time = 0, $atime = 0 ) { * @return bool True on success, false on failure. */ public function mkdir( $path, $chmod = false, $chown = false, $chgrp = false ) { + if ( ! $this->sftp_link ) { + return false; + } $path = untrailingslashit( $path ); if ( empty( $path ) ) { @@ -768,6 +838,7 @@ public function rmdir( $path, $recursive = false ) { * files. False if unable to list directory contents. * } * } + * @phpstan-return array|false */ public function dirlist( $path, $include_hidden = true, $recursive = false ) { if ( $this->is_file( $path ) ) { @@ -813,8 +884,8 @@ public function dirlist( $path, $include_hidden = true, $recursive = false ) { $struc['group'] = $this->group( $path . $entry ); $struc['size'] = $this->size( $path . $entry ); $struc['lastmodunix'] = $this->mtime( $path . $entry ); - $struc['lastmod'] = gmdate( 'M j', $struc['lastmodunix'] ); - $struc['time'] = gmdate( 'h:i:s', $struc['lastmodunix'] ); + $struc['lastmod'] = is_int( $struc['lastmodunix'] ) ? gmdate( 'M j', $struc['lastmodunix'] ) : false; + $struc['time'] = is_int( $struc['lastmodunix'] ) ? gmdate( 'h:i:s', $struc['lastmodunix'] ) : false; $struc['type'] = $this->is_dir( $path . $entry ) ? 'd' : 'f'; if ( 'd' === $struc['type'] ) {