Browse Source
Provides the ability to embed an HTTP verb inside a hidden form field (like Thymeleaf's th:method operator). Issue: SPR-15206pull/1313/merge
2 changed files with 216 additions and 0 deletions
@ -0,0 +1,97 @@
@@ -0,0 +1,97 @@
|
||||
/* |
||||
* Copyright 2017 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.web.filter.reactive; |
||||
|
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
import org.springframework.web.server.WebFilter; |
||||
import org.springframework.web.server.WebFilterChain; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import java.util.Locale; |
||||
import java.util.Optional; |
||||
|
||||
/** |
||||
* Reactive {@link WebFilter} that converts posted method parameters into HTTP methods, |
||||
* retrievable via {@link ServerHttpRequest#getMethod()}. Since browsers currently only |
||||
* support GET and POST, a common technique - used by the Prototype library, for instance - |
||||
* is to use a normal POST with an additional hidden form field ({@code _method}) |
||||
* to pass the "real" HTTP method along. This filter reads that parameter and changes |
||||
* the {@link ServerHttpRequest#getMethod()} return value using {@link ServerWebExchange#mutate()}. |
||||
* |
||||
* <p>The name of the request parameter defaults to {@code _method}, but can be |
||||
* adapted via the {@link #setMethodParam(String) methodParam} property. |
||||
* |
||||
* @author Greg Turnquist |
||||
* @since 5.0 |
||||
*/ |
||||
public class HiddenHttpMethodFilter implements WebFilter { |
||||
|
||||
/** Default method parameter: {@code _method} */ |
||||
public static final String DEFAULT_METHOD_PARAM = "_method"; |
||||
|
||||
private String methodParam = DEFAULT_METHOD_PARAM; |
||||
|
||||
/** |
||||
* Set the parameter name to look for HTTP methods. |
||||
* @see #DEFAULT_METHOD_PARAM |
||||
*/ |
||||
public void setMethodParam(String methodParam) { |
||||
Assert.hasText(methodParam, "'methodParam' must not be empty"); |
||||
this.methodParam = methodParam; |
||||
} |
||||
|
||||
/** |
||||
* Transform an HTTP POST into another method based on {@code methodParam} |
||||
* |
||||
* @param exchange the current server exchange |
||||
* @param chain provides a way to delegate to the next filter |
||||
* @return |
||||
*/ |
||||
@Override |
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { |
||||
|
||||
if (exchange.getRequest().getMethod() == HttpMethod.POST) { |
||||
return exchange.getFormData() |
||||
.map(map -> Optional.ofNullable(map.getFirst(methodParam))) |
||||
.map(method -> convertedRequest(exchange, method)) |
||||
.then(convertedExchange -> chain.filter(convertedExchange)); |
||||
} else { |
||||
return chain.filter(exchange); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Mutate exchange into a new HTTP method. |
||||
* |
||||
* @param exchange - original request |
||||
* @param method - request HTTP method based on form data |
||||
* @return a mutated {@link ServerWebExchange} |
||||
*/ |
||||
private ServerWebExchange convertedRequest(ServerWebExchange exchange, Optional<String> method) { |
||||
|
||||
String upperMethod = method |
||||
.map(String::toString) |
||||
.orElse(HttpMethod.POST.toString()) |
||||
.toUpperCase(Locale.ENGLISH); |
||||
|
||||
return exchange.mutate() |
||||
.request(builder -> builder.method(HttpMethod.resolve(upperMethod))) |
||||
.build(); |
||||
} |
||||
} |
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
/* |
||||
* Copyright 2017 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.web.filter.reactive; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; |
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
import org.springframework.web.server.WebFilterChain; |
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange; |
||||
import reactor.core.publisher.Mono; |
||||
import reactor.test.StepVerifier; |
||||
|
||||
import java.util.Optional; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* @author Greg Turnquist |
||||
*/ |
||||
public class HiddenHttpMethodFilterTests { |
||||
|
||||
private final HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter(); |
||||
|
||||
@Test |
||||
public void filterWithParameter() { |
||||
ServerWebExchange mockExchange = createExchange(Optional.of("DELETE")); |
||||
|
||||
WebFilterChain filterChain = exchange -> { |
||||
assertEquals("Invalid method", HttpMethod.DELETE, exchange.getRequest().getMethod()); |
||||
return Mono.empty(); |
||||
}; |
||||
|
||||
StepVerifier.create(filter.filter(mockExchange, filterChain)) |
||||
.expectComplete() |
||||
.verify(); |
||||
} |
||||
|
||||
@Test |
||||
public void filterWithNoParameter() { |
||||
ServerWebExchange mockExchange = createExchange(Optional.empty()); |
||||
|
||||
WebFilterChain filterChain = exchange -> { |
||||
assertEquals("Invalid method", HttpMethod.POST, exchange.getRequest().getMethod()); |
||||
return Mono.empty(); |
||||
}; |
||||
|
||||
StepVerifier.create(filter.filter(mockExchange, filterChain)) |
||||
.expectComplete() |
||||
.verify(); |
||||
} |
||||
|
||||
@Test |
||||
public void filterWithDifferentMethodParam() { |
||||
ServerWebExchange mockExchange = createExchange("_foo", Optional.of("DELETE")); |
||||
|
||||
WebFilterChain filterChain = exchange -> { |
||||
assertEquals("Invalid method", HttpMethod.DELETE, exchange.getRequest().getMethod()); |
||||
return Mono.empty(); |
||||
}; |
||||
|
||||
filter.setMethodParam("_foo"); |
||||
|
||||
StepVerifier.create(filter.filter(mockExchange, filterChain)) |
||||
.expectComplete() |
||||
.verify(); |
||||
} |
||||
|
||||
@Test |
||||
public void filterWithoutPost() { |
||||
ServerWebExchange mockExchange = createExchange(Optional.of("DELETE")).mutate() |
||||
.request(builder -> builder.method(HttpMethod.PUT)) |
||||
.build(); |
||||
|
||||
WebFilterChain filterChain = exchange -> { |
||||
assertEquals("Invalid method", HttpMethod.PUT, exchange.getRequest().getMethod()); |
||||
return Mono.empty(); |
||||
}; |
||||
|
||||
StepVerifier.create(filter.filter(mockExchange, filterChain)) |
||||
.expectComplete() |
||||
.verify(); |
||||
} |
||||
|
||||
private ServerWebExchange createExchange(Optional<String> optionalMethod) { |
||||
return createExchange("_method", optionalMethod); |
||||
} |
||||
|
||||
private ServerWebExchange createExchange(String methodName, Optional<String> optionalBody) { |
||||
MockServerHttpRequest.BodyBuilder builder = MockServerHttpRequest |
||||
.post("/hotels") |
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); |
||||
|
||||
MockServerHttpRequest request = optionalBody |
||||
.map(method -> builder.body(methodName + "=" + method)) |
||||
.orElse(builder.build()); |
||||
|
||||
MockServerHttpResponse response = new MockServerHttpResponse(); |
||||
|
||||
return new DefaultServerWebExchange(request, response); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue