diff --git a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java index 12eded344fb4..1259aa439c4d 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java +++ b/spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java @@ -47,12 +47,19 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { private MockServerWebExchange( MockServerHttpRequest request, @Nullable WebSessionManager sessionManager, - @Nullable ApplicationContext applicationContext, @Nullable Principal principal) { + @Nullable ApplicationContext applicationContext , @Nullable Principal principal) { + + this(request, sessionManager, applicationContext, null, principal); + } + + private MockServerWebExchange( + MockServerHttpRequest request, @Nullable WebSessionManager sessionManager, + @Nullable ApplicationContext applicationContext , @Nullable Boolean defaultHtmlEscape, @Nullable Principal principal) { super(request, new MockServerHttpResponse(), sessionManager != null ? sessionManager : new DefaultWebSessionManager(), ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(), - applicationContext); + applicationContext , defaultHtmlEscape); this.principalMono = (principal != null) ? Mono.just(principal) : Mono.empty(); } @@ -125,6 +132,8 @@ public static class Builder { private @Nullable ApplicationContext applicationContext; + private @Nullable Boolean defaultHtmlEscape; + private @Nullable Principal principal; public Builder(MockServerHttpRequest request) { @@ -163,6 +172,18 @@ public Builder applicationContext(ApplicationContext applicationContext) { return this; } + /** + * Set the default HTML escape setting for the exchange. + * @param defaultHtmlEscape whether to enable default HTML escaping, + * or {@code null} if not configured + * @return this builder + * @since 7.0 + */ + public Builder defaultHtmlEscape(@Nullable Boolean defaultHtmlEscape) { + this.defaultHtmlEscape = defaultHtmlEscape; + return this; + } + /** * Provide a user to associate with the exchange. * @param principal the principal to use @@ -178,7 +199,7 @@ public Builder principal(@Nullable Principal principal) { */ public MockServerWebExchange build() { return new MockServerWebExchange( - this.request, this.sessionManager, this.applicationContext, this.principal); + this.request, this.sessionManager, this.applicationContext, this.defaultHtmlEscape, this.principal); } } diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java index 8d5cc409b4ad..092a99efbfea 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchange.java @@ -169,6 +169,15 @@ default Mono cleanupMultipart() { */ @Nullable ApplicationContext getApplicationContext(); + /** + * Return the default HTML escape setting available for the current request, + * or {@code null} if no default was configured at the handler level. + * @return whether default HTML escaping is enabled, or {@code null} if not configured + * @since 7.0 + * @see org.springframework.web.server.adapter.WebHttpHandlerBuilder#defaultHtmlEscape(boolean) + */ + @Nullable Boolean getDefaultHtmlEscape(); + /** * Returns {@code true} if the one of the {@code checkNotModified} methods * in this contract were used and they returned true. diff --git a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java index 6b5d91ec107f..b9f9f9f3d31d 100644 --- a/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java +++ b/spring-web/src/main/java/org/springframework/web/server/ServerWebExchangeDecorator.java @@ -98,6 +98,11 @@ public LocaleContext getLocaleContext() { return getDelegate().getApplicationContext(); } + @Override + public @Nullable Boolean getDefaultHtmlEscape() { + return getDelegate().getDefaultHtmlEscape(); + } + @Override public Mono> getFormData() { return getDelegate().getFormData(); diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 0e3eb72f4fdd..9a0a37f4cdb6 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -101,6 +101,8 @@ public class DefaultServerWebExchange implements ServerWebExchange { private final @Nullable ApplicationContext applicationContext; + private final @Nullable Boolean defaultHtmlEscape; + private volatile boolean notModified; private Function urlTransformer = url -> url; @@ -114,12 +116,19 @@ public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse re WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer, LocaleContextResolver localeContextResolver) { - this(request, response, sessionManager, codecConfigurer, localeContextResolver, null); + this(request, response, sessionManager, codecConfigurer, localeContextResolver, null , null); + } + + public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response, + WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer, + LocaleContextResolver localeContextResolver , @Nullable ApplicationContext applicationContext) { + + this(request, response, sessionManager, codecConfigurer, localeContextResolver, applicationContext , null); } protected DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response, WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer, - LocaleContextResolver localeContextResolver, @Nullable ApplicationContext applicationContext) { + LocaleContextResolver localeContextResolver, @Nullable ApplicationContext applicationContext , @Nullable Boolean defaultHtmlEscape) { Assert.notNull(request, "'request' is required"); Assert.notNull(response, "'response' is required"); @@ -137,6 +146,7 @@ protected DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse this.formDataMono = initFormData(request, codecConfigurer, getLogPrefix()); this.multipartDataMono = initMultipartData(codecConfigurer, getLogPrefix()); this.applicationContext = applicationContext; + this.defaultHtmlEscape = defaultHtmlEscape; if (request instanceof AbstractServerHttpRequest abstractServerHttpRequest) { abstractServerHttpRequest.setAttributesSupplier(() -> this.attributes); @@ -278,6 +288,11 @@ public LocaleContext getLocaleContext() { return this.applicationContext; } + @Override + public @Nullable Boolean getDefaultHtmlEscape() { + return this.defaultHtmlEscape; + } + @Override public boolean isNotModified() { return this.notModified; diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index 06bfecc539e3..f194b95e6a3d 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -99,6 +99,8 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa private @Nullable ApplicationContext applicationContext; + private @Nullable Boolean defaultHtmlEscape; + /** Whether to log potentially sensitive info (form data at DEBUG, headers at TRACE). */ private boolean enableLoggingRequestDetails = false; @@ -250,6 +252,26 @@ public void setApplicationContext(ApplicationContext applicationContext) { return this.applicationContext; } + /** + * Configure a default HTML escape setting to apply to every + * {@link org.springframework.web.server.ServerWebExchange} created + * by this adapter. + * @param defaultHtmlEscape whether to enable default HTML escaping + * @since 7.0 + */ + public void setDefaultHtmlEscape(Boolean defaultHtmlEscape) { + this.defaultHtmlEscape = defaultHtmlEscape; + } + + /** + * Return the configured default HTML escape setting, + * or {@code null} if not configured. + * @since 7.0 + */ + public @Nullable Boolean getDefaultHtmlEscape() { + return this.defaultHtmlEscape; + } + /** * This method must be invoked after all properties have been set to * complete initialization. @@ -300,7 +322,7 @@ public Mono handle(ServerHttpRequest request, ServerHttpResponse response) protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) { return new DefaultServerWebExchange(request, response, this.sessionManager, - getCodecConfigurer(), getLocaleContextResolver(), this.applicationContext); + getCodecConfigurer(), getLocaleContextResolver(), this.applicationContext , this.defaultHtmlEscape); } /** diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java b/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java index fa7b7933b568..89f0f0610f50 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java @@ -91,6 +91,8 @@ public final class WebHttpHandlerBuilder { private final List exceptionHandlers = new ArrayList<>(); + private @Nullable Boolean defaultHtmlEscape; + private @Nullable Function httpHandlerDecorator; private @Nullable WebSessionManager sessionManager; @@ -130,6 +132,7 @@ private WebHttpHandlerBuilder(WebHttpHandlerBuilder other) { this.observationRegistry = other.observationRegistry; this.observationConvention = other.observationConvention; this.httpHandlerDecorator = other.httpHandlerDecorator; + this.defaultHtmlEscape = other.defaultHtmlEscape; } @@ -289,6 +292,26 @@ public boolean hasSessionManager() { return (this.sessionManager != null); } + /** + * Configure a default HTML escape setting to apply to the created + * {@link org.springframework.web.server.ServerWebExchange}. + * @param defaultHtmlEscape whether to enable default HTML escaping + * @return this builder + * @since 7.0 + */ + public WebHttpHandlerBuilder defaultHtmlEscape(Boolean defaultHtmlEscape) { + this.defaultHtmlEscape = defaultHtmlEscape; + return this; + } + + /** + * Return whether a default HTML escape setting has been configured. + * @since 7.0 + */ + public boolean hasDefaultHtmlEscape() { + return (this.defaultHtmlEscape != null); + } + /** * Configure the {@link ServerCodecConfigurer} to set on the {@code WebServerExchange}. * @param codecConfigurer the codec configurer @@ -424,6 +447,9 @@ public HttpHandler build() { if (this.applicationContext != null) { adapted.setApplicationContext(this.applicationContext); } + if(this.defaultHtmlEscape != null) { + adapted.setDefaultHtmlEscape(this.defaultHtmlEscape); + } adapted.afterPropertiesSet(); return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted); diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java index 2662ae5fb524..58b79cb06210 100644 --- a/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java @@ -127,6 +127,48 @@ void cloneWithApplicationContext() { assertThat(((HttpWebHandlerAdapter) builder.clone().build()).getApplicationContext()).isSameAs(context); } + @Test + void defaultHtmlEscape() { + HttpHandler httpHandler = WebHttpHandlerBuilder + .webHandler(exchange -> Mono.empty()) + .defaultHtmlEscape(true) + .build(); + + assertThat(httpHandler).isInstanceOf(HttpWebHandlerAdapter.class); + assertThat(((HttpWebHandlerAdapter) httpHandler).getDefaultHtmlEscape()).isTrue(); + } + + @Test + void defaultHtmlEscapeSetToFalse() { + HttpHandler httpHandler = WebHttpHandlerBuilder + .webHandler(exchange -> Mono.empty()) + .defaultHtmlEscape(false) + .build(); + + assertThat(httpHandler).isInstanceOf(HttpWebHandlerAdapter.class); + assertThat(((HttpWebHandlerAdapter) httpHandler).getDefaultHtmlEscape()).isFalse(); + } + + @Test + void defaultHtmlEscapeNotConfigured() { + HttpHandler httpHandler = WebHttpHandlerBuilder + .webHandler(exchange -> Mono.empty()) + .build(); + + assertThat(httpHandler).isInstanceOf(HttpWebHandlerAdapter.class); + assertThat(((HttpWebHandlerAdapter) httpHandler).getDefaultHtmlEscape()).isNull(); + } + + @Test + void cloneWithDefaultHtmlEscape() { + WebHttpHandlerBuilder builder = WebHttpHandlerBuilder + .webHandler(exchange -> Mono.empty()) + .defaultHtmlEscape(true); + + assertThat(((HttpWebHandlerAdapter) builder.build()).getDefaultHtmlEscape()).isTrue(); + assertThat(((HttpWebHandlerAdapter) builder.clone().build()).getDefaultHtmlEscape()).isTrue(); + } + @Test void httpHandlerDecorator() { BiFunction mutator = diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/server/MockServerWebExchange.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/server/MockServerWebExchange.java index 1078c248743c..4720cf5f6656 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/server/MockServerWebExchange.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/server/MockServerWebExchange.java @@ -40,9 +40,9 @@ public final class MockServerWebExchange extends DefaultServerWebExchange { - private MockServerWebExchange(MockServerHttpRequest request, WebSessionManager sessionManager) { + private MockServerWebExchange(MockServerHttpRequest request, WebSessionManager sessionManager, Boolean defaultHtmlEscape) { super(request, new MockServerHttpResponse(), sessionManager, - ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver()); + ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver() , null, defaultHtmlEscape); } @@ -101,6 +101,8 @@ public static class Builder { private @Nullable WebSessionManager sessionManager; + private @Nullable Boolean defaultHtmlEscape; + public Builder(MockServerHttpRequest request) { this.request = request; @@ -127,12 +129,22 @@ public Builder sessionManager(WebSessionManager sessionManager) { return this; } + /** + * Configure the default HTML escaping setting for the exchange. + * @param defaultHtmlEscape the default HTML escaping setting to use + * @since 5.3.8 + */ + public Builder defaultHtmlEscape(boolean defaultHtmlEscape) { + this.defaultHtmlEscape = defaultHtmlEscape; + return this; + } + /** * Build the {@code MockServerWebExchange} instance. */ public MockServerWebExchange build() { return new MockServerWebExchange(this.request, - this.sessionManager != null ? this.sessionManager : new DefaultWebSessionManager()); + this.sessionManager != null ? this.sessionManager : new DefaultWebSessionManager(),this.defaultHtmlEscape); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java index 2a0929f416aa..4a8ec2b01ed0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java @@ -434,6 +434,11 @@ public LocaleContext getLocaleContext() { return this.delegate.getApplicationContext(); } + @Override + public @Nullable Boolean getDefaultHtmlEscape() { + return this.delegate.getDefaultHtmlEscape(); + } + @Override public boolean isNotModified() { return this.delegate.isNotModified(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java index b0590c53d6cc..048c3f3320a6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java @@ -94,7 +94,7 @@ public RequestContext(ServerWebExchange exchange, Map model, Mes tzaLocaleContext.getTimeZone() : null); this.timeZone = (timeZone != null ? timeZone : TimeZone.getDefault()); - this.defaultHtmlEscape = null; // TODO + this.defaultHtmlEscape = exchange.getDefaultHtmlEscape() != null ? exchange.getDefaultHtmlEscape() : Boolean.FALSE; this.dataValueProcessor = dataValueProcessor; } @@ -150,7 +150,6 @@ public void changeLocale(Locale locale, TimeZone timeZone) { /** * (De)activate default HTML escaping for messages and errors, for the scope * of this RequestContext. - *

TODO: currently no application-wide setting ... */ public void setDefaultHtmlEscape(boolean defaultHtmlEscape) { this.defaultHtmlEscape = defaultHtmlEscape; @@ -165,10 +164,11 @@ public boolean isDefaultHtmlEscape() { } /** - * Return the default HTML escape setting, differentiating between no default - * specified and an explicit value. - * @return whether default HTML escaping is enabled (null = no explicit default) - */ + * Return the default HTML escape setting, differentiating between no default + * specified and an explicit value. + * @return whether default HTML escaping is enabled (null = no explicit default + * specified at the handler level or the request context level) + */ public @Nullable Boolean getDefaultHtmlEscape() { return this.defaultHtmlEscape; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java index a7723f0d8317..1c8b26c2c2d9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java @@ -73,4 +73,45 @@ void testGetContextUrlWithMapEscaping() { assertThat(context.getContextUrl("{foo}?spam={spam}", map)).isEqualTo("/foo/bar%20baz?spam=%26bucket%3D"); } + @Test + void defaultHtmlEscapeNotConfigured() { + RequestContext context = new RequestContext(this.exchange, this.model, this.applicationContext); + assertThat(context.getDefaultHtmlEscape()).isFalse(); + assertThat(context.isDefaultHtmlEscape()).isFalse(); + } + @Test + void defaultHtmlEscapeSetToTrue() { + MockServerWebExchange exchange = MockServerWebExchange.builder( + MockServerHttpRequest.get("/foo/path").contextPath("/foo")) + .defaultHtmlEscape(true) + .build(); + + RequestContext context = new RequestContext(exchange, this.model, this.applicationContext); + assertThat(context.getDefaultHtmlEscape()).isTrue(); + assertThat(context.isDefaultHtmlEscape()).isTrue(); + } + @Test + void defaultHtmlEscapeSetToFalse() { + MockServerWebExchange exchange = MockServerWebExchange.builder( + MockServerHttpRequest.get("/foo/path").contextPath("/foo")) + .defaultHtmlEscape(false) + .build(); + + RequestContext context = new RequestContext(exchange, this.model, this.applicationContext); + assertThat(context.getDefaultHtmlEscape()).isFalse(); + assertThat(context.isDefaultHtmlEscape()).isFalse(); + } + + @Test + void defaultHtmlEscapeOverriddenPerRequest() { + MockServerWebExchange exchange = MockServerWebExchange.builder( + MockServerHttpRequest.get("/foo/path").contextPath("/foo")) + .defaultHtmlEscape(true) + .build(); + + RequestContext context = new RequestContext(exchange, this.model, this.applicationContext); + context.setDefaultHtmlEscape(false); + assertThat(context.getDefaultHtmlEscape()).isFalse(); + assertThat(context.isDefaultHtmlEscape()).isFalse(); + } }