Support changing locale using query language (#6658)

#### What type of PR is this?

/kind improvement
/area core
/milestone 2.20.x

#### What this PR does / why we need it:

This PR adds support changing locale using query `language`. After passing the query, we will automatically respond a cookie `language` back to browser.

Please see the result below:

```bash
http http://localhost:8090/\?language\=zh-CN Accept:text/html -p h

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Language: zh-CN
Content-Type: text/html
Expires: 0
Pragma: no-cache
Referrer-Policy: strict-origin-when-cross-origin
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 0
content-encoding: gzip
content-length: 4765
set-cookie: language=zh-CN; Path=/; Secure
set-cookie: XSRF-TOKEN=f0f2c972-0024-4575-aef2-0609356b4757; Path=/
```

#### Does this PR introduce a user-facing change?

```release-note
支持利用参数 language 切换地域语言
```
pull/6671/head
John Niang 2024-09-14 10:48:29 +08:00 committed by GitHub
parent 46793af0bd
commit c5f9c766bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 149 additions and 0 deletions

View File

@ -0,0 +1,59 @@
package run.halo.app.webfilter;
import static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_COOKIE_NAME;
import static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_PARAMETER_NAME;
import java.util.Locale;
import java.util.Set;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.lang.NonNull;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Component
public class LocaleChangeWebFilter implements WebFilter {
private final ServerWebExchangeMatcher matcher;
public LocaleChangeWebFilter() {
var pathMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
var textHtmlMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
textHtmlMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
matcher = new AndServerWebExchangeMatcher(pathMatcher, textHtmlMatcher);
}
@Override
@NonNull
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
var request = exchange.getRequest();
return matcher.matches(exchange)
.filter(MatchResult::isMatch)
.doOnNext(result -> {
var language = request
.getQueryParams()
.getFirst(LANGUAGE_PARAMETER_NAME);
if (StringUtils.hasText(language)) {
var locale = Locale.forLanguageTag(language);
exchange.getResponse()
.addCookie(ResponseCookie.from(LANGUAGE_COOKIE_NAME, locale.toLanguageTag())
.path("/")
.secure(true)
.build()
);
}
})
.then(Mono.defer(() -> chain.filter(exchange)));
}
}

View File

@ -0,0 +1,90 @@
package run.halo.app.webfilter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
class LocaleChangeWebFilterTest {
LocaleChangeWebFilter filter;
@BeforeEach
void setUp() {
filter = new LocaleChangeWebFilter();
}
@Test
void shouldRespondLanguageCookie() {
WebFilterChain webFilterChain = filterExchange -> {
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
assertNotNull(languageCookie);
assertEquals("zh-CN", languageCookie.getValue());
return Mono.empty();
};
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home")
.accept(MediaType.TEXT_HTML)
.queryParam("language", "zh-CN")
.build()
);
this.filter.filter(exchange, webFilterChain).block();
}
@Test
void shouldRespondLanguageCookieWithUndefinedLanguageTag() {
WebFilterChain webFilterChain = filterExchange -> {
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
assertNotNull(languageCookie);
assertEquals("und", languageCookie.getValue());
return Mono.empty();
};
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home")
.accept(MediaType.TEXT_HTML)
.queryParam("language", "invalid_language_tag")
.build()
);
this.filter.filter(exchange, webFilterChain).block();
}
@ParameterizedTest
@MethodSource("provideInvalidRequest")
void shouldNotRespondLanguageCookieIfRequestNotMatch(MockServerHttpRequest mockRequest) {
WebFilterChain webFilterChain = filterExchange -> {
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
assertNull(languageCookie);
return Mono.empty();
};
var exchange = MockServerWebExchange.from(mockRequest);
this.filter.filter(exchange, webFilterChain).block();
}
static Stream<MockServerHttpRequest> provideInvalidRequest() {
return Stream.of(
MockServerHttpRequest.get("/home")
.accept(MediaType.ALL)
.queryParam("language", "zh-CN")
.build(),
MockServerHttpRequest.get("/home")
.accept(MediaType.APPLICATION_JSON)
.queryParam("language", "zh-CN")
.build(),
MockServerHttpRequest.post("/home")
.accept(MediaType.TEXT_HTML)
.queryParam("language", "zh-CN")
.build(),
MockServerHttpRequest.get("/home")
.accept(MediaType.TEXT_HTML)
.build()
);
}
}