mirror of
https://github.com/apache/struts.git
synced 2026-08-05 14:47:09 +00:00
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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user