2828 * 4.5.2
2929 *
3030 * @package wp-cli
31+ *
32+ * @phpstan-type HTTP_Response array{headers: array<string, string>, body: string, response: array{code:false|int, message: false|string}, cookies: array<string, string>, http_response: mixed}
3133 */
3234class Core_Command extends WP_CLI_Command {
3335
36+ /**
37+ * Stores HTTP API errors encountered during version check.
38+ *
39+ * @var \WP_Error|null
40+ */
41+ private $ version_check_error = null ;
42+
3443 /**
3544 * Checks for WordPress updates via Version Check API.
3645 *
@@ -92,6 +101,14 @@ public function check_update( $args, $assoc_args ) {
92101 [ 'version ' , 'update_type ' , 'package_url ' ]
93102 );
94103 $ formatter ->display_items ( $ updates );
104+ } elseif ( $ this ->version_check_error ) {
105+ // If there was an HTTP error during version check, show a warning
106+ WP_CLI ::warning (
107+ sprintf (
108+ 'Failed to check for updates: %s ' ,
109+ $ this ->version_check_error ->get_error_message ()
110+ )
111+ );
95112 } else {
96113 WP_CLI ::success ( 'WordPress is at the latest version. ' );
97114 }
@@ -644,6 +661,10 @@ private static function set_multisite_defaults( $assoc_args ) {
644661 }
645662
646663 private function do_install ( $ assoc_args ) {
664+ /**
665+ * @var \wpdb $wpdb
666+ */
667+ global $ wpdb ;
647668 if ( is_blog_installed () ) {
648669 return false ;
649670 }
@@ -695,7 +716,7 @@ function wp_new_blog_notification() {
695716 $ args ['locale ' ]
696717 );
697718
698- if ( ! empty ( $ GLOBALS [ ' wpdb ' ] ->last_error ) ) {
719+ if ( ! empty ( $ wpdb ->last_error ) ) {
699720 WP_CLI ::error ( 'Installation produced database errors, and may have partially or completely failed. ' );
700721 }
701722
@@ -1283,8 +1304,14 @@ static function () {
12831304
12841305 WP_CLI ::success ( 'WordPress updated successfully. ' );
12851306 }
1307+ // Check if user attempted to downgrade without --force
1308+ } elseif ( ! empty ( Utils \get_flag_value ( $ assoc_args , 'version ' ) ) && version_compare ( Utils \get_flag_value ( $ assoc_args , 'version ' ), $ wp_version , '< ' ) ) {
1309+ // Requested version is older than current (downgrade attempt)
1310+ WP_CLI ::log ( "WordPress is up to date at version {$ wp_version }. " );
1311+ WP_CLI ::log ( 'The version you requested ( ' . Utils \get_flag_value ( $ assoc_args , 'version ' ) . ") is older than the current version ( {$ wp_version }). " );
1312+ WP_CLI ::log ( 'Use --force to update anyway (e.g., to downgrade to version ' . Utils \get_flag_value ( $ assoc_args , 'version ' ) . '). ' );
12861313 } else {
1287- WP_CLI ::success ( ' WordPress is up to date. ' );
1314+ WP_CLI ::success ( " WordPress is up to date at version { $ wp_version } . " );
12881315 }
12891316 }
12901317
@@ -1555,7 +1582,25 @@ private function get_download_url( $version, $locale = 'en_US', $file_type = 'zi
15551582 */
15561583 private function get_updates ( $ assoc_args ) {
15571584 $ force_check = Utils \get_flag_value ( $ assoc_args , 'force-check ' );
1558- wp_version_check ( [], $ force_check );
1585+
1586+ // Reset error tracking
1587+ $ this ->version_check_error = null ;
1588+
1589+ // Hook into pre_http_request with max priority to capture errors during version check
1590+ // This is necessary because tests and plugins may use pre_http_request to mock responses
1591+ add_filter ( 'pre_http_request ' , [ $ this , 'capture_version_check_error ' ], PHP_INT_MAX , 3 );
1592+
1593+ // Also hook into http_api_debug to capture errors from real HTTP requests
1594+ // This fires when pre_http_request doesn't short-circuit the request
1595+ add_action ( 'http_api_debug ' , [ $ this , 'capture_version_check_error_from_response ' ], 10 , 5 );
1596+
1597+ try {
1598+ wp_version_check ( [], $ force_check );
1599+ } finally {
1600+ // Ensure the hooks are always removed, even if wp_version_check() throws an exception
1601+ remove_filter ( 'pre_http_request ' , [ $ this , 'capture_version_check_error ' ], PHP_INT_MAX );
1602+ remove_action ( 'http_api_debug ' , [ $ this , 'capture_version_check_error_from_response ' ], 10 );
1603+ }
15591604
15601605 /**
15611606 * @var object{updates: array<object{version: string, locale: string, packages: object{partial?: string, full: string}}>}|false $from_api
@@ -1565,6 +1610,10 @@ private function get_updates( $assoc_args ) {
15651610 return [];
15661611 }
15671612
1613+ /**
1614+ * @var array{wp_version: string} $GLOBALS
1615+ */
1616+
15681617 $ compare_version = str_replace ( '-src ' , '' , $ GLOBALS ['wp_version ' ] );
15691618
15701619 $ updates = [
@@ -1612,6 +1661,87 @@ private function get_updates( $assoc_args ) {
16121661 return array_values ( $ updates );
16131662 }
16141663
1664+ /**
1665+ * Sets or clears the version check error property based on an HTTP response.
1666+ *
1667+ * @param mixed|\WP_Error $response The HTTP response (WP_Error, array, or other).
1668+ *
1669+ * @phpstan-param HTTP_Response|WP_Error $response
1670+ */
1671+ private function set_version_check_error ( $ response ) {
1672+ if ( is_wp_error ( $ response ) ) {
1673+ $ this ->version_check_error = $ response ;
1674+ } elseif ( is_array ( $ response ) && isset ( $ response ['response ' ]['code ' ] ) && $ response ['response ' ]['code ' ] >= 400 ) {
1675+ // HTTP error status code (4xx or 5xx) - convert to WP_Error for consistency
1676+ $ this ->version_check_error = new \WP_Error (
1677+ 'http_request_failed ' ,
1678+ sprintf (
1679+ 'HTTP request failed with status %d: %s ' ,
1680+ $ response ['response ' ]['code ' ],
1681+ isset ( $ response ['response ' ]['message ' ] ) ? $ response ['response ' ]['message ' ] : 'Unknown error '
1682+ )
1683+ );
1684+ } else {
1685+ // Clear any previous error if we got a successful response.
1686+ $ this ->version_check_error = null ;
1687+ }
1688+ }
1689+
1690+ /**
1691+ * Handles the pre_http_request filter to capture HTTP errors during version check.
1692+ *
1693+ * This method signature matches the pre_http_request filter signature.
1694+ * Uses PHP_INT_MAX priority to run after test mocking and plugin modifications.
1695+ *
1696+ * @param false|array|WP_Error $response A preemptive return value of an HTTP request.
1697+ * @param array $args HTTP request arguments.
1698+ * @param string $url The request URL.
1699+ * @return false|array|WP_Error The response, unmodified.
1700+ *
1701+ * @phpstan-param HTTP_Response|WP_Error|false $response
1702+ */
1703+ public function capture_version_check_error ( $ response , $ args , $ url ) {
1704+ if ( false === strpos ( $ url , 'api.wordpress.org/core/version-check ' ) ) {
1705+ return $ response ;
1706+ }
1707+
1708+ // A `false` response means the request is not being preempted.
1709+ // In this case, we don't want to change the error status, as the subsequent
1710+ // `http_api_debug` hook will handle the actual response.
1711+ if ( false !== $ response ) {
1712+ $ this ->set_version_check_error ( $ response );
1713+ }
1714+
1715+ return $ response ;
1716+ }
1717+
1718+ /**
1719+ * Handles the http_api_debug action to capture HTTP errors from real requests.
1720+ *
1721+ * This fires when pre_http_request doesn't short-circuit the request.
1722+ * Uses the http_api_debug action hook signature.
1723+ *
1724+ * @param array|WP_Error $response HTTP response or WP_Error object.
1725+ * @param string $context Context of the HTTP request.
1726+ * @param string $_class HTTP transport class name (unused).
1727+ * @param array $_args HTTP request arguments (unused).
1728+ * @param string $url URL being requested.
1729+ *
1730+ * @phpstan-param HTTP_Response|WP_Error $response
1731+ */
1732+ public function capture_version_check_error_from_response ( $ response , $ context , $ _class , $ _args , $ url ) {
1733+ if ( false === strpos ( $ url , 'api.wordpress.org/core/version-check ' ) ) {
1734+ return ;
1735+ }
1736+
1737+ // Only capture on response, not pre_response
1738+ if ( 'response ' !== $ context ) {
1739+ return ;
1740+ }
1741+
1742+ $ this ->set_version_check_error ( $ response );
1743+ }
1744+
16151745 /**
16161746 * Clean up extra files.
16171747 *
0 commit comments