I18N: Ignore non-string values in the locale getters - #13335
Draft
sirreal wants to merge 1 commit into
Draft
Conversation
`get_locale()`, `get_user_locale()` and `determine_locale()` are each documented to return a string, and each takes its value from a source that carries no type: the `WPLANG` option and site option, the `WPLANG` constant, the `$locale` and `$wp_local_package` globals, the `locale` user meta row, the `wp_lang` request parameter and cookie, and the `locale`, `option_WPLANG` and `determine_locale` filters. Only `empty()` and truthiness stood in the way, and a non-empty array passes both. A non-string locale reaches `WP_Textdomain_Registry::set()`, which uses it as an array key, so the first just-in-time translation for an unloaded text domain ends the request with "Cannot access offset of type array on array". On `wp-login.php` that needs no authentication: `sanitize_locale_name()` applies `preg_replace()`, which maps over an array subject and returns an array, so `?wp_lang[]=de_DE` carries one straight through. Add `is_string()` beside the non-empty checks that are already there, and fall back the way each function already falls back: to `en_US` for the site locale, to `get_locale()` for the user locale, and to the unfiltered value when a filter returns something else. Shape is left alone; only the type and emptiness are checked, so locales that work today keep working. Also ignore a non-string `locale` field in `wp_insert_user()`, the one core write path that stored it unchecked. That does not repair rows already in the database, which is why the read side is guarded too.
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
get_locale(),get_user_locale()anddetermine_locale()each document@return string, then take that value from theWPLANGoption and site option, theWPLANGconstant, the$localeand$wp_local_packageglobals, thelocaleuser meta row,$_GET['wp_lang'],$_COOKIE['wp_lang'], or thelocale,option_WPLANGanddetermine_localefilters. None of those carry a type, and the only guards areempty()and truthiness, which a non-empty array passes.An array locale reaches
WP_Textdomain_Registry::set(), which uses it as an array key:TypeError: Cannot access offset of type array on array. That fires on the first just-in-time translation for any unloaded text domain. The registry's docblocks declarestring $locale, so the fix belongs upstream of it.Two inputs reach that state, both covered by tests here:
localeuser meta row holding an array.WP_Userhas nolocaleproperty, so$user_object->localeresolves toget_user_meta( $id, 'locale', true )on every read. Admin requests and_locale=userJSON requests routedetermine_locale()throughget_user_locale().wp-login.php?wp_lang[]=de_DE, from an anonymous visitor.sanitize_locale_name()is apreg_replace(), which maps over an array subject and returns an array, andif ( ! $determined_locale )then treats that array as usable.The change
is_string()joins each existing non-empty check. The fallbacks are the ones already in place:en_USfor the site locale,get_locale()for the user locale, the unfiltered value when a filter returns something else. Nothing here inspects the shape of the string, so no locale value in use today changes._doing_it_wrong()would recurse: it calls__(), which comes back throughdetermine_locale()andget_user_locale().wp_insert_user()also drops a non-stringlocalefield, the one core write path for that row without a check; the profile handler,WP_REST_Users_Controllerandregister_new_user()each have one. That write guard cannot stand alone, hence the read-side guards: existing rows are never rewritten,update_user_meta()skipswp_insert_user(), and direct SQL skips core.get_locale()sits the same way, sincesanitize_option()coversupdate_option()but not a direct write or anoption_WPLANGfilter.Testing instructions
l10n/getUserLocale.phpl10n/getLocale.phpl10n/getLocale.php, multisitel10n/determineLocale.phpuser.php, the new test over 7 data setsThe two errors are the reproductions that run through to
WP_Textdomain_Registry. PHPUnit escalates the "Array to string conversion" notice inget_path_from_lang_dir()into an exception, so they stop one frame short of theTypeErrorinset().--group l10n,--group i18nand--group usergain no new failures.Follow-ups
sanitize_locale_name()would rewrite stored locales that currently work, so it wants its own ticket.sanitize_locale_name()returns an array for an array argument under a@return stringdocblock. Its other callers need a look first.WP_Textdomain_Registryagainst its own callers.Sweeping
src/wp-includes/andsrc/wp-admin/for the same shape turned up more reachable cases, mostly guards that were adequate until PHP 8 stopped coercing arrays for internal functions. Separate tickets.Trac ticket: TBD
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Investigation, the patch, the tests and this description, working from my brief and my decisions on where the guard belongs, what it checks and what to leave alone. Every added test was run red against unmodified trunk and green with the patch. This stays a draft until I have reviewed it line by line.
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.