Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions src/wp-includes/l10n.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* always be filtered using the {@see 'locale'} hook.
*
* @since 1.5.0
* @since 7.2.0 Non-string values are ignored.
*
* @global string $locale The current locale.
* @global string $wp_local_package Locale code of the package.
Expand All @@ -31,8 +32,18 @@ function get_locale() {
global $locale, $wp_local_package;

if ( isset( $locale ) ) {
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 ) ) {
Expand Down Expand Up @@ -66,18 +77,30 @@ function get_locale() {
}
}

if ( empty( $locale ) ) {
/*
* The value may have come from an option, a constant or a global, none of
* which guarantee a type. Callers are documented to receive a string.
*/
if ( empty( $locale ) || ! is_string( $locale ) ) {
$locale = 'en_US';
}

/**
* Filters the locale ID of the WordPress installation.
*
* A value that is not a non-empty string 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;
}

/**
Expand All @@ -87,6 +110,7 @@ function get_locale() {
* returned. Otherwise it returns the locale of get_locale().
*
* @since 4.7.0
* @since 7.2.0 A non-string `locale` user meta value is ignored.
*
* @param int|WP_User $user User's ID or a WP_User object. Defaults to current user.
* @return string The locale of the user.
Expand All @@ -106,15 +130,25 @@ function get_user_locale( $user = 0 ) {
return get_locale();
}

/*
* WP_User has no `locale` property. Reading it runs
* get_user_meta( $user_id, 'locale', true ), so this is a read of untyped
* storage and the row may hold anything, including an array.
*/
$locale = $user_object->locale;

return $locale ? $locale : get_locale();
if ( empty( $locale ) || ! is_string( $locale ) ) {
return get_locale();
}

return $locale;
}

/**
* Determines the current locale desired for the request.
*
* @since 5.0.0
* @since 7.2.0 Non-string values are ignored.
*
* @global string $pagenow The filename of the current screen.
* @global string $wp_local_package Locale code of the package.
Expand Down Expand Up @@ -162,18 +196,26 @@ function determine_locale() {
}
}

if ( ! $determined_locale ) {
if ( empty( $determined_locale ) || ! is_string( $determined_locale ) ) {
$determined_locale = get_locale();
}

/**
* Filters the locale for the current request.
*
* A value that is not a non-empty string 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;
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/wp-includes/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,7 @@ function validate_username( $username ) {
* @since 5.3.0 The `user_activation_key` field can be passed to `$userdata`.
* @since 5.3.0 The `spam` field can be passed to `$userdata` (Multisite only).
* @since 5.9.0 The `meta_input` field can be passed to `$userdata` to allow addition of user meta data.
* @since 7.2.0 A non-string `locale` field is ignored.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
Expand Down Expand Up @@ -2523,7 +2524,8 @@ function wp_insert_user( $userdata ) {

$meta['show_admin_bar_front'] = empty( $userdata['show_admin_bar_front'] ) ? 'true' : $userdata['show_admin_bar_front'];

$meta['locale'] = $userdata['locale'] ?? '';
// The row is read back by get_user_locale(), which is documented to return a string.
$meta['locale'] = isset( $userdata['locale'] ) && is_string( $userdata['locale'] ) ? $userdata['locale'] : '';

$compacted = compact( 'user_pass', 'user_nicename', 'user_email', 'user_url', 'user_registered', 'user_activation_key', 'display_name' );
$data = wp_unslash( $compacted );
Expand Down
146 changes: 146 additions & 0 deletions tests/phpunit/tests/l10n/determineLocale.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,150 @@ public function test_wp_local_package_global_installing() {
wp_installing( true );
$this->assertSame( 'de_DE', determine_locale() );
}

/**
* sanitize_locale_name() applies preg_replace(), which maps over an array
* subject and returns an array, so `wp-login.php?wp_lang[]=de_DE` reaches
* the return statement with an array. No authentication is needed.
*
* @dataProvider data_array_request_value
*
* @param array $value Array request value.
*/
public function test_wp_login_get_param_on_login_page_array( $value ) {
$GLOBALS['pagenow'] = 'wp-login.php';
$_GET['wp_lang'] = $value;

$this->assertSame( 'en_US', determine_locale() );
}

/**
* @dataProvider data_array_request_value
*
* @param array $value Array request value.
*/
public function test_wp_login_cookie_on_login_page_array( $value ) {
$GLOBALS['pagenow'] = 'wp-login.php';
$_COOKIE['wp_lang'] = $value;

$this->assertSame( 'en_US', determine_locale() );
}

/**
* @dataProvider data_array_request_value
*
* @param array $value Array request value.
*/
public function test_language_param_installing_array( $value ) {
$_REQUEST['language'] = $value;
wp_installing( true );

$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 takes down the login page for an anonymous visitor.
*/
public function test_array_wp_lang_param_does_not_fatal_in_the_textdomain_registry() {
$GLOBALS['pagenow'] = 'wp-login.php';
$_GET['wp_lang'] = array( 'de_DE' );

$this->assertSame( 'Some text', __( 'Some text', 'my-login-plugin' ) );
}

/**
* Data provider.
*
* @return array[]
*/
public function data_array_request_value() {
// An empty array is falsy, so it never reaches sanitize_locale_name().
return array(
'a list' => array( array( 'de_DE' ) ),
'a map' => array( array( 'lang' => 'de_DE' ) ),
);
}

/**
* The `$wp_local_package` global is untyped and only checked for truthiness.
*
* @dataProvider data_non_string_locale
*
* @param mixed $value Non-string value.
*/
public function test_wp_local_package_global_installing_non_string( $value ) {
$GLOBALS['wp_local_package'] = $value;
wp_installing( true );
$this->assertSame( 'en_US', determine_locale() );
}

/**
* The `determine_locale` filter result is returned unchecked, unlike
* `pre_determine_locale`, which is guarded with is_string().
*
* @dataProvider data_non_string_locale
*
* @param mixed $value Non-string value.
*/
public function test_ignores_a_non_string_determine_locale_filter( $value ) {
add_filter(
'determine_locale',
static function () use ( $value ) {
return $value;
}
);

$this->assertSame( 'en_US', determine_locale() );
}

/**
* An array `locale` user meta row reaches determine_locale() through
* get_user_locale() on every admin request.
*
* @dataProvider data_non_string_user_locale_meta
*
* @param mixed $meta_value Value stored in the `locale` user meta row.
*/
public function test_returns_a_string_for_a_non_string_user_locale_meta( $meta_value ) {
set_current_screen( 'dashboard' );
wp_set_current_user( self::$user_id );
update_user_meta( self::$user_id, 'locale', $meta_value );

$this->assertSame( 'en_US', determine_locale() );
}

/**
* Data provider.
*
* @return array[]
*/
public function data_non_string_locale() {
return array(
'a list' => array( array( 'de_DE' ) ),
'a map' => array( array( 'locale' => 'de_DE' ) ),
'an empty array' => array( array() ),
'an object' => array( new stdClass() ),
'an integer' => array( 1234 ),
'a float' => array( 1.5 ),
'true' => array( true ),
);
}

/**
* Data provider.
*
* @return array[]
*/
public function data_non_string_user_locale_meta() {
// Scalars survive the meta round trip as strings, so only arrays and
// objects can come back from get_user_meta() with the wrong type.
return array(
'a list' => array( array( 'de_DE' ) ),
'a map' => array( array( 'locale' => 'de_DE' ) ),
'an empty array' => array( array() ),
'an object' => array( new stdClass() ),
);
}
}
Loading
Loading