fix(i18n): WW-5549 validate locale parameters against supportedLocale (#1602)

When supportedLocale is configured on I18nInterceptor, request_locale
and request_cookie_locale parameters were ignored because
AcceptLanguageLocaleHandler.find() matched the Accept-Language header
before session/cookie handlers checked their explicit locale parameters.
Additionally, stored locales (session/cookie) were never validated
against supportedLocale.

Changes:
- Add isLocaleSupported() helper to validate locales against config
- RequestLocaleHandler.find() now validates against supportedLocale
- AcceptLanguageLocaleHandler.find() checks request_only_locale first,
  then falls back to Accept-Language matching
- SessionLocaleHandler.find() checks request_locale before super.find()
- CookieLocaleHandler.find() checks request_cookie_locale before
  super.find()
- SessionLocaleHandler.read() discards stale session locales
- CookieLocaleHandler.read() discards stale cookie locales

Port of PR #1594 (bug fix only, no refactoring)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Lukasz Lenart
2026-03-06 07:50:38 +01:00
committed by GitHub
parent 66ea9eaf24
commit df97ee23dc
2 changed files with 122 additions and 37 deletions
@@ -65,7 +65,7 @@ public class I18nInterceptor extends AbstractInterceptor {
private Set<Locale> supportedLocale = Collections.emptySet();
protected enum Storage { COOKIE, SESSION, REQUEST, ACCEPT_LANGUAGE }
protected enum Storage {COOKIE, SESSION, REQUEST, ACCEPT_LANGUAGE}
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
@@ -103,10 +103,14 @@ public class I18nInterceptor extends AbstractInterceptor {
*/
public void setSupportedLocale(String supportedLocale) {
this.supportedLocale = TextParseUtil
.commaDelimitedStringToSet(supportedLocale)
.stream()
.map(Locale::new)
.collect(Collectors.toSet());
.commaDelimitedStringToSet(supportedLocale)
.stream()
.map(Locale::new)
.collect(Collectors.toSet());
}
protected boolean isLocaleSupported(Locale locale) {
return supportedLocale.isEmpty() || supportedLocale.contains(locale);
}
@Inject
@@ -222,8 +226,11 @@ public class I18nInterceptor extends AbstractInterceptor {
*/
protected interface LocaleHandler {
Locale find();
Locale read(ActionInvocation invocation);
Locale store(ActionInvocation invocation, Locale locale);
boolean shouldStore();
}
@@ -241,7 +248,10 @@ public class I18nInterceptor extends AbstractInterceptor {
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
return null;
@@ -278,6 +288,11 @@ public class I18nInterceptor extends AbstractInterceptor {
@Override
@SuppressWarnings("rawtypes")
public Locale find() {
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
return requestOnlyLocale;
}
if (!supportedLocale.isEmpty()) {
Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales();
while (locales.hasMoreElements()) {
@@ -287,7 +302,7 @@ public class I18nInterceptor extends AbstractInterceptor {
}
}
}
return super.find();
return null;
}
}
@@ -300,18 +315,18 @@ public class I18nInterceptor extends AbstractInterceptor {
@Override
public Locale find() {
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
LOG.debug("Found locale under request only param, it won't be stored in session!");
shouldStore = false;
return requestOnlyLocale;
}
LOG.debug("Searching locale in request under parameter {}", parameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
shouldStore = false;
return requestOnlyLocale;
}
return null;
@@ -344,7 +359,12 @@ public class I18nInterceptor extends AbstractInterceptor {
Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName);
if (sessionLocale instanceof Locale) {
locale = (Locale) sessionLocale;
LOG.debug("Applied session locale: {}", locale);
if (!isLocaleSupported(locale)) {
LOG.debug("Stored session locale {} is not supported, discarding", locale);
locale = null;
} else {
LOG.debug("Applied session locale: {}", locale);
}
}
}
}
@@ -368,17 +388,18 @@ public class I18nInterceptor extends AbstractInterceptor {
@Override
public Locale find() {
Locale requestOnlySessionLocale = super.find();
if (requestOnlySessionLocale != null) {
shouldStore = false;
return requestOnlySessionLocale;
}
LOG.debug("Searching locale in request under parameter {}", requestCookieParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestCookieParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
shouldStore = false;
return requestOnlyLocale;
}
return null;
@@ -404,6 +425,10 @@ public class I18nInterceptor extends AbstractInterceptor {
for (Cookie cookie : cookies) {
if (attributeName.equals(cookie.getName())) {
locale = getLocaleFromParam(cookie.getValue());
if (locale != null && !isLocaleSupported(locale)) {
LOG.debug("Stored cookie locale {} is not supported, discarding", locale);
locale = null;
}
}
}
}
@@ -205,7 +205,7 @@ public class I18nInterceptorTest extends TestCase {
}
public void testRealLocalesInParams() throws Exception {
Locale[] locales = new Locale[] { Locale.CANADA_FRENCH };
Locale[] locales = new Locale[]{Locale.CANADA_FRENCH};
assertTrue(locales.getClass().isArray());
prepare(I18nInterceptor.DEFAULT_PARAMETER, locales);
interceptor.intercept(mai);
@@ -294,6 +294,66 @@ public class I18nInterceptorTest extends TestCase {
assertEquals(Locale.US, mai.getInvocationContext().getLocale());
}
public void testRequestLocaleWithSupportedLocale() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "de");
// when
interceptor.intercept(mai);
// then
Locale german = new Locale("de");
assertEquals(german, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
assertEquals(german, mai.getInvocationContext().getLocale());
}
public void testUnsupportedRequestLocaleRejected() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "fr");
// when
interceptor.intercept(mai);
// then - fr is not supported, should fall back to default
assertNull("unsupported locale should not be stored", session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
}
public void testStaleSessionLocaleRejected() throws Exception {
// given - session has a stored locale that is no longer supported
session.put(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE, Locale.FRENCH);
interceptor.setSupportedLocale("en,de");
// when
interceptor.intercept(mai);
// then - stored fr locale should be discarded since it's not in supportedLocale
assertFalse("stale session locale should be discarded",
Locale.FRENCH.equals(mai.getInvocationContext().getLocale()));
}
public void testCookieRequestLocaleWithSupportedLocale() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "de");
final Cookie cookie = new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "de");
HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class);
response.addCookie(CookieMatcher.eqCookie(cookie));
EasyMock.replay(response);
ac.put(StrutsStatics.HTTP_RESPONSE, response);
// when
interceptor.intercept(mai);
// then
EasyMock.verify(response);
Locale german = new Locale("de");
assertEquals(german, mai.getInvocationContext().getLocale());
}
private void prepare(String key, Serializable value) {
Map<String, Serializable> params = new HashMap<>();
params.put(key, value);
@@ -308,9 +368,9 @@ public class I18nInterceptorTest extends TestCase {
session = new HashMap<>();
ac = ActionContext.of()
.bind()
.withSession(session)
.withParameters(HttpParameters.create().build());
.bind()
.withSession(session)
.withParameters(HttpParameters.create().build());
request = new MockHttpServletRequest();
request.setSession(new MockHttpSession());
@@ -348,8 +408,8 @@ public class I18nInterceptorTest extends TestCase {
public boolean matches(Object argument) {
Cookie cookie = ((Cookie) argument);
return
(cookie.getName().equals(expected.getName()) &&
cookie.getValue().equals(expected.getValue()));
(cookie.getName().equals(expected.getName()) &&
cookie.getValue().equals(expected.getValue()));
}
public static Cookie eqCookie(Cookie ck) {
@@ -359,10 +419,10 @@ public class I18nInterceptorTest extends TestCase {
public void appendTo(StringBuffer buffer) {
buffer
.append("Received")
.append(expected.getName())
.append("/")
.append(expected.getValue());
.append("Received")
.append(expected.getName())
.append("/")
.append(expected.getValue());
}
}