Skip to content

Commit 1f518df

Browse files
Copilotswissspidy
andauthored
Add --skip-locale-check flag to wp core download for locale fallback (#322)
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler <pascalb@google.com>
1 parent 50719f2 commit 1f518df

File tree

2 files changed

+142
-11
lines changed

2 files changed

+142
-11
lines changed

features/core-download.feature

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,74 @@ Feature: Download WordPress
5151
Then the wp-settings.php file should exist
5252
And the {SUITE_CACHE_DIR}/core/wordpress-{VERSION}-de_DE.tar.gz file should exist
5353

54+
Scenario: Error when requested locale is not available for the latest version
55+
Given an empty directory
56+
And an empty cache
57+
And that HTTP requests to https://api.wordpress.org/core/version-check/1.7/ will respond with:
58+
"""
59+
HTTP/1.1 200 OK
60+
Content-Type: application/json
61+
62+
{"offers":[{"response":"upgrade","download":"https://downloads.wordpress.org/release/wordpress-6.9.4.zip","locale":"en_US","packages":{"full":"https://downloads.wordpress.org/release/wordpress-6.9.4.zip","no_content":"https://downloads.wordpress.org/release/wordpress-6.9.4-no-content.zip","new_bundled":"https://downloads.wordpress.org/release/wordpress-6.9.4-new-bundled.zip","partial":false,"rollback":false},"current":"6.9.4","version":"6.9.4","php_version":"7.2.24","mysql_version":"5.5.5","new_bundled":"6.7","partial_version":false}]}
63+
"""
64+
65+
When I try `wp core download --locale=de_DE`
66+
Then the return code should be 1
67+
And STDERR should contain:
68+
"""
69+
Error: The requested locale (de_DE) was not found.
70+
"""
71+
72+
Scenario: Download older locale version when latest is not yet available using --skip-locale-check
73+
Given an empty directory
74+
And an empty cache
75+
And that HTTP requests to https://api.wordpress.org/core/version-check/1.7/ will respond with:
76+
"""
77+
HTTP/1.1 200 OK
78+
Content-Type: application/json
79+
80+
{"offers":[{"response":"upgrade","download":"https://downloads.wordpress.org/release/wordpress-6.9.4.zip","locale":"en_US","packages":{"full":"https://downloads.wordpress.org/release/wordpress-6.9.4.zip","no_content":"https://downloads.wordpress.org/release/wordpress-6.9.4-no-content.zip","new_bundled":"https://downloads.wordpress.org/release/wordpress-6.9.4-new-bundled.zip","partial":false,"rollback":false},"current":"6.9.4","version":"6.9.4","php_version":"7.2.24","mysql_version":"5.5.5","new_bundled":"6.7","partial_version":false}]}
81+
"""
82+
And that HTTP requests to https://api.wordpress.org/translations/core/1.0/ will respond with:
83+
"""
84+
HTTP/1.1 200 OK
85+
Content-Type: application/json
86+
87+
{"translations":[{"language":"de_DE","version":"4.4.2","updated":"2024-01-01 00:00:00","english_name":"German","native_name":"Deutsch","package":"https://downloads.wordpress.org/translation/core/4.4.2/de_DE.zip"}]}
88+
"""
89+
90+
When I try `wp core download --locale=de_DE --skip-locale-check`
91+
Then the wp-settings.php file should exist
92+
And STDERR should contain:
93+
"""
94+
Warning: The latest WordPress version is not yet available in the de_DE locale. Downloading version 4.4.2 instead.
95+
"""
96+
97+
Scenario: Error when --skip-locale-check is set but no translation exists for locale
98+
Given an empty directory
99+
And an empty cache
100+
And that HTTP requests to https://api.wordpress.org/core/version-check/1.7/ will respond with:
101+
"""
102+
HTTP/1.1 200 OK
103+
Content-Type: application/json
104+
105+
{"offers":[{"response":"upgrade","download":"https://downloads.wordpress.org/release/wordpress-6.9.4.zip","locale":"en_US","packages":{"full":"https://downloads.wordpress.org/release/wordpress-6.9.4.zip","no_content":"https://downloads.wordpress.org/release/wordpress-6.9.4-no-content.zip","new_bundled":"https://downloads.wordpress.org/release/wordpress-6.9.4-new-bundled.zip","partial":false,"rollback":false},"current":"6.9.4","version":"6.9.4","php_version":"7.2.24","mysql_version":"5.5.5","new_bundled":"6.7","partial_version":false}]}
106+
"""
107+
And that HTTP requests to https://api.wordpress.org/translations/core/1.0/ will respond with:
108+
"""
109+
HTTP/1.1 200 OK
110+
Content-Type: application/json
111+
112+
{"translations":[]}
113+
"""
114+
115+
When I try `wp core download --locale=de_DE --skip-locale-check`
116+
Then the return code should be 1
117+
And STDERR should contain:
118+
"""
119+
Error: The requested locale (de_DE) was not found.
120+
"""
121+
54122
Scenario: Catch download of non-existent WP version
55123
Given an empty directory
56124

src/Core_Command.php

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use WP_CLI\Extractor;
55
use WP_CLI\Iterators\Table as TableIterator;
66
use WP_CLI\Utils;
7+
use WP_CLI\Path;
78
use WP_CLI\Formatter;
89
use WP_CLI\Loggers;
910
use WP_CLI\WpOrgApi;
@@ -149,6 +150,9 @@ public function check_update( $args, $assoc_args ) {
149150
* [--extract]
150151
* : Whether to extract the downloaded file. Defaults to true.
151152
*
153+
* [--skip-locale-check]
154+
* : If specified, allows downloading an older version of WordPress when the requested locale is not available for the latest release.
155+
*
152156
* ## EXAMPLES
153157
*
154158
* $ wp core download --locale=nl_NL
@@ -160,7 +164,7 @@ public function check_update( $args, $assoc_args ) {
160164
* @when before_wp_load
161165
*
162166
* @param array{0?: string} $args Positional arguments.
163-
* @param array{path?: string, locale?: string, version?: string, 'skip-content'?: bool, force?: bool, insecure?: bool, extract?: bool} $assoc_args Associative arguments.
167+
* @param array{path?: string, locale?: string, version?: string, 'skip-content'?: bool, force?: bool, insecure?: bool, extract?: bool, 'skip-locale-check'?: bool} $assoc_args Associative arguments.
164168
*/
165169
public function download( $args, $assoc_args ) {
166170
/**
@@ -233,14 +237,22 @@ public function download( $args, $assoc_args ) {
233237

234238
$download_url = $this->get_download_url( $version, $locale, $extension );
235239
} else {
240+
$wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] );
236241
try {
237-
$offer = ( new WpOrgApi( [ 'insecure' => $insecure ] ) )
238-
->get_core_download_offer( $locale );
242+
$offer = $wp_org_api->get_core_download_offer( $locale );
239243
} catch ( Exception $exception ) {
240244
WP_CLI::error( $exception );
241245
}
242246
if ( ! $offer ) {
243-
WP_CLI::error( "The requested locale ({$locale}) was not found." );
247+
if ( Utils\get_flag_value( $assoc_args, 'skip-locale-check', false ) ) {
248+
$offer = $this->find_latest_offer_for_locale( $locale, $insecure );
249+
if ( is_array( $offer ) ) {
250+
WP_CLI::warning( "The latest WordPress version is not yet available in the {$locale} locale. Downloading version {$offer['current']} instead." );
251+
}
252+
}
253+
if ( ! $offer ) {
254+
WP_CLI::error( "The requested locale ({$locale}) was not found." );
255+
}
244256
}
245257
$version = $offer['current'];
246258
$download_url = $offer['download'];
@@ -703,8 +715,8 @@ private function set_server_url_vars( $url ) {
703715
$_SERVER['SCRIPT_NAME'] = $path;
704716

705717
// Set SCRIPT_FILENAME to the actual WordPress index.php if available.
706-
if ( file_exists( Utils\trailingslashit( ABSPATH ) . 'index.php' ) ) {
707-
$_SERVER['SCRIPT_FILENAME'] = Utils\trailingslashit( ABSPATH ) . 'index.php';
718+
if ( file_exists( Path::trailingslashit( ABSPATH ) . 'index.php' ) ) {
719+
$_SERVER['SCRIPT_FILENAME'] = Path::trailingslashit( ABSPATH ) . 'index.php';
708720
}
709721
}
710722

@@ -1063,7 +1075,7 @@ private static function get_wp_details( $abspath = ABSPATH ) {
10631075
* Gets the template path based on installation type.
10641076
*/
10651077
private static function get_template_path( $template ) {
1066-
$command_root = Utils\phar_safe_path( dirname( __DIR__ ) );
1078+
$command_root = Path::phar_safe( dirname( __DIR__ ) );
10671079
$template_path = "{$command_root}/templates/{$template}";
10681080

10691081
if ( ! file_exists( $template_path ) ) {
@@ -1671,6 +1683,57 @@ private function get_download_url( $version, $locale = 'en_US', $file_type = 'zi
16711683
return "https://{$locale_subdomain}wordpress.org/wordpress-{$version}{$locale_suffix}.{$file_type}";
16721684
}
16731685

1686+
/**
1687+
* Finds the latest available WordPress download offer for a given locale by consulting
1688+
* the WordPress.org translations API.
1689+
*
1690+
* Used as a fallback when the primary version-check API does not return an offer for
1691+
* the requested locale (e.g., when a new WordPress release hasn't been translated yet).
1692+
*
1693+
* @param string $locale Locale to find an offer for.
1694+
* @param bool $insecure Whether to disable SSL verification.
1695+
* @return array{current: string, download: string}|false Offer array on success, false on failure.
1696+
*/
1697+
private function find_latest_offer_for_locale( $locale, $insecure ) {
1698+
$headers = [ 'Accept' => 'application/json' ];
1699+
$options = [
1700+
'timeout' => 30,
1701+
'insecure' => $insecure,
1702+
];
1703+
1704+
try {
1705+
/** @var \WpOrg\Requests\Response $response */
1706+
$response = Utils\http_request( 'GET', 'https://api.wordpress.org/translations/core/1.0/', null, $headers, $options );
1707+
} catch ( Exception $exception ) {
1708+
return false;
1709+
}
1710+
1711+
if ( $response->status_code < 200 || $response->status_code >= 300 ) {
1712+
return false;
1713+
}
1714+
1715+
/** @var array{translations: array<int, array{language: string, version: string}>}|null $body */
1716+
$body = json_decode( $response->body, true );
1717+
1718+
if ( ! is_array( $body ) || empty( $body['translations'] ) ) {
1719+
return false;
1720+
}
1721+
1722+
foreach ( $body['translations'] as $translation ) {
1723+
if (
1724+
isset( $translation['language'], $translation['version'] )
1725+
&& $locale === $translation['language']
1726+
) {
1727+
return [
1728+
'current' => $translation['version'],
1729+
'download' => $this->get_download_url( $translation['version'], $locale, 'zip' ),
1730+
];
1731+
}
1732+
}
1733+
1734+
return false;
1735+
}
1736+
16741737
/**
16751738
* Returns update information.
16761739
*
@@ -2027,7 +2090,7 @@ private function remove_old_files_from_list( $files ) {
20272090
WP_CLI::debug( 'Failed to resolve ABSPATH realpath', 'core' );
20282091
return $count;
20292092
}
2030-
$abspath_realpath_trailing = Utils\trailingslashit( $abspath_realpath );
2093+
$abspath_realpath_trailing = Path::trailingslashit( $abspath_realpath );
20312094

20322095
foreach ( $files as $file ) {
20332096
$file_path = ABSPATH . $file;
@@ -2041,7 +2104,7 @@ private function remove_old_files_from_list( $files ) {
20412104
if ( is_link( $file_path ) ) {
20422105
$normalized_path = realpath( dirname( $file_path ) );
20432106
if ( false === $normalized_path
2044-
|| 0 !== strpos( Utils\trailingslashit( $normalized_path ), $abspath_realpath_trailing )
2107+
|| 0 !== strpos( Path::trailingslashit( $normalized_path ), $abspath_realpath_trailing )
20452108
) {
20462109
WP_CLI::debug( "Skipping symbolic link outside of ABSPATH: {$file}", 'core' );
20472110
continue;
@@ -2057,7 +2120,7 @@ private function remove_old_files_from_list( $files ) {
20572120

20582121
// Regular files/directories: validate real path is within ABSPATH.
20592122
$file_realpath = realpath( $file_path );
2060-
if ( false === $file_realpath || 0 !== strpos( Utils\trailingslashit( $file_realpath ), $abspath_realpath_trailing ) ) {
2123+
if ( false === $file_realpath || 0 !== strpos( Path::trailingslashit( $file_realpath ), $abspath_realpath_trailing ) ) {
20612124
WP_CLI::debug( "Skipping file outside of ABSPATH: {$file}", 'core' );
20622125
continue;
20632126
}
@@ -2093,7 +2156,7 @@ private function remove_directory( $dir, $abspath_realpath_trailing ) {
20932156
WP_CLI::debug( "Failed to resolve realpath for directory: {$dir}", 'core' );
20942157
return false;
20952158
}
2096-
if ( 0 !== strpos( Utils\trailingslashit( $dir_realpath ), $abspath_realpath_trailing ) ) {
2159+
if ( 0 !== strpos( Path::trailingslashit( $dir_realpath ), $abspath_realpath_trailing ) ) {
20972160
WP_CLI::debug( "Attempted to remove directory outside of ABSPATH: {$dir_realpath}", 'core' );
20982161
return false;
20992162
}

0 commit comments

Comments
 (0)