001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.filter.reactive;
018
019import java.util.Arrays;
020import java.util.Collections;
021import java.util.List;
022import java.util.Locale;
023
024import reactor.core.publisher.Mono;
025
026import org.springframework.http.HttpMethod;
027import org.springframework.http.server.reactive.ServerHttpRequest;
028import org.springframework.util.Assert;
029import org.springframework.util.StringUtils;
030import org.springframework.web.server.ServerWebExchange;
031import org.springframework.web.server.WebFilter;
032import org.springframework.web.server.WebFilterChain;
033
034/**
035 * Reactive {@link WebFilter} that converts posted method parameters into HTTP methods,
036 * retrievable via {@link ServerHttpRequest#getMethod()}. Since browsers currently only
037 * support GET and POST, a common technique is to use a normal POST with an additional
038 * hidden form field ({@code _method}) to pass the "real" HTTP method along.
039 * This filter reads that parameter and changes the {@link ServerHttpRequest#getMethod()}
040 * return value using {@link ServerWebExchange#mutate()}.
041 *
042 * <p>The name of the request parameter defaults to {@code _method}, but can be
043 * adapted via the {@link #setMethodParamName(String) methodParamName} property.
044 *
045 * @author Greg Turnquist
046 * @author Rossen Stoyanchev
047 * @since 5.0
048 */
049public class HiddenHttpMethodFilter implements WebFilter {
050
051        private static final List<HttpMethod> ALLOWED_METHODS =
052                        Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT,
053                                        HttpMethod.DELETE, HttpMethod.PATCH));
054
055        /** Default name of the form parameter with the HTTP method to use. */
056        public static final String DEFAULT_METHOD_PARAMETER_NAME = "_method";
057
058
059        private String methodParamName = DEFAULT_METHOD_PARAMETER_NAME;
060
061
062        /**
063         * Set the name of the form parameter with the HTTP method to use.
064         * <p>By default this is set to {@code "_method"}.
065         */
066        public void setMethodParamName(String methodParamName) {
067                Assert.hasText(methodParamName, "'methodParamName' must not be empty");
068                this.methodParamName = methodParamName;
069        }
070
071
072        /**
073         * Transform an HTTP POST into another method based on {@code methodParamName}.
074         * @param exchange the current server exchange
075         * @param chain provides a way to delegate to the next filter
076         * @return {@code Mono<Void>} to indicate when request processing is complete
077         */
078        @Override
079        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
080
081                if (exchange.getRequest().getMethod() != HttpMethod.POST) {
082                        return chain.filter(exchange);
083                }
084
085                return exchange.getFormData()
086                                .map(formData -> {
087                                        String method = formData.getFirst(this.methodParamName);
088                                        return StringUtils.hasLength(method) ? mapExchange(exchange, method) : exchange;
089                                })
090                                .flatMap(chain::filter);
091        }
092
093        private ServerWebExchange mapExchange(ServerWebExchange exchange, String methodParamValue) {
094                HttpMethod httpMethod = HttpMethod.resolve(methodParamValue.toUpperCase(Locale.ENGLISH));
095                Assert.notNull(httpMethod, () -> "HttpMethod '" + methodParamValue + "' not supported");
096                if (ALLOWED_METHODS.contains(httpMethod)) {
097                        return exchange.mutate().request(builder -> builder.method(httpMethod)).build();
098                }
099                else {
100                        return exchange;
101                }
102        }
103
104}