001/* 002 * Copyright 2002-2020 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.reactive.result.view; 018 019import java.net.URI; 020import java.nio.charset.StandardCharsets; 021import java.util.Collections; 022import java.util.Locale; 023import java.util.Map; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026 027import reactor.core.publisher.Mono; 028 029import org.springframework.http.HttpStatus; 030import org.springframework.http.MediaType; 031import org.springframework.http.server.reactive.ServerHttpRequest; 032import org.springframework.http.server.reactive.ServerHttpResponse; 033import org.springframework.lang.Nullable; 034import org.springframework.util.Assert; 035import org.springframework.util.ObjectUtils; 036import org.springframework.util.StringUtils; 037import org.springframework.web.reactive.HandlerMapping; 038import org.springframework.web.server.ServerWebExchange; 039import org.springframework.web.util.UriComponentsBuilder; 040import org.springframework.web.util.UriUtils; 041 042/** 043 * View that redirects to an absolute or context relative URL. The URL may be a 044 * URI template in which case the URI template variables will be replaced with 045 * values from the model or with URI variables from the current request. 046 * 047 * <p>By default {@link HttpStatus#SEE_OTHER} is used but alternate status codes 048 * may be via constructor or setters arguments. 049 * 050 * @author Sebastien Deleuze 051 * @author Rossen Stoyanchev 052 * @since 5.0 053 */ 054public class RedirectView extends AbstractUrlBasedView { 055 056 private static final Pattern URI_TEMPLATE_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)}"); 057 058 059 private HttpStatus statusCode = HttpStatus.SEE_OTHER; 060 061 private boolean contextRelative = true; 062 063 private boolean propagateQuery = false; 064 065 @Nullable 066 private String[] hosts; 067 068 069 /** 070 * Constructor for use as a bean. 071 */ 072 public RedirectView() { 073 } 074 075 /** 076 * Create a new {@code RedirectView} with the given redirect URL. 077 * Status code {@link HttpStatus#SEE_OTHER} is used by default. 078 */ 079 public RedirectView(String redirectUrl) { 080 super(redirectUrl); 081 } 082 083 /** 084 * Create a new {@code RedirectView} with the given URL and an alternate 085 * redirect status code such as {@link HttpStatus#TEMPORARY_REDIRECT} or 086 * {@link HttpStatus#PERMANENT_REDIRECT}. 087 */ 088 public RedirectView(String redirectUrl, HttpStatus statusCode) { 089 super(redirectUrl); 090 setStatusCode(statusCode); 091 } 092 093 094 /** 095 * Set an alternate redirect status code such as 096 * {@link HttpStatus#TEMPORARY_REDIRECT} or 097 * {@link HttpStatus#PERMANENT_REDIRECT}. 098 */ 099 public void setStatusCode(HttpStatus statusCode) { 100 Assert.isTrue(statusCode.is3xxRedirection(), "Not a redirect status code"); 101 this.statusCode = statusCode; 102 } 103 104 /** 105 * Get the redirect status code to use. 106 */ 107 public HttpStatus getStatusCode() { 108 return this.statusCode; 109 } 110 111 /** 112 * Whether to interpret a given redirect URLs that starts with a slash ("/") 113 * as relative to the current context path ({@code true}, the default) or to 114 * the web server root ({@code false}). 115 */ 116 public void setContextRelative(boolean contextRelative) { 117 this.contextRelative = contextRelative; 118 } 119 120 /** 121 * Whether to interpret URLs as relative to the current context path. 122 */ 123 public boolean isContextRelative() { 124 return this.contextRelative; 125 } 126 127 /** 128 * Whether to append the query string of the current URL to the redirect URL 129 * ({@code true}) or not ({@code false}, the default). 130 */ 131 public void setPropagateQuery(boolean propagateQuery) { 132 this.propagateQuery = propagateQuery; 133 } 134 135 /** 136 * Whether the query string of the current URL is appended to the redirect URL. 137 */ 138 public boolean isPropagateQuery() { 139 return this.propagateQuery; 140 } 141 142 /** 143 * Configure one or more hosts associated with the application. 144 * All other hosts will be considered external hosts. 145 * <p>In effect this provides a way turn off encoding for URLs that 146 * have a host and that host is not listed as a known host. 147 * <p>If not set (the default) all redirect URLs are encoded. 148 * @param hosts one or more application hosts 149 */ 150 public void setHosts(@Nullable String... hosts) { 151 this.hosts = hosts; 152 } 153 154 /** 155 * Return the configured application hosts. 156 */ 157 @Nullable 158 public String[] getHosts() { 159 return this.hosts; 160 } 161 162 163 @Override 164 public void afterPropertiesSet() throws Exception { 165 super.afterPropertiesSet(); 166 } 167 168 169 @Override 170 public boolean isRedirectView() { 171 return true; 172 } 173 174 @Override 175 public boolean checkResourceExists(Locale locale) throws Exception { 176 return true; 177 } 178 179 /** 180 * Convert model to request parameters and redirect to the given URL. 181 */ 182 @Override 183 protected Mono<Void> renderInternal( 184 Map<String, Object> model, @Nullable MediaType contentType, ServerWebExchange exchange) { 185 186 String targetUrl = createTargetUrl(model, exchange); 187 return sendRedirect(targetUrl, exchange); 188 } 189 190 /** 191 * Create the target URL and, if necessary, pre-pend the contextPath, expand 192 * URI template variables, append the current request query, and apply the 193 * configured {@link #getRequestDataValueProcessor() 194 * RequestDataValueProcessor}. 195 */ 196 protected final String createTargetUrl(Map<String, Object> model, ServerWebExchange exchange) { 197 String url = getUrl(); 198 Assert.state(url != null, "'url' not set"); 199 200 ServerHttpRequest request = exchange.getRequest(); 201 202 StringBuilder targetUrl = new StringBuilder(); 203 if (isContextRelative() && url.startsWith("/")) { 204 targetUrl.append(request.getPath().contextPath().value()); 205 } 206 targetUrl.append(url); 207 208 if (StringUtils.hasText(targetUrl)) { 209 Map<String, String> uriVars = getCurrentUriVariables(exchange); 210 targetUrl = expandTargetUrlTemplate(targetUrl.toString(), model, uriVars); 211 } 212 213 if (isPropagateQuery()) { 214 targetUrl = appendCurrentRequestQuery(targetUrl.toString(), request); 215 } 216 217 String result = targetUrl.toString(); 218 219 RequestDataValueProcessor processor = getRequestDataValueProcessor(); 220 return (processor != null ? processor.processUrl(exchange, result) : result); 221 } 222 223 private Map<String, String> getCurrentUriVariables(ServerWebExchange exchange) { 224 String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; 225 return exchange.getAttributeOrDefault(name, Collections.emptyMap()); 226 } 227 228 /** 229 * Expand URI template variables in the target URL with either model 230 * attribute values or as a fallback with URI variable values from the 231 * current request. Values are encoded. 232 */ 233 protected StringBuilder expandTargetUrlTemplate(String targetUrl, 234 Map<String, Object> model, Map<String, String> uriVariables) { 235 236 Matcher matcher = URI_TEMPLATE_VARIABLE_PATTERN.matcher(targetUrl); 237 boolean found = matcher.find(); 238 if (!found) { 239 return new StringBuilder(targetUrl); 240 } 241 StringBuilder result = new StringBuilder(); 242 int endLastMatch = 0; 243 while (found) { 244 String name = matcher.group(1); 245 Object value = (model.containsKey(name) ? model.get(name) : uriVariables.get(name)); 246 Assert.notNull(value, () -> "No value for URI variable '" + name + "'"); 247 result.append(targetUrl, endLastMatch, matcher.start()); 248 result.append(encodeUriVariable(value.toString())); 249 endLastMatch = matcher.end(); 250 found = matcher.find(); 251 } 252 result.append(targetUrl, endLastMatch, targetUrl.length()); 253 return result; 254 } 255 256 private String encodeUriVariable(String text) { 257 // Strict encoding of all reserved URI characters 258 return UriUtils.encode(text, StandardCharsets.UTF_8); 259 } 260 261 /** 262 * Append the query of the current request to the target redirect URL. 263 */ 264 protected StringBuilder appendCurrentRequestQuery(String targetUrl, ServerHttpRequest request) { 265 String query = request.getURI().getRawQuery(); 266 if (!StringUtils.hasText(query)) { 267 return new StringBuilder(targetUrl); 268 } 269 270 int index = targetUrl.indexOf('#'); 271 String fragment = (index > -1 ? targetUrl.substring(index) : null); 272 273 StringBuilder result = new StringBuilder(); 274 result.append(index != -1 ? targetUrl.substring(0, index) : targetUrl); 275 result.append(targetUrl.indexOf('?') < 0 ? '?' : '&').append(query); 276 277 if (fragment != null) { 278 result.append(fragment); 279 } 280 281 return result; 282 } 283 284 /** 285 * Send a redirect back to the HTTP client. 286 * @param targetUrl the target URL to redirect to 287 * @param exchange current exchange 288 */ 289 protected Mono<Void> sendRedirect(String targetUrl, ServerWebExchange exchange) { 290 String transformedUrl = (isRemoteHost(targetUrl) ? targetUrl : exchange.transformUrl(targetUrl)); 291 ServerHttpResponse response = exchange.getResponse(); 292 response.getHeaders().setLocation(URI.create(transformedUrl)); 293 response.setStatusCode(getStatusCode()); 294 return Mono.empty(); 295 } 296 297 /** 298 * Whether the given targetUrl has a host that is a "foreign" system in which 299 * case {@link javax.servlet.http.HttpServletResponse#encodeRedirectURL} will not be applied. 300 * This method returns {@code true} if the {@link #setHosts(String[])} 301 * property is configured and the target URL has a host that does not match. 302 * @param targetUrl the target redirect URL 303 * @return {@code true} the target URL has a remote host, {@code false} if it 304 * the URL does not have a host or the "host" property is not configured. 305 */ 306 protected boolean isRemoteHost(String targetUrl) { 307 if (ObjectUtils.isEmpty(this.hosts)) { 308 return false; 309 } 310 String targetHost = UriComponentsBuilder.fromUriString(targetUrl).build().getHost(); 311 if (!StringUtils.hasLength(targetHost)) { 312 return false; 313 } 314 for (String host : this.hosts) { 315 if (targetHost.equals(host)) { 316 return false; 317 } 318 } 319 return true; 320 } 321 322}