Skip to content

Revise CookieLocaleResolver to use ResponseCookie #28779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,13 +30,12 @@
* given response.
*
* <p>Can serve as base class for components that generate specific cookies,
* such as CookieLocaleResolver and CookieThemeResolver.
* such as CookieThemeResolver.
*
* @author Juergen Hoeller
* @since 1.1.4
* @see #addCookie
* @see #removeCookie
* @see org.springframework.web.servlet.i18n.CookieLocaleResolver
* @see org.springframework.web.servlet.theme.CookieThemeResolver
*/
public class CookieGenerator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,25 @@

package org.springframework.web.servlet.i18n;

import java.time.Duration;
import java.util.Locale;
import java.util.TimeZone;
import java.util.function.Function;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.util.CookieGenerator;
import org.springframework.web.util.WebUtils;

/**
Expand All @@ -57,7 +59,7 @@
* @see #setDefaultLocale
* @see #setDefaultTimeZone
*/
public class CookieLocaleResolver extends CookieGenerator implements LocaleContextResolver {
public class CookieLocaleResolver extends AbstractLocaleContextResolver {

/**
* The name of the request attribute that holds the {@code Locale}.
Expand Down Expand Up @@ -86,16 +88,39 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
*/
public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";

/**
* The default cookie max age (persists until browser shutdown) if none is
* explicitly set.
*/
public static final Duration DEFAULT_COOKIE_MAX_AGE = Duration.ofSeconds(-1);

private boolean languageTagCompliant = true;
/**
* The default cookie path used if none is explicitly set.
*/
public static final String DEFAULT_COOKIE_PATH = "/";

private boolean rejectInvalidCookies = true;
private static final Log logger = LogFactory.getLog(CookieLocaleResolver.class);


private String cookieName;

private Duration cookieMaxAge = DEFAULT_COOKIE_MAX_AGE;

@Nullable
private Locale defaultLocale;
private String cookiePath = DEFAULT_COOKIE_PATH;

@Nullable
private TimeZone defaultTimeZone;
private String cookieDomain;

private boolean cookieSecure;

private boolean cookieHttpOnly;

private String cookieSameSite = "Lax";

private boolean languageTagCompliant = true;

private boolean rejectInvalidCookies = true;

private Function<HttpServletRequest, Locale> defaultLocaleFunction = request -> {
Locale defaultLocale = getDefaultLocale();
Expand All @@ -104,15 +129,99 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte

private Function<HttpServletRequest, TimeZone> defaultTimeZoneFunction = request -> getDefaultTimeZone();


/**
* Create a new instance of {@link CookieLocaleResolver} using the supplied
* cookie name.
* @param cookieName the cookie name
* @since 6.0
*/
public CookieLocaleResolver(String cookieName) {
Assert.notNull(cookieName, "cookieName must not be null");
this.cookieName = cookieName;
}

/**
* Create a new instance of {@link CookieLocaleResolver} using the
* {@linkplain #DEFAULT_COOKIE_NAME default cookie name}.
*/
public CookieLocaleResolver() {
setCookieName(DEFAULT_COOKIE_NAME);
this(DEFAULT_COOKIE_NAME);
}


/**
* Set the name of cookie created by this resolver.
* @param cookieName the cookie name
* @deprecated since 6.0, in favor of {@link #CookieLocaleResolver(String)}
*/
@Deprecated
public void setCookieName(String cookieName) {
Assert.notNull(cookieName, "cookieName must not be null");
this.cookieName = cookieName;
}

/**
* Set the cookie "Max-Age" attribute.
* @since 6.0
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#maxAge(Duration)
*/
public void setCookieMaxAge(Duration cookieMaxAge) {
Assert.notNull(cookieMaxAge, "cookieMaxAge must not be null");
this.cookieMaxAge = cookieMaxAge;
}

/**
* Variant of {@link #setCookieMaxAge(Duration)} accepting a value in
* seconds.
*/
@Deprecated
public void setCookieMaxAge(@Nullable Integer cookieMaxAge) {
setCookieMaxAge(Duration.ofSeconds((cookieMaxAge != null) ? cookieMaxAge : -1));
}

/**
* Set the cookie "Path" attribute.
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#path(String)
*/
public void setCookiePath(@Nullable String cookiePath) {
this.cookiePath = cookiePath;
}

/**
* Set the cookie "Domain" attribute.
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#domain(String)
*/
public void setCookieDomain(@Nullable String cookieDomain) {
this.cookieDomain = cookieDomain;
}

/**
* Add the "Secure" attribute to the cookie.
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#secure(boolean)
*/
public void setCookieSecure(boolean cookieSecure) {
this.cookieSecure = cookieSecure;
}

/**
* Add the "HttpOnly" attribute to the cookie.
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#httpOnly(boolean)
*/
public void setCookieHttpOnly(boolean cookieHttpOnly) {
this.cookieHttpOnly = cookieHttpOnly;
}

/**
* Add the "SameSite" attribute to the cookie.
* @since 6.0
* @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#sameSite(String)
*/
public void setCookieSameSite(String cookieSameSite) {
Assert.notNull(cookieSameSite, "cookieSameSite must not be null");
this.cookieSameSite = cookieSameSite;
}

/**
* Specify whether this resolver's cookies should be compliant with BCP 47
* language tags instead of Java's legacy locale specification format.
Expand Down Expand Up @@ -161,40 +270,6 @@ public boolean isRejectInvalidCookies() {
return this.rejectInvalidCookies;
}

/**
* Set a fixed locale that this resolver will return if no cookie is found.
*/
public void setDefaultLocale(@Nullable Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}

/**
* Return the fixed locale that this resolver will return if no cookie is found,
* if any.
*/
@Nullable
protected Locale getDefaultLocale() {
return this.defaultLocale;
}

/**
* Set a fixed time zone that this resolver will return if no cookie is found.
* @since 4.0
*/
public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) {
this.defaultTimeZone = defaultTimeZone;
}

/**
* Return the fixed time zone that this resolver will return if no cookie is found,
* if any.
* @since 4.0
*/
@Nullable
protected TimeZone getDefaultTimeZone() {
return this.defaultTimeZone;
}

/**
* Set the function used to determine the default locale for the given request,
* called if no locale cookie has been found.
Expand Down Expand Up @@ -256,46 +331,43 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
TimeZone timeZone = null;

// Retrieve and parse cookie value.
String cookieName = getCookieName();
if (cookieName != null) {
Cookie cookie = WebUtils.getCookie(request, cookieName);
if (cookie != null) {
String value = cookie.getValue();
String localePart = value;
String timeZonePart = null;
int separatorIndex = localePart.indexOf('/');
if (separatorIndex == -1) {
// Leniently accept older cookies separated by a space...
separatorIndex = localePart.indexOf(' ');
}
if (separatorIndex >= 0) {
localePart = value.substring(0, separatorIndex);
timeZonePart = value.substring(separatorIndex + 1);
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie != null) {
String value = cookie.getValue();
String localePart = value;
String timeZonePart = null;
int separatorIndex = localePart.indexOf('/');
if (separatorIndex == -1) {
// Leniently accept older cookies separated by a space...
separatorIndex = localePart.indexOf(' ');
}
if (separatorIndex >= 0) {
localePart = value.substring(0, separatorIndex);
timeZonePart = value.substring(separatorIndex + 1);
}
try {
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
if (timeZonePart != null) {
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
}
try {
locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
if (timeZonePart != null) {
timeZone = StringUtils.parseTimeZoneString(timeZonePart);
}
}
catch (IllegalArgumentException ex) {
if (isRejectInvalidCookies() &&
request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
throw new IllegalStateException("Encountered invalid locale cookie '" +
this.cookieName + "': [" + value + "] due to: " + ex.getMessage());
}
catch (IllegalArgumentException ex) {
if (isRejectInvalidCookies() &&
request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
throw new IllegalStateException("Encountered invalid locale cookie '" +
cookieName + "': [" + value + "] due to: " + ex.getMessage());
}
else {
// Lenient handling (e.g. error dispatch): ignore locale/timezone parse exceptions
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid locale cookie '" + cookieName +
"': [" + value + "] due to: " + ex.getMessage());
}
else {
// Lenient handling (e.g. error dispatch): ignore locale/timezone parse exceptions
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid locale cookie '" + this.cookieName +
"': [" + value + "] due to: " + ex.getMessage());
}
}
if (logger.isTraceEnabled()) {
logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale +
"'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : ""));
}
}
if (logger.isTraceEnabled()) {
logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale +
"'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : ""));
}
}

Expand All @@ -306,11 +378,6 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) {
}
}

@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
}

@Override
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,
@Nullable LocaleContext localeContext) {
Expand All @@ -319,17 +386,35 @@ public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletRe

Locale locale = null;
TimeZone timeZone = null;
ResponseCookie cookie;
if (localeContext != null) {
locale = localeContext.getLocale();
if (localeContext instanceof TimeZoneAwareLocaleContext timeZoneAwareLocaleContext) {
timeZone = timeZoneAwareLocaleContext.getTimeZone();
}
addCookie(response,
(locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? '/' + timeZone.getID() : ""));
cookie = ResponseCookie.from(this.cookieName,
(locale != null ? toLocaleValue(locale) : "-") +
(timeZone != null ? '/' + timeZone.getID() : ""))
.maxAge(this.cookieMaxAge)
.path(this.cookiePath)
.domain(this.cookieDomain)
.secure(this.cookieSecure)
.httpOnly(this.cookieHttpOnly)
.sameSite(this.cookieSameSite)
.build();
}
else {
removeCookie(response);
// a cookie with empty value and max age 0
cookie = ResponseCookie.from(this.cookieName, "")
.maxAge(Duration.ZERO)
.path(this.cookiePath)
.domain(this.cookieDomain)
.secure(this.cookieSecure)
.httpOnly(this.cookieHttpOnly)
.sameSite(this.cookieSameSite)
.build();
}
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
(locale != null ? locale : this.defaultLocaleFunction.apply(request)));
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
Expand Down
Loading