Problem/Motivation
Follow-up for #3569172: Weird language negotiation behavior inside getLanguageSwitchLinks leading to incorrect languages being used
We should avoid the global state change in \Drupal\language\ConfigurableLanguageManager::getLanguageSwitchLinks().
Rendering a node for a given language is a perfectly valid use case and done plenty of times without this workaround (for example in a view of teasers that displays a specific language). I wonder if this global state is even necessary. I think I'd start with removing it and seeing what happens. Maybe this is legacy code that isn't even needed anymore.
Steps to reproduce
Proposed resolution
- The Url object already supports a
languageoption. Extend its use so that on routedUrlobjects, route parameters are set, keyed by each supported language type (with a'_'suffix to avoid collisions), to the value of the specified language - In
AccessManager::checkNamedRoute(), set the matching passed in parameters (e.g.'language_type_', or'language_content_') as route defaults - In EntityConverter and EntityRevisionParamConverted, check whether the matching language route default is set, and get the entity translation for the language for the upcast parameter value
- In AccessArgumentsResolveFactory, make language values available to access callback arguments that are typed LanguageInterface and have names matching language types (e.g.
LanguageInterface $languageContent, orLanguageInterface $language_interface). Name matching supports both camel and snake case
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|
Issue fork drupal-3576381
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
godotislate@nicrodgers in #3573391: getLanguageSwitchLinks() leaks temporary content language into placeholder rendering via Fiber interleaving (wrong translations in breadcrumbs, blocks) uncovered that the global state changing "was introduced in Drupal 9.4.12 by the SA-CORE-2023-003 fix (#2499357: Language switcher block does not adequately check content access when displaying links)." So it will be important not to regress here.
Comment #3
velmir_taky commentedProblem/Motivation
getLanguageSwitchLinks()inConfigurableLanguageManagertemporarily overwrites$this->negotiatedLanguagesto set the "current language" before running$url->access()for each language link, then restores it after the loop.This is a problem with Fibers (#3569172) — if a Fiber yields during the access check, other Fibers see a wrong language in
negotiatedLanguages. Leads to random wrong translations in menus/blocks depending on timing.Proposed resolution
Pass the language through the access chain as data instead of mutating global state.
EntityRepository::getContentLanguageFromContexts()already supports alangcodekey in$contexts— it skipsgetCurrentLanguage()when that key is present. So we just need to get the language from the URL down to that point:-
getLanguageSwitchLinks()— stop mutatingnegotiatedLanguages, just set thelanguageoption on the Url (same option already used for URL generation)-
Url::access()— iflanguageoption is set, add_content_langcodeto route parameters before callingcheckNamedRoute()-
EntityConverter::convert()andEntityRevisionParamConverter::convert()— read_content_langcodefrom defaults and pass aslangcodein contexts toEntityRepositoryWithout
_content_langcode(all normal requests) behavior is unchanged —EntityRepositoryfalls back togetCurrentLanguage()as before.No API/BC breaks — no interface signatures change.
Comment #5
velmir_taky commentedComment #6
godotislateNice work, @velmir_taky! I think this is a clever approach that covers the vast majority of use cases, but here are couple that aren't covered that I think are worth bringing up:
It's possible that these are pretty edge-casey, but it would be a change in behavior here regardless.
The other comment on the MR is that the test does not confirm that the negotiated languages did not mutate during the access test. It only show that the negotiated language has not changed upon exit of the method. I've confirmed this by running the test only job and seeing that it passes: https://git.drupalcode.org/issue/drupal-3576381/-/jobs/8702779
Comment #7
velmir_taky commented@godotislate thanks for the review!
Test fix: Rewrote the test. The old one only checked state before/after
getLanguageSwitchLinks()- confirmed it passes on unpatched code as you noted. The new approach registers a custom access checker (LanguageStateCapturingAccessCheck) inlanguage_testthat snapshotsnegotiatedLanguageslangcodes into State during eachUrl::access()call. The test then asserts:- the checker was actually invoked
- it saw
TYPE_CONTENT = 'fr'when checking the French link- full state is restored after the method returns
Coverage gap: Agreed,
_content_langcodealone isn't enough — it's filtered byRouteMatchand never reaches access checkers. Fixed by adding save/restore ofnegotiatedLanguagesingetLanguageSwitchLinks(): before thearray_filterloop we snapshot the state, temporarily setnegotiatedLanguages[TYPE_CONTENT]to the link's language before each access check, and restore right after. SogetCurrentLanguage(TYPE_CONTENT)inside any access checker now returns the correct language per link — handles both your examples (entity routes with extra language-based access, and non-entity routes like Views/Webform pages checking config translation availability)._content_langcodeis kept for entity param converters.Comment #8
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #10
velmir_taky commentedThanks @godotislate for picking this up, and for the new approach here.
Carrying the language through the access stack as data — instead of temporarily changing the negotiated language state — is clearly the better direction, and it lines up with what @berdir outlined in the summary. My !14950 still leans on swapping the negotiated languages, so it doesn't really get us to that goal.
So I'll close !14950 in favour of !16451 and we can consolidate on the cleaner approach. Happy to help review or test it whenever that's useful — just say the word.
Thanks again for moving this forward.
Comment #12
godotislateHave a draft MR up: https://git.drupalcode.org/project/drupal/-/merge_requests/16451
Took inspiration from @velmir_taky's work and expanded on it a little so that route access callbacks can have the language objects passed to them for any language-based logic.
Tests pass for now, but I think I might need to tweak how the languages are passed from the Url Object to the Route object to the param converters and argument resolver. I'll have to think that over for later because I'm tired now, so leaving as NW.
This will also need a CR and IS update, so tagging for those.
And thanks to @velmir_taky for the original work
Comment #13
godotislateLooking at the MR again after a day, I think it's ready for review at least.
IS updated with proposed solution.
CR created: https://www.drupal.org/node/3613488
Comment #14
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. The merge request has merge conflicts and cannot be merged. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #15
godotislateRebased for performance test class move.
Comment #16
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #17
godotislateSwitched to using
LanguageManagerInterface::getDefinedLanguageTypesInfoinstead ofgetLanguageTypes, because with the ConfigurableLanguageManager, TYPE_CONTENT might not be returned if it's not configurable.