This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch fix/WW-5549-i18n-supported-locale-s6 in repository https://gitbox.apache.org/repos/asf/struts.git
commit 61527fc5a327ac8fe160ab167d894893db780889 Author: Lukasz Lenart <[email protected]> AuthorDate: Fri Feb 27 11:58:59 2026 +0100 fix(i18n): WW-5549 validate locale parameters against supportedLocale 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 <[email protected]> --- .../struts2/interceptor/I18nInterceptor.java | 75 +++++++++++++------- .../struts2/interceptor/I18nInterceptorTest.java | 80 +++++++++++++++++++--- 2 files changed, 120 insertions(+), 35 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java index 6bce04244..e2aedba1d 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java @@ -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,20 +315,20 @@ public class I18nInterceptor extends AbstractInterceptor { @Override public Locale find() { - Locale requestOnlyLocale = super.find(); + Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName); + if (requestedLocale.isDefined()) { + Locale locale = getLocaleFromParam(requestedLocale.getValue()); + if (locale != null && isLocaleSupported(locale)) { + return locale; + } + } + 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()); - } - 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; + } } } } diff --git a/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java index 604d61be6..3a592574d 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java @@ -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()); } }
