diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index abbf18c3b74d2..a341f7f335676 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -2466,6 +2466,11 @@ function sanitize_html_class( $classname, $fallback = '' ) { * @return string The sanitized value. */ function sanitize_locale_name( $locale_name ) { + // Request values can arrive as arrays, and preg_replace() would map over them. + if ( ! is_string( $locale_name ) ) { + return ''; + } + // Limit to A-Z, a-z, 0-9, '_', '-'. $sanitized = preg_replace( '/[^A-Za-z0-9_-]/', '', $locale_name ); diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index 7b9d2652d41dd..0d97ee064ae63 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -31,8 +31,19 @@ function get_locale() { global $locale, $wp_local_package; if ( isset( $locale ) ) { + // The global may be set by wp-config.php, by a plugin, or by an earlier call to this function. + if ( empty( $locale ) || ! is_string( $locale ) ) { + $locale = 'en_US'; + } + /** This filter is documented in wp-includes/l10n.php */ - return apply_filters( 'locale', $locale ); + $filtered_locale = apply_filters( 'locale', $locale ); + + if ( empty( $filtered_locale ) || ! is_string( $filtered_locale ) ) { + return $locale; + } + + return $filtered_locale; } if ( isset( $wp_local_package ) ) { @@ -66,18 +77,27 @@ function get_locale() { } } - if ( empty( $locale ) ) { + // This value may come from a global, a constant or an option, none of which are validated. + if ( empty( $locale ) || ! is_string( $locale ) ) { $locale = 'en_US'; } /** * Filters the locale ID of the WordPress installation. * + * A non-string or empty value is ignored. + * * @since 1.5.0 * * @param string $locale The locale ID. */ - return apply_filters( 'locale', $locale ); + $filtered_locale = apply_filters( 'locale', $locale ); + + if ( empty( $filtered_locale ) || ! is_string( $filtered_locale ) ) { + return $locale; + } + + return $filtered_locale; } /** @@ -106,9 +126,14 @@ function get_user_locale( $user = 0 ) { return get_locale(); } + // WP_User has no `locale` property: this reads the unvalidated `locale` user meta row. $locale = $user_object->locale; - return $locale ? $locale : get_locale(); + if ( empty( $locale ) || ! is_string( $locale ) ) { + return get_locale(); + } + + return $locale; } /** @@ -162,18 +187,27 @@ function determine_locale() { } } - if ( ! $determined_locale ) { + // This value may come from user meta or from a global, and is not validated. + if ( empty( $determined_locale ) || ! is_string( $determined_locale ) ) { $determined_locale = get_locale(); } /** * Filters the locale for the current request. * + * A non-string or empty value is ignored. + * * @since 5.0.0 * * @param string $determined_locale The locale. */ - return apply_filters( 'determine_locale', $determined_locale ); + $filtered_locale = apply_filters( 'determine_locale', $determined_locale ); + + if ( empty( $filtered_locale ) || ! is_string( $filtered_locale ) ) { + return $determined_locale; + } + + return $filtered_locale; } /** diff --git a/tests/phpunit/tests/formatting/sanitizeLocaleName.php b/tests/phpunit/tests/formatting/sanitizeLocaleName.php index cd22acbf2c60a..844584cb65606 100644 --- a/tests/phpunit/tests/formatting/sanitizeLocaleName.php +++ b/tests/phpunit/tests/formatting/sanitizeLocaleName.php @@ -46,4 +46,13 @@ public function data_sanitize_locale_name_returns_empty_string() { array( '@///' ), ); } + + /** + * Request parameters can arrive as arrays. + * + * @ticket 66106 + */ + public function test_sanitize_locale_name_returns_empty_string_for_a_non_string(): void { + $this->assertSame( '', sanitize_locale_name( array( 'de_DE' ) ) ); // @phpstan-ignore argument.type (Passing an array is intentional here.) + } } diff --git a/tests/phpunit/tests/l10n/determineLocale.php b/tests/phpunit/tests/l10n/determineLocale.php index ca8c0bafeccb7..37f638cf0d5dc 100644 --- a/tests/phpunit/tests/l10n/determineLocale.php +++ b/tests/phpunit/tests/l10n/determineLocale.php @@ -308,4 +308,37 @@ public function test_wp_local_package_global_installing() { wp_installing( true ); $this->assertSame( 'de_DE', determine_locale() ); } + + /** + * @ticket 66106 + */ + public function test_wp_login_get_param_on_login_page_array(): void { + $GLOBALS['pagenow'] = 'wp-login.php'; + $_GET['wp_lang'] = array( 'de_DE' ); + + $this->assertSame( 'en_US', determine_locale() ); + } + + /** + * An array locale reaches WP_Textdomain_Registry::set(), which uses it as + * an array key and throws a TypeError, so translating any string for an + * unloaded text domain ends the request. + * + * @ticket 66106 + */ + public function test_array_wp_lang_param_does_not_fatal_in_the_textdomain_registry(): void { + $GLOBALS['pagenow'] = 'wp-login.php'; + $_GET['wp_lang'] = array( 'de_DE' ); + + $this->assertSame( 'Some text', __( 'Some text', 'my-login-plugin' ) ); + } + + /** + * @ticket 66106 + */ + public function test_ignores_a_non_string_determine_locale_filter(): void { + add_filter( 'determine_locale', '__return_empty_array' ); + + $this->assertSame( 'en_US', determine_locale() ); + } } diff --git a/tests/phpunit/tests/l10n/getLocale.php b/tests/phpunit/tests/l10n/getLocale.php index bebae56316b87..d61b1e1bfbc01 100644 --- a/tests/phpunit/tests/l10n/getLocale.php +++ b/tests/phpunit/tests/l10n/getLocale.php @@ -7,76 +7,104 @@ * @covers ::get_locale */ class Tests_L10n_GetLocale extends WP_UnitTestCase { + + /** + * The value of the `$locale` global before the current test ran. + */ + private ?string $original_locale = null; + + /** + * Saves the locale global, which these tests overwrite. + * + * @global string $locale The current locale. + */ + public function set_up(): void { + parent::set_up(); + + global $locale; + + $this->original_locale = $locale; + } + + /** + * Restores the locale global, including after a test fails part way through. + * + * @global string $locale The current locale. + */ + public function tear_down(): void { + global $locale; + + $locale = $this->original_locale; + + parent::tear_down(); + } + + /** + * @global string $locale The current locale. + */ public function test_should_respect_locale_global() { global $locale; - $old_locale = $locale; $locale = 'foo'; - $found = get_locale(); - $locale = $old_locale; - - $this->assertSame( 'foo', $found ); + $this->assertSame( 'foo', get_locale() ); } /** * @group ms-required + * + * @global string $locale The current locale. */ public function test_local_option_should_take_precedence_on_multisite() { global $locale; - $old_locale = $locale; - $locale = null; + + $locale = null; update_option( 'WPLANG', 'en_GB' ); update_site_option( 'WPLANG', 'es_ES' ); - $found = get_locale(); - $locale = $old_locale; - - $this->assertSame( 'en_GB', $found ); + $this->assertSame( 'en_GB', get_locale() ); } /** * @group ms-required + * + * @global string $locale The current locale. */ public function test_network_option_should_be_fallback_on_multisite() { global $locale; - $old_locale = $locale; - $locale = null; - update_site_option( 'WPLANG', 'es_ES' ); + $locale = null; - $found = get_locale(); - $locale = $old_locale; + update_site_option( 'WPLANG', 'es_ES' ); - $this->assertSame( 'es_ES', $found ); + $this->assertSame( 'es_ES', get_locale() ); } /** * @group ms-excluded + * + * @global string $locale The current locale. */ public function test_option_should_be_respected_on_nonmultisite() { global $locale; - $old_locale = $locale; - $locale = null; - update_option( 'WPLANG', 'es_ES' ); + $locale = null; - $found = get_locale(); - $locale = $old_locale; + update_option( 'WPLANG', 'es_ES' ); - $this->assertSame( 'es_ES', $found ); + $this->assertSame( 'es_ES', get_locale() ); } + /** + * @global string $locale The current locale. + */ public function test_should_fall_back_on_en_US() { global $locale; - $old_locale = $locale; - $locale = null; - $found = get_locale(); - $locale = $old_locale; + $locale = null; - $this->assertSame( 'en_US', $found ); + $this->assertSame( 'en_US', get_locale() ); } public function test_should_respect_get_locale_filter() { @@ -90,4 +118,60 @@ public function test_should_respect_get_locale_filter() { public function filter_get_locale() { return 'foo'; } + + /** + * Nothing checks the type of the `WPLANG` option on the way out, so a row + * written by a direct database query or a migration reaches the return value. + * + * @ticket 66106 + * + * @group ms-excluded + * + * @global wpdb $wpdb WordPress database abstraction object. + * @global string $locale The current locale. + */ + public function test_should_fall_back_on_en_US_for_a_non_string_option(): void { + global $locale, $wpdb; + + $locale = null; + + $wpdb->replace( + $wpdb->options, + array( + 'option_name' => 'WPLANG', + 'option_value' => maybe_serialize( array( 'de_DE' ) ), + ) + ); + wp_cache_flush(); + + $this->assertSame( 'en_US', get_locale() ); + } + + /** + * @ticket 66106 + * + * @global string $locale The current locale. + */ + public function test_should_fall_back_on_en_US_for_a_non_string_locale_global(): void { + global $locale; + + $locale = array( 'de_DE' ); + + $this->assertSame( 'en_US', get_locale() ); + } + + /** + * @ticket 66106 + * + * @global string $locale The current locale. + */ + public function test_should_ignore_a_non_string_locale_filter(): void { + global $locale; + + $locale = 'es_ES'; + + add_filter( 'locale', '__return_empty_array' ); + + $this->assertSame( 'es_ES', get_locale() ); + } } diff --git a/tests/phpunit/tests/l10n/getUserLocale.php b/tests/phpunit/tests/l10n/getUserLocale.php index e4eaf7a2601bc..f79864dd25e79 100644 --- a/tests/phpunit/tests/l10n/getUserLocale.php +++ b/tests/phpunit/tests/l10n/getUserLocale.php @@ -146,4 +146,17 @@ public function test_user_id_argument_with_invalid_type() { $user_locale = get_user_locale( 'string' ); $this->assertSame( get_locale(), $user_locale ); } + + /** + * A `locale` user meta row holding an array is truthy, so a truthiness + * check alone passes it through to callers that expect a string. + * + * @ticket 66106 + */ + public function test_returns_site_locale_for_non_string_user_locale_meta(): void { + set_current_screen( 'dashboard' ); + update_user_meta( self::$administrator_de_de, 'locale', array( 'de_DE' ) ); + + $this->assertSame( get_locale(), get_user_locale() ); + } }