diff --git a/features/plugin-list-wporg-status.feature b/features/plugin-list-wporg-status.feature index 1da8360f..ff21ea03 100644 --- a/features/plugin-list-wporg-status.feature +++ b/features/plugin-list-wporg-status.feature @@ -111,6 +111,72 @@ Feature: Check the status of plugins on WordPress.org | no-longer-in-directory | closed | 2017-11-13 | | never-wporg | | | + @require-wp-5.2 + Scenario: The wp.org last updated date for an active plugin does not depend on a second, rate-limited request + Given a WP install + And I run `wp plugin install wordpress-importer --version=0.5 --force` + And that HTTP requests to https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request%5Blocale%5D=en_US&request%5Bslug%5D=wordpress-importer will respond with: + """ + HTTP/1.1 200 + Content-Type: application/json + + { + "name": "WordPress Importer", + "slug": "wordpress-importer", + "last_updated": "2025-09-26 9:07pm GMT" + } + """ + # plugins.trac.wordpress.org is known to rate-limit this scrape (HTTP 429), with no + # pubDate in the response body. wporg_last_updated must still resolve correctly for an + # active plugin, because the date is meant to come from the plugin-info API response + # above, not from a second request to trac. + And that HTTP requests to https://plugins.trac.wordpress.org/log/wordpress-importer/?limit=1&mode=stop_on_copy&format=rss will respond with: + """ + HTTP/1.1 429 + Content-Type: text/html + + 429 Too Many Requests + """ + + When I run `wp plugin list --fields=name,wporg_status,wporg_last_updated` + Then STDOUT should be a table containing rows: + | name | wporg_status | wporg_last_updated | + | wordpress-importer | active | 2025-09-26 | + + @require-wp-5.2 + Scenario: The wp.org last updated date falls back to the trac log when the plugin-info API omits it + Given a WP install + And I run `wp plugin install wordpress-importer --version=0.5 --force` + And that HTTP requests to https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request%5Blocale%5D=en_US&request%5Bslug%5D=wordpress-importer will respond with: + """ + HTTP/1.1 200 + Content-Type: application/json + + { + "name": "WordPress Importer", + "slug": "wordpress-importer" + } + """ + And that HTTP requests to https://plugins.trac.wordpress.org/log/wordpress-importer/?limit=1&mode=stop_on_copy&format=rss will respond with: + """ + HTTP/1.1 200 + Content-Type: application/rss+xml;charset=utf-8 + + + + + + Fri, 26 Sep 2025 21:07:26 GMT + + + + """ + + When I run `wp plugin list --fields=name,wporg_status,wporg_last_updated` + Then STDOUT should be a table containing rows: + | name | wporg_status | wporg_last_updated | + | wordpress-importer | active | 2025-09-26 | + @less-than-wp-5.3 Scenario: The wp.org last updated date is still rendered on WordPress < 5.3 Given a WP install diff --git a/src/Plugin_Command.php b/src/Plugin_Command.php index 00505984..c2e3877f 100644 --- a/src/Plugin_Command.php +++ b/src/Plugin_Command.php @@ -1128,6 +1128,17 @@ protected function get_wporg_data( $plugin_name ) { if ( ! $this->check_wporg['last_updated'] ) { return $data; // The plugin is active on .org, but we don't need the date. } + // The plugins API already reports when the plugin was last updated, so use + // that instead of also scraping the trac log, which is rate-limited and + // otherwise unnecessary here. If the value can't be parsed, fall through + // to the trac log below rather than defaulting to today's date. + if ( ! empty( $plugin_data['last_updated'] ) && is_string( $plugin_data['last_updated'] ) ) { + $pub_date = strtotime( $plugin_data['last_updated'] ); + if ( false !== $pub_date ) { + $data['last_updated'] = $this->format_wporg_last_updated( $pub_date ); + return $data; + } + } } // Just because the plugin is not in the api, does not mean it was never on .org. } @@ -1152,18 +1163,9 @@ protected function get_wporg_data( $plugin_name ) { if ( false !== $xml ) { $xml_pub_date = $xml->xpath( '//pubDate' ); if ( $xml_pub_date ) { - $pub_date = strtotime( $xml_pub_date[0] ) ?: null; - - if ( function_exists( 'wp_date' ) ) { - $data['last_updated'] = wp_date( 'Y-m-d', $pub_date ); - } else { - // wp_date() is WordPress 5.3+. get_date_from_gmt() renders in the site - // timezone the same way, without date_i18n()'s pre-5.3 contract of - // expecting a timestamp that already has the offset added to it. - $data['last_updated'] = get_date_from_gmt( - gmdate( 'Y-m-d H:i:s', $pub_date ?? time() ), - 'Y-m-d' - ); + $pub_date = strtotime( $xml_pub_date[0] ); + if ( false !== $pub_date ) { + $data['last_updated'] = $this->format_wporg_last_updated( $pub_date ); } } } @@ -1172,6 +1174,27 @@ protected function get_wporg_data( $plugin_name ) { return $data; } + /** + * Formats a wp.org publish date as a `Y-m-d` string in the site's configured timezone. + * + * @param int $pub_date Unix timestamp. + * + * @return string|false + */ + private function format_wporg_last_updated( $pub_date ) { + if ( function_exists( 'wp_date' ) ) { + return wp_date( 'Y-m-d', $pub_date ); + } + + // wp_date() is WordPress 5.3+. get_date_from_gmt() renders in the site + // timezone the same way, without date_i18n()'s pre-5.3 contract of + // expecting a timestamp that already has the offset added to it. + return get_date_from_gmt( + gmdate( 'Y-m-d H:i:s', $pub_date ), + 'Y-m-d' + ); + } + protected function filter_item_list( $items, $args ) { $basenames = wp_list_pluck( $this->fetcher->get_many( $args ), 'file' ); return Utils\pick_fields( $items, $basenames );